mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-01-15 11:18:41 +00:00
started with #203
This commit is contained in:
parent
ab125042e2
commit
6c630c78b5
17 changed files with 1287 additions and 335 deletions
|
|
@ -8,7 +8,6 @@ import 'package:twonly/src/services/api/media_send.dart';
|
|||
import 'package:twonly/src/services/api.service.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:twonly/src/providers/connection.provider.dart';
|
||||
import 'package:twonly/src/utils/hive.dart';
|
||||
import 'package:twonly/src/providers/settings.provider.dart';
|
||||
import 'package:twonly/src/services/fcm.service.dart';
|
||||
import 'package:twonly/src/services/notification.service.dart';
|
||||
|
|
@ -33,9 +32,9 @@ void main() async {
|
|||
// Load the user's preferred theme while the splash screen is displayed.
|
||||
// This prevents a sudden theme change when the app is first displayed.
|
||||
await settingsController.loadSettings();
|
||||
await SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);
|
||||
|
||||
await setupPushNotification();
|
||||
await initMediaStorage();
|
||||
setupPushNotification();
|
||||
|
||||
gCameras = await availableCameras();
|
||||
|
||||
|
|
@ -48,8 +47,6 @@ void main() async {
|
|||
purgeReceivedMediaFiles();
|
||||
purgeSendMediaFiles();
|
||||
|
||||
await SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);
|
||||
|
||||
runApp(
|
||||
MultiProvider(
|
||||
providers: [
|
||||
|
|
|
|||
44
lib/src/database/daos/message_retransmissions.dao.dart
Normal file
44
lib/src/database/daos/message_retransmissions.dao.dart
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
import 'package:drift/drift.dart';
|
||||
import 'package:twonly/src/database/tables/message_retransmissions.dart';
|
||||
import 'package:twonly/src/database/twonly_database.dart';
|
||||
import 'package:twonly/src/utils/log.dart';
|
||||
|
||||
part 'message_retransmissions.dao.g.dart';
|
||||
|
||||
@DriftAccessor(tables: [MessageRetransmissions])
|
||||
class MessageRetransmissionDao extends DatabaseAccessor<TwonlyDatabase>
|
||||
with _$MessageRetransmissionDaoMixin {
|
||||
// this constructor is required so that the main database can create an instance
|
||||
// of this object.
|
||||
MessageRetransmissionDao(super.db);
|
||||
|
||||
Future<int?> insertRetransmission(
|
||||
MessageRetransmissionsCompanion message) async {
|
||||
try {
|
||||
return await into(messageRetransmissions).insert(message);
|
||||
} catch (e) {
|
||||
Log.error("Error while inserting message for retransmission: $e");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<int>> getRetransmitAbleMessages() async {
|
||||
return (await (select(messageRetransmissions)
|
||||
..where((t) => t.acknowledgeByServerAt.isNull()))
|
||||
.get())
|
||||
.map((msg) => msg.retransmissionId)
|
||||
.toList();
|
||||
}
|
||||
|
||||
SingleOrNullSelectable<MessageRetransmission> getRetransmissionById(
|
||||
int retransmissionId) {
|
||||
return select(messageRetransmissions)
|
||||
..where((t) => t.retransmissionId.equals(retransmissionId));
|
||||
}
|
||||
|
||||
Future deleteRetransmissionById(int retransmissionId) {
|
||||
return (delete(messageRetransmissions)
|
||||
..where((t) => t.retransmissionId.equals(retransmissionId)))
|
||||
.go();
|
||||
}
|
||||
}
|
||||
11
lib/src/database/daos/message_retransmissions.dao.g.dart
Normal file
11
lib/src/database/daos/message_retransmissions.dao.g.dart
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'message_retransmissions.dao.dart';
|
||||
|
||||
// ignore_for_file: type=lint
|
||||
mixin _$MessageRetransmissionDaoMixin on DatabaseAccessor<TwonlyDatabase> {
|
||||
$ContactsTable get contacts => attachedDatabase.contacts;
|
||||
$MessagesTable get messages => attachedDatabase.messages;
|
||||
$MessageRetransmissionsTable get messageRetransmissions =>
|
||||
attachedDatabase.messageRetransmissions;
|
||||
}
|
||||
19
lib/src/database/tables/message_retransmissions.dart
Normal file
19
lib/src/database/tables/message_retransmissions.dart
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import 'package:drift/drift.dart';
|
||||
import 'package:twonly/src/database/tables/contacts_table.dart';
|
||||
import 'package:twonly/src/database/tables/messages_table.dart';
|
||||
|
||||
@DataClassName('MessageRetransmission')
|
||||
class MessageRetransmissions extends Table {
|
||||
IntColumn get retransmissionId => integer().autoIncrement()();
|
||||
IntColumn get contactId =>
|
||||
integer().references(Contacts, #userId, onDelete: KeyAction.cascade)();
|
||||
|
||||
IntColumn get messageId => integer()
|
||||
.nullable()
|
||||
.references(Messages, #messageId, onDelete: KeyAction.cascade)();
|
||||
|
||||
BlobColumn get plaintextContent => blob()();
|
||||
BlobColumn get pushData => blob().nullable()();
|
||||
|
||||
DateTimeColumn get acknowledgeByServerAt => dateTime().nullable()();
|
||||
}
|
||||
|
|
@ -5,11 +5,13 @@ import 'package:path_provider/path_provider.dart';
|
|||
import 'package:twonly/src/database/daos/contacts_dao.dart';
|
||||
import 'package:twonly/src/database/daos/media_downloads_dao.dart';
|
||||
import 'package:twonly/src/database/daos/media_uploads_dao.dart';
|
||||
import 'package:twonly/src/database/daos/message_retransmissions.dao.dart';
|
||||
import 'package:twonly/src/database/daos/messages_dao.dart';
|
||||
import 'package:twonly/src/database/daos/signal_dao.dart';
|
||||
import 'package:twonly/src/database/tables/contacts_table.dart';
|
||||
import 'package:twonly/src/database/tables/media_download_table.dart';
|
||||
import 'package:twonly/src/database/tables/media_uploads_table.dart';
|
||||
import 'package:twonly/src/database/tables/message_retransmissions.dart';
|
||||
import 'package:twonly/src/database/tables/messages_table.dart';
|
||||
import 'package:twonly/src/database/tables/signal_contact_prekey_table.dart';
|
||||
import 'package:twonly/src/database/tables/signal_contact_signed_prekey_table.dart';
|
||||
|
|
@ -32,13 +34,15 @@ part 'twonly_database.g.dart';
|
|||
SignalSenderKeyStores,
|
||||
SignalSessionStores,
|
||||
SignalContactPreKeys,
|
||||
SignalContactSignedPreKeys
|
||||
SignalContactSignedPreKeys,
|
||||
MessageRetransmissions
|
||||
], daos: [
|
||||
MessagesDao,
|
||||
ContactsDao,
|
||||
MediaUploadsDao,
|
||||
MediaDownloadsDao,
|
||||
SignalDao
|
||||
SignalDao,
|
||||
MessageRetransmissionDao
|
||||
])
|
||||
class TwonlyDatabase extends _$TwonlyDatabase {
|
||||
TwonlyDatabase([QueryExecutor? e])
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -26,9 +26,9 @@ import 'package:twonly/src/services/api/server_messages.dart';
|
|||
import 'package:twonly/src/services/signal/identity.signal.dart';
|
||||
import 'package:twonly/src/services/signal/prekeys.signal.dart';
|
||||
import 'package:twonly/src/services/signal/utils.signal.dart';
|
||||
import 'package:twonly/src/utils/hive.dart';
|
||||
import 'package:twonly/src/services/fcm.service.dart';
|
||||
import 'package:twonly/src/services/flame.service.dart';
|
||||
import 'package:twonly/src/utils/keyvalue.dart';
|
||||
import 'package:twonly/src/utils/log.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
import 'package:twonly/src/utils/storage.dart';
|
||||
|
|
@ -38,6 +38,7 @@ import 'package:libsignal_protocol_dart/src/ecc/ed25519.dart';
|
|||
import 'package:web_socket_channel/web_socket_channel.dart';
|
||||
|
||||
final lockConnecting = Mutex();
|
||||
final lockRetransStore = Mutex();
|
||||
|
||||
/// The ApiProvider is responsible for communicating with the server.
|
||||
/// It handles errors and does automatically tries to reconnect on
|
||||
|
|
@ -203,39 +204,43 @@ class ApiService {
|
|||
}
|
||||
|
||||
Future<Map<String, dynamic>> getRetransmission() async {
|
||||
final box = await getMediaStorage();
|
||||
try {
|
||||
return box.get("rawbytes-to-retransmit");
|
||||
} catch (e) {
|
||||
return {};
|
||||
}
|
||||
return await KeyValueStore.get("rawbytes-to-retransmit") ?? {};
|
||||
}
|
||||
|
||||
Future retransmitRawBytes() async {
|
||||
var retransmit = await getRetransmission();
|
||||
Log.info("retransmitting ${retransmit.keys.length} messages");
|
||||
for (final seq in retransmit.keys) {
|
||||
try {
|
||||
_channel!.sink.add(base64Decode(retransmit[seq]));
|
||||
} catch (e) {
|
||||
Log.error("$e");
|
||||
await lockRetransStore.protect(() async {
|
||||
var retransmit = await getRetransmission();
|
||||
Log.info("retransmitting ${retransmit.keys.length} messages");
|
||||
bool gotError = false;
|
||||
for (final seq in retransmit.keys) {
|
||||
try {
|
||||
_channel!.sink.add(base64Decode(retransmit[seq]));
|
||||
} catch (e) {
|
||||
gotError = true;
|
||||
Log.error("$e");
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!gotError) {
|
||||
KeyValueStore.put("rawbytes-to-retransmit", {});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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);
|
||||
await lockRetransStore.protect(() async {
|
||||
var retransmit = await getRetransmission();
|
||||
retransmit[seq.toString()] = base64Encode(bytes);
|
||||
KeyValueStore.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);
|
||||
await lockRetransStore.protect(() async {
|
||||
var retransmit = await getRetransmission();
|
||||
if (retransmit.isEmpty) return;
|
||||
retransmit.remove(seq.toString());
|
||||
KeyValueStore.put("rawbytes-to-retransmit", retransmit);
|
||||
});
|
||||
}
|
||||
|
||||
Future<Result> sendRequestSync(
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
import 'dart:convert';
|
||||
import 'dart:math';
|
||||
import 'dart:io';
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:hive/hive.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';
|
||||
|
|
@ -11,192 +9,79 @@ import 'package:twonly/src/model/json/userdata.dart';
|
|||
import 'package:twonly/src/model/protobuf/api/error.pb.dart';
|
||||
import 'package:twonly/src/services/api/utils.dart';
|
||||
import 'package:twonly/src/services/signal/encryption.signal.dart';
|
||||
import 'package:twonly/src/utils/hive.dart';
|
||||
import 'package:twonly/src/services/notification.service.dart';
|
||||
import 'package:twonly/src/utils/log.dart';
|
||||
import 'package:twonly/src/utils/storage.dart';
|
||||
|
||||
final lockSendingMessages = Mutex();
|
||||
|
||||
Future tryTransmitMessages() async {
|
||||
lockSendingMessages.protect(() async {
|
||||
Map<String, dynamic> retransmit = await getAllMessagesForRetransmitting();
|
||||
final retransIds =
|
||||
await twonlyDB.messageRetransmissionDao.getRetransmitAbleMessages();
|
||||
|
||||
if (retransmit.isEmpty) return;
|
||||
|
||||
Log.info("try sending messages: ${retransmit.length}");
|
||||
|
||||
Map<String, dynamic> failed = {};
|
||||
|
||||
List<MapEntry<String, dynamic>> sortedList = retransmit.entries.toList()
|
||||
..sort((a, b) => int.parse(a.key).compareTo(int.parse(b.key)));
|
||||
|
||||
for (final element in sortedList) {
|
||||
RetransmitMessage msg =
|
||||
RetransmitMessage.fromJson(jsonDecode(element.value));
|
||||
|
||||
Result resp = await apiService.sendTextMessage(
|
||||
msg.userId,
|
||||
msg.bytes,
|
||||
msg.pushData,
|
||||
);
|
||||
|
||||
if (resp.isSuccess) {
|
||||
if (msg.messageId != null) {
|
||||
await twonlyDB.messagesDao.updateMessageByMessageId(
|
||||
msg.messageId!,
|
||||
MessagesCompanion(
|
||||
acknowledgeByServer: Value(true),
|
||||
),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
failed[element.key] = element.value;
|
||||
}
|
||||
}
|
||||
Box box = await getMediaStorage();
|
||||
box.put("messages-to-retransmit", jsonEncode(failed));
|
||||
});
|
||||
}
|
||||
|
||||
class RetransmitMessage {
|
||||
int? messageId;
|
||||
int userId;
|
||||
Uint8List bytes;
|
||||
List<int>? pushData;
|
||||
RetransmitMessage({
|
||||
this.messageId,
|
||||
required this.userId,
|
||||
required this.bytes,
|
||||
this.pushData,
|
||||
});
|
||||
|
||||
// From JSON constructor
|
||||
factory RetransmitMessage.fromJson(Map<String, dynamic> json) {
|
||||
return RetransmitMessage(
|
||||
messageId: json['messageId'],
|
||||
userId: json['userId'],
|
||||
bytes: base64Decode(json['bytes']),
|
||||
pushData: json['pushData'] == null
|
||||
? null
|
||||
: List<int>.from(json['pushData'].map((item) => item as int)),
|
||||
);
|
||||
}
|
||||
|
||||
// To JSON method
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'messageId': messageId,
|
||||
'userId': userId,
|
||||
'bytes': base64Encode(bytes),
|
||||
'pushData': pushData,
|
||||
};
|
||||
for (final retransId in retransIds) {
|
||||
sendRetransmitMessage(retransId);
|
||||
}
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> getAllMessagesForRetransmitting() async {
|
||||
Box box = await getMediaStorage();
|
||||
String? retransmitJson = box.get("messages-to-retransmit");
|
||||
Map<String, dynamic> retransmit = {};
|
||||
Future sendRetransmitMessage(int retransId) async {
|
||||
MessageRetransmission? retrans = await twonlyDB.messageRetransmissionDao
|
||||
.getRetransmissionById(retransId)
|
||||
.getSingleOrNull();
|
||||
|
||||
if (retransmitJson != null) {
|
||||
try {
|
||||
retransmit = jsonDecode(retransmitJson);
|
||||
} catch (e) {
|
||||
Log.error("Could not decode the retransmit messages: $e");
|
||||
await box.delete("messages-to-retransmit");
|
||||
}
|
||||
if (retrans == null) {
|
||||
Log.error("$retransId not found in database");
|
||||
return;
|
||||
}
|
||||
return retransmit;
|
||||
}
|
||||
|
||||
Future<Result> sendRetransmitMessage(
|
||||
String stateId, RetransmitMessage msg) async {
|
||||
Log.info("Sending ${msg.messageId}");
|
||||
Result resp =
|
||||
await apiService.sendTextMessage(msg.userId, msg.bytes, msg.pushData);
|
||||
Uint8List? encryptedBytes = await signalEncryptMessage(
|
||||
retrans.contactId,
|
||||
retrans.plaintextContent,
|
||||
);
|
||||
|
||||
if (encryptedBytes == null) {
|
||||
Log.error("Could not encrypt the message. Aborting and trying again.");
|
||||
return;
|
||||
}
|
||||
|
||||
Result resp = await apiService.sendTextMessage(
|
||||
retrans.contactId,
|
||||
encryptedBytes,
|
||||
retrans.pushData,
|
||||
);
|
||||
|
||||
bool retry = true;
|
||||
|
||||
if (resp.isError) {
|
||||
if (resp.error == ErrorCode.UserIdNotFound) {
|
||||
retry = false;
|
||||
if (msg.messageId != null) {
|
||||
if (retrans.messageId != null) {
|
||||
await twonlyDB.messagesDao.updateMessageByMessageId(
|
||||
msg.messageId!,
|
||||
retrans.messageId!,
|
||||
MessagesCompanion(errorWhileSending: Value(true)),
|
||||
);
|
||||
}
|
||||
await twonlyDB.contactsDao.updateContact(
|
||||
retrans.contactId,
|
||||
ContactsCompanion(deleted: Value(true)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (resp.isSuccess) {
|
||||
retry = false;
|
||||
if (msg.messageId != null) {
|
||||
if (retrans.messageId != null) {
|
||||
await twonlyDB.messagesDao.updateMessageByMessageId(
|
||||
msg.messageId!,
|
||||
MessagesCompanion(acknowledgeByServer: Value(true)),
|
||||
retrans.messageId!,
|
||||
MessagesCompanion(
|
||||
acknowledgeByServer: Value(true),
|
||||
errorWhileSending: Value(false),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (!retry) {
|
||||
{
|
||||
var retransmit = await getAllMessagesForRetransmitting();
|
||||
retransmit.remove(stateId);
|
||||
Box box = await getMediaStorage();
|
||||
box.put("messages-to-retransmit", jsonEncode(retransmit));
|
||||
}
|
||||
await twonlyDB.messageRetransmissionDao.deleteRetransmissionById(retransId);
|
||||
}
|
||||
return resp;
|
||||
}
|
||||
|
||||
// this functions ensures that the message is received by the server and in case of errors will try again later
|
||||
Future<(String, RetransmitMessage)?> encryptMessage(
|
||||
int? messageId, int userId, MessageJson msg,
|
||||
{PushKind? pushKind}) async {
|
||||
return await lockSendingMessages
|
||||
.protect<(String, RetransmitMessage)?>(() async {
|
||||
Uint8List? bytes = await signalEncryptMessage(userId, msg);
|
||||
|
||||
if (bytes == null) {
|
||||
Log.error("Error encryption message!");
|
||||
return null;
|
||||
}
|
||||
|
||||
var retransmit = await getAllMessagesForRetransmitting();
|
||||
|
||||
int currentMaxStateId = messageId ?? 60000;
|
||||
if (retransmit.isNotEmpty && messageId == null) {
|
||||
currentMaxStateId = retransmit.keys.map((x) => int.parse(x)).reduce(max);
|
||||
if (currentMaxStateId < 60000) {
|
||||
currentMaxStateId = 60000;
|
||||
}
|
||||
}
|
||||
|
||||
String stateId = (currentMaxStateId + 1).toString();
|
||||
|
||||
Box box = await getMediaStorage();
|
||||
|
||||
List<int>? pushData;
|
||||
if (pushKind != null) {
|
||||
pushData = await getPushData(userId, pushKind);
|
||||
}
|
||||
|
||||
RetransmitMessage encryptedMessage = RetransmitMessage(
|
||||
messageId: messageId,
|
||||
userId: userId,
|
||||
bytes: bytes,
|
||||
pushData: pushData,
|
||||
);
|
||||
|
||||
{
|
||||
retransmit[stateId] = jsonEncode(encryptedMessage.toJson());
|
||||
box.put("messages-to-retransmit", jsonEncode(retransmit));
|
||||
}
|
||||
|
||||
return (stateId, encryptedMessage);
|
||||
});
|
||||
}
|
||||
|
||||
// encrypts and stores the message and then sends it in the background
|
||||
|
|
@ -205,16 +90,38 @@ Future encryptAndSendMessageAsync(int? messageId, int userId, MessageJson msg,
|
|||
if (gIsDemoUser) {
|
||||
return;
|
||||
}
|
||||
(String, RetransmitMessage)? stateData =
|
||||
await encryptMessage(messageId, userId, msg, pushKind: pushKind);
|
||||
if (stateData != null) {
|
||||
final (stateId, message) = stateData;
|
||||
sendRetransmitMessage(stateId, message);
|
||||
|
||||
Uint8List? pushData;
|
||||
if (pushKind != null) {
|
||||
pushData = await getPushData(userId, pushKind);
|
||||
}
|
||||
|
||||
Uint8List plaintextContent =
|
||||
Uint8List.fromList(gzip.encode(utf8.encode(jsonEncode(msg.toJson()))));
|
||||
|
||||
int? retransId = await twonlyDB.messageRetransmissionDao.insertRetransmission(
|
||||
MessageRetransmissionsCompanion(
|
||||
contactId: Value(userId),
|
||||
messageId: Value(messageId),
|
||||
plaintextContent: Value(plaintextContent),
|
||||
pushData: Value(pushData),
|
||||
),
|
||||
);
|
||||
|
||||
if (retransId == null) {
|
||||
Log.error("Could not insert the message into the retransmission database");
|
||||
return;
|
||||
}
|
||||
|
||||
// this can now be done in the background...
|
||||
sendRetransmitMessage(retransId);
|
||||
}
|
||||
|
||||
Future sendTextMessage(
|
||||
int target, TextMessageContent content, PushKind? pushKind) async {
|
||||
int target,
|
||||
TextMessageContent content,
|
||||
PushKind? pushKind,
|
||||
) async {
|
||||
DateTime messageSendAt = DateTime.now();
|
||||
|
||||
int? messageId = await twonlyDB.messagesDao.insertMessage(
|
||||
|
|
|
|||
|
|
@ -100,8 +100,9 @@ Future<client.Response> handleNewMessage(int fromUserId, Uint8List body) async {
|
|||
case MessageKind.opened:
|
||||
if (message.messageId != null) {
|
||||
final update = MessagesCompanion(
|
||||
openedAt: Value(message.timestamp),
|
||||
errorWhileSending: Value(false));
|
||||
openedAt: Value(message.timestamp),
|
||||
errorWhileSending: Value(false),
|
||||
);
|
||||
await twonlyDB.messagesDao.updateMessageByOtherUser(
|
||||
fromUserId,
|
||||
message.messageId!,
|
||||
|
|
@ -214,8 +215,11 @@ Future<client.Response> handleNewMessage(int fromUserId, Uint8List body) async {
|
|||
fromUserId,
|
||||
responseToMessageId,
|
||||
MessagesCompanion(
|
||||
errorWhileSending: Value(false),
|
||||
),
|
||||
errorWhileSending: Value(false),
|
||||
openedAt: Value(
|
||||
DateTime.now(),
|
||||
) // when a user reacted to the media file, it should be marked as opened
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -259,16 +263,16 @@ Future<client.Response> handleNewMessage(int fromUserId, Uint8List body) async {
|
|||
}
|
||||
}
|
||||
|
||||
// await encryptAndSendMessageAsync(
|
||||
// message.messageId!,
|
||||
// fromUserId,
|
||||
// MessageJson(
|
||||
// kind: MessageKind.ack,
|
||||
// messageId: message.messageId!,
|
||||
// content: MessageContent(),
|
||||
// timestamp: DateTime.now(),
|
||||
// ),
|
||||
// );
|
||||
await encryptAndSendMessageAsync(
|
||||
message.messageId!,
|
||||
fromUserId,
|
||||
MessageJson(
|
||||
kind: MessageKind.ack,
|
||||
messageId: message.messageId!,
|
||||
content: MessageContent(),
|
||||
timestamp: DateTime.now(),
|
||||
),
|
||||
);
|
||||
|
||||
// unarchive contact when receiving a new message
|
||||
await twonlyDB.contactsDao.updateContact(
|
||||
|
|
|
|||
|
|
@ -238,7 +238,7 @@ class PushNotification {
|
|||
|
||||
/// this will trigger a push notification
|
||||
/// push notification only containing the message kind and username
|
||||
Future<List<int>?> getPushData(int toUserId, PushKind kind) async {
|
||||
Future<Uint8List?> getPushData(int toUserId, PushKind kind) async {
|
||||
final Map<int, PushUser> pushKeys = await getPushKeys("sendingPushKeys");
|
||||
|
||||
List<int> key = "InsecureOnlyUsedForAddingContact".codeUnits;
|
||||
|
|
@ -278,8 +278,7 @@ Future<List<int>?> getPushData(int toUserId, PushKind kind) async {
|
|||
cipherText: secretBox.cipherText,
|
||||
mac: secretBox.mac.bytes,
|
||||
);
|
||||
|
||||
return jsonEncode(res.toJson()).codeUnits;
|
||||
return Utf8Encoder().convert(jsonEncode(res.toJson()));
|
||||
}
|
||||
|
||||
Future<PushKind?> tryDecryptMessage(
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import 'dart:convert';
|
|||
import 'dart:io';
|
||||
import 'dart:typed_data';
|
||||
import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart';
|
||||
import 'package:mutex/mutex.dart';
|
||||
import 'package:twonly/src/database/twonly_database.dart';
|
||||
import 'package:twonly/src/model/json/message.dart';
|
||||
import 'package:twonly/src/database/signal/connect_signal_protocol_store.dart';
|
||||
|
|
@ -11,82 +12,87 @@ import 'package:twonly/src/services/signal/utils.signal.dart';
|
|||
import 'package:twonly/src/utils/log.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
|
||||
Future<Uint8List?> signalEncryptMessage(int target, MessageJson msg) async {
|
||||
try {
|
||||
ConnectSignalProtocolStore signalStore = (await getSignalStore())!;
|
||||
final address = SignalProtocolAddress(target.toString(), defaultDeviceId);
|
||||
/// This caused some troubles, so protection the encryption...
|
||||
final lockingSignalEncryption = Mutex();
|
||||
|
||||
SessionCipher session = SessionCipher.fromStore(signalStore, address);
|
||||
Future<Uint8List?> signalEncryptMessage(
|
||||
int target, Uint8List plaintextContent) async {
|
||||
return await lockingSignalEncryption.protect<Uint8List?>(() async {
|
||||
try {
|
||||
ConnectSignalProtocolStore signalStore = (await getSignalStore())!;
|
||||
final address = SignalProtocolAddress(target.toString(), defaultDeviceId);
|
||||
|
||||
SignalContactPreKey? preKey = await getPreKeyByContactId(target);
|
||||
SignalContactSignedPreKey? signedPreKey = await getSignedPreKeyByContactId(
|
||||
target,
|
||||
);
|
||||
SessionCipher session = SessionCipher.fromStore(signalStore, address);
|
||||
|
||||
if (signedPreKey != null) {
|
||||
SessionBuilder sessionBuilder = SessionBuilder.fromSignalStore(
|
||||
signalStore,
|
||||
address,
|
||||
SignalContactPreKey? preKey = await getPreKeyByContactId(target);
|
||||
SignalContactSignedPreKey? signedPreKey =
|
||||
await getSignedPreKeyByContactId(
|
||||
target,
|
||||
);
|
||||
|
||||
ECPublicKey? tempPrePublicKey;
|
||||
if (signedPreKey != null) {
|
||||
SessionBuilder sessionBuilder = SessionBuilder.fromSignalStore(
|
||||
signalStore,
|
||||
address,
|
||||
);
|
||||
|
||||
if (preKey != null) {
|
||||
tempPrePublicKey = Curve.decodePoint(
|
||||
DjbECPublicKey(
|
||||
Uint8List.fromList(preKey.preKey),
|
||||
).serialize(),
|
||||
ECPublicKey? tempPrePublicKey;
|
||||
|
||||
if (preKey != null) {
|
||||
tempPrePublicKey = Curve.decodePoint(
|
||||
DjbECPublicKey(
|
||||
Uint8List.fromList(preKey.preKey),
|
||||
).serialize(),
|
||||
1,
|
||||
);
|
||||
}
|
||||
|
||||
ECPublicKey? tempSignedPreKeyPublic = Curve.decodePoint(
|
||||
DjbECPublicKey(Uint8List.fromList(signedPreKey.signedPreKey))
|
||||
.serialize(),
|
||||
1,
|
||||
);
|
||||
}
|
||||
|
||||
ECPublicKey? tempSignedPreKeyPublic = Curve.decodePoint(
|
||||
DjbECPublicKey(Uint8List.fromList(signedPreKey.signedPreKey))
|
||||
.serialize(),
|
||||
1,
|
||||
);
|
||||
|
||||
Uint8List? tempSignedPreKeySignature = Uint8List.fromList(
|
||||
signedPreKey.signedPreKeySignature,
|
||||
);
|
||||
|
||||
final IdentityKey? tempIdentityKey =
|
||||
await signalStore.getIdentity(address);
|
||||
if (tempIdentityKey != null) {
|
||||
PreKeyBundle preKeyBundle = PreKeyBundle(
|
||||
target,
|
||||
defaultDeviceId,
|
||||
preKey?.preKeyId,
|
||||
tempPrePublicKey,
|
||||
signedPreKey.signedPreKeyId,
|
||||
tempSignedPreKeyPublic,
|
||||
tempSignedPreKeySignature,
|
||||
tempIdentityKey,
|
||||
Uint8List? tempSignedPreKeySignature = Uint8List.fromList(
|
||||
signedPreKey.signedPreKeySignature,
|
||||
);
|
||||
|
||||
try {
|
||||
await sessionBuilder.processPreKeyBundle(preKeyBundle);
|
||||
} catch (e) {
|
||||
Log.error("could not process pre key bundle: $e");
|
||||
final IdentityKey? tempIdentityKey =
|
||||
await signalStore.getIdentity(address);
|
||||
if (tempIdentityKey != null) {
|
||||
PreKeyBundle preKeyBundle = PreKeyBundle(
|
||||
target,
|
||||
defaultDeviceId,
|
||||
preKey?.preKeyId,
|
||||
tempPrePublicKey,
|
||||
signedPreKey.signedPreKeyId,
|
||||
tempSignedPreKeyPublic,
|
||||
tempSignedPreKeySignature,
|
||||
tempIdentityKey,
|
||||
);
|
||||
|
||||
try {
|
||||
await sessionBuilder.processPreKeyBundle(preKeyBundle);
|
||||
} catch (e) {
|
||||
Log.error("could not process pre key bundle: $e");
|
||||
}
|
||||
} else {
|
||||
Log.error("did not get the identity of the remote address");
|
||||
}
|
||||
} else {
|
||||
Log.error("did not get the identity of the remote address");
|
||||
}
|
||||
|
||||
final ciphertext = await session.encrypt(plaintextContent);
|
||||
|
||||
var b = BytesBuilder();
|
||||
b.add(ciphertext.serialize());
|
||||
b.add(intToBytes(ciphertext.getType()));
|
||||
|
||||
return b.takeBytes();
|
||||
} catch (e) {
|
||||
Log.error(e.toString());
|
||||
return null;
|
||||
}
|
||||
|
||||
final ciphertext = await session.encrypt(
|
||||
Uint8List.fromList(gzip.encode(utf8.encode(jsonEncode(msg.toJson())))),
|
||||
);
|
||||
|
||||
var b = BytesBuilder();
|
||||
b.add(ciphertext.serialize());
|
||||
b.add(intToBytes(ciphertext.getType()));
|
||||
|
||||
return b.takeBytes();
|
||||
} catch (e) {
|
||||
Log.error(e.toString());
|
||||
return null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<MessageJson?> signalDecryptMessage(int source, Uint8List msg) async {
|
||||
|
|
@ -115,7 +121,12 @@ Future<MessageJson?> signalDecryptMessage(int source, Uint8List msg) async {
|
|||
return null;
|
||||
}
|
||||
return MessageJson.fromJson(
|
||||
jsonDecode(utf8.decode(gzip.decode(plaintext))));
|
||||
jsonDecode(
|
||||
utf8.decode(
|
||||
gzip.decode(plaintext),
|
||||
),
|
||||
),
|
||||
);
|
||||
} on InvalidKeyIdException catch (_) {
|
||||
return null; // got the same message again
|
||||
} on DuplicateMessageException catch (_) {
|
||||
|
|
|
|||
|
|
@ -1,41 +0,0 @@
|
|||
import 'dart:convert';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:twonly/src/services/notification.service.dart';
|
||||
import 'package:twonly/src/utils/log.dart';
|
||||
|
||||
Future initMediaStorage() async {
|
||||
final storage = FlutterSecureStorage();
|
||||
var containsEncryptionKey =
|
||||
await storage.containsKey(key: 'hive_encryption_key');
|
||||
if (!containsEncryptionKey) {
|
||||
var key = Hive.generateSecureKey();
|
||||
await storage.write(
|
||||
key: 'hive_encryption_key',
|
||||
value: base64UrlEncode(key),
|
||||
);
|
||||
}
|
||||
final dir = await getApplicationSupportDirectory();
|
||||
Hive.init(dir.path);
|
||||
}
|
||||
|
||||
Future<Box> getMediaStorage() async {
|
||||
try {
|
||||
await initMediaStorage();
|
||||
final storage = FlutterSecureStorage();
|
||||
|
||||
var encryptionKey =
|
||||
base64Url.decode((await storage.read(key: 'hive_encryption_key'))!);
|
||||
|
||||
return await Hive.openBox(
|
||||
'media_storage',
|
||||
encryptionCipher: HiveAesCipher(encryptionKey),
|
||||
);
|
||||
} catch (e) {
|
||||
await customLocalPushNotification("Secure Storage Error",
|
||||
"Settings > Apps > twonly > Storage and Cache > Press clear on both");
|
||||
Log.error(e);
|
||||
throw Exception(e);
|
||||
}
|
||||
}
|
||||
44
lib/src/utils/keyvalue.dart
Normal file
44
lib/src/utils/keyvalue.dart
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:twonly/src/utils/log.dart';
|
||||
|
||||
class KeyValueStore {
|
||||
static Future<String> _getFilePath(String key) async {
|
||||
final directory = await getApplicationSupportDirectory();
|
||||
return '${directory.path}/keyvalue/$key.json';
|
||||
}
|
||||
|
||||
static Future<Map<String, dynamic>?> get(String key) async {
|
||||
try {
|
||||
final filePath = await _getFilePath(key);
|
||||
final file = File(filePath);
|
||||
|
||||
// Check if the file exists
|
||||
if (await file.exists()) {
|
||||
final contents = await file.readAsString();
|
||||
return jsonDecode(contents);
|
||||
} else {
|
||||
return null; // File does not exist
|
||||
}
|
||||
} catch (e) {
|
||||
Log.error('Error reading file: $e');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
static Future<void> put(String key, Map<String, dynamic> value) async {
|
||||
try {
|
||||
final filePath = await _getFilePath(key);
|
||||
final file = File(filePath);
|
||||
|
||||
// Create the directory if it doesn't exist
|
||||
await file.parent.create(recursive: true);
|
||||
|
||||
// Write the JSON data to the file
|
||||
await file.writeAsString(jsonEncode(value));
|
||||
} catch (e) {
|
||||
Log.error('Error writing file: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -146,8 +146,10 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
|
|||
}
|
||||
|
||||
if (openedMessageOtherIds.isNotEmpty) {
|
||||
notifyContactAboutOpeningMessage(
|
||||
widget.contact.userId, openedMessageOtherIds);
|
||||
await notifyContactAboutOpeningMessage(
|
||||
widget.contact.userId,
|
||||
openedMessageOtherIds,
|
||||
);
|
||||
}
|
||||
|
||||
twonlyDB.messagesDao.openedAllNonMediaMessages(widget.contact.userId);
|
||||
|
|
|
|||
|
|
@ -210,7 +210,7 @@ class _MediaViewerViewState extends State<MediaViewerView> {
|
|||
}
|
||||
}
|
||||
|
||||
notifyContactAboutOpeningMessage(
|
||||
await notifyContactAboutOpeningMessage(
|
||||
current.contactId,
|
||||
[current.messageOtherId!],
|
||||
);
|
||||
|
|
|
|||
|
|
@ -779,14 +779,6 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.3"
|
||||
hive:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: hive
|
||||
sha256: "8dcf6db979d7933da8217edcec84e9df1bdb4e4edc7fc77dbd5aa74356d6d941"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.3"
|
||||
html:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
|
|||
|
|
@ -30,7 +30,6 @@ dependencies:
|
|||
font_awesome_flutter: ^10.8.0
|
||||
gal: ^2.3.1
|
||||
hand_signature: ^3.0.3
|
||||
hive: ^2.2.3
|
||||
image: ^4.3.0
|
||||
intl: ^0.20.2
|
||||
introduction_screen: ^3.1.14
|
||||
|
|
|
|||
Loading…
Reference in a new issue