mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-01-15 12:48:41 +00:00
use also for download background manager
This commit is contained in:
parent
88649c0dc0
commit
6946164d1e
8 changed files with 254 additions and 145 deletions
|
|
@ -48,7 +48,7 @@ void main() async {
|
||||||
purgeReceivedMediaFiles();
|
purgeReceivedMediaFiles();
|
||||||
purgeSendMediaFiles();
|
purgeSendMediaFiles();
|
||||||
|
|
||||||
await initMediaUploader();
|
await initFileDownloader();
|
||||||
|
|
||||||
runApp(
|
runApp(
|
||||||
MultiProvider(
|
MultiProvider(
|
||||||
|
|
|
||||||
|
|
@ -97,6 +97,18 @@ class MessagesDao extends DatabaseAccessor<TwonlyDatabase>
|
||||||
.get();
|
.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<List<Message>> getAllNonACKMessagesFromUser() {
|
||||||
|
return (select(messages)
|
||||||
|
..where((t) =>
|
||||||
|
t.acknowledgeByUser.equals(false) &
|
||||||
|
t.messageOtherId.isNull() &
|
||||||
|
t.errorWhileSending.equals(false) &
|
||||||
|
t.sendAt.isBiggerThanValue(
|
||||||
|
DateTime.now().subtract(Duration(minutes: 10)),
|
||||||
|
)))
|
||||||
|
.get();
|
||||||
|
}
|
||||||
|
|
||||||
Stream<List<Message>> getAllStoredMediaFiles() {
|
Stream<List<Message>> getAllStoredMediaFiles() {
|
||||||
return (select(messages)
|
return (select(messages)
|
||||||
..where((t) => t.mediaStored.equals(true))
|
..where((t) => t.mediaStored.equals(true))
|
||||||
|
|
@ -191,10 +203,6 @@ class MessagesDao extends DatabaseAccessor<TwonlyDatabase>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future deleteMessageById(int messageId) {
|
|
||||||
return (delete(messages)..where((t) => t.messageId.equals(messageId))).go();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future deleteMessagesByContactId(int contactId) {
|
Future deleteMessagesByContactId(int contactId) {
|
||||||
return (delete(messages)
|
return (delete(messages)
|
||||||
..where((t) =>
|
..where((t) =>
|
||||||
|
|
@ -207,7 +215,9 @@ class MessagesDao extends DatabaseAccessor<TwonlyDatabase>
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> containsOtherMessageId(
|
Future<bool> containsOtherMessageId(
|
||||||
int fromUserId, int messageOtherId) async {
|
int fromUserId,
|
||||||
|
int messageOtherId,
|
||||||
|
) async {
|
||||||
final query = select(messages)
|
final query = select(messages)
|
||||||
..where((t) =>
|
..where((t) =>
|
||||||
t.messageOtherId.equals(messageOtherId) &
|
t.messageOtherId.equals(messageOtherId) &
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
import 'package:background_downloader/background_downloader.dart';
|
||||||
import 'package:connectivity_plus/connectivity_plus.dart';
|
import 'package:connectivity_plus/connectivity_plus.dart';
|
||||||
import 'package:drift/drift.dart';
|
import 'package:drift/drift.dart';
|
||||||
import 'package:path/path.dart';
|
import 'package:path/path.dart';
|
||||||
|
|
@ -8,8 +9,6 @@ import 'package:twonly/globals.dart';
|
||||||
import 'package:twonly/src/database/twonly_database.dart';
|
import 'package:twonly/src/database/twonly_database.dart';
|
||||||
import 'package:twonly/src/database/tables/messages_table.dart';
|
import 'package:twonly/src/database/tables/messages_table.dart';
|
||||||
import 'package:twonly/src/model/json/message.dart';
|
import 'package:twonly/src/model/json/message.dart';
|
||||||
import 'package:http/http.dart' as http;
|
|
||||||
// import 'package:twonly/src/providers/api/api_utils.dart';
|
|
||||||
import 'package:twonly/src/services/api/media_send.dart';
|
import 'package:twonly/src/services/api/media_send.dart';
|
||||||
import 'package:cryptography_plus/cryptography_plus.dart';
|
import 'package:cryptography_plus/cryptography_plus.dart';
|
||||||
import 'package:twonly/src/services/api/utils.dart';
|
import 'package:twonly/src/services/api/utils.dart';
|
||||||
|
|
@ -75,6 +74,34 @@ Future<bool> isAllowedToDownload(bool isVideo) async {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future handleDownloadStatusUpdate(TaskStatusUpdate update) async {
|
||||||
|
bool failed = false;
|
||||||
|
int messageId = int.parse(update.task.taskId.replaceAll("download_", ""));
|
||||||
|
|
||||||
|
if (update.status == TaskStatus.failed ||
|
||||||
|
update.status == TaskStatus.canceled) {
|
||||||
|
Log.error("Download failed: ${update.status}");
|
||||||
|
failed = true;
|
||||||
|
} else if (update.status == TaskStatus.complete) {
|
||||||
|
if (update.responseStatusCode == 200) {
|
||||||
|
Log.info("Download was successfully for $messageId");
|
||||||
|
await handleEncryptedFile(messageId);
|
||||||
|
} else {
|
||||||
|
Log.error(
|
||||||
|
"Got invalid response status code: ${update.responseStatusCode}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (failed) {
|
||||||
|
Message? message = await twonlyDB.messagesDao
|
||||||
|
.getMessageByMessageId(messageId)
|
||||||
|
.getSingleOrNull();
|
||||||
|
if (message != null) {
|
||||||
|
await handleMediaError(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future startDownloadMedia(Message message, bool force,
|
Future startDownloadMedia(Message message, bool force,
|
||||||
{int retryCounter = 0}) async {
|
{int retryCounter = 0}) async {
|
||||||
if (message.contentJson == null) return;
|
if (message.contentJson == null) return;
|
||||||
|
|
@ -90,6 +117,7 @@ Future startDownloadMedia(Message message, bool force,
|
||||||
|
|
||||||
final content =
|
final content =
|
||||||
MessageContent.fromJson(message.kind, jsonDecode(message.contentJson!));
|
MessageContent.fromJson(message.kind, jsonDecode(message.contentJson!));
|
||||||
|
|
||||||
if (content is! MediaMessageContent) return;
|
if (content is! MediaMessageContent) return;
|
||||||
if (content.downloadToken == null) return;
|
if (content.downloadToken == null) return;
|
||||||
|
|
||||||
|
|
@ -135,60 +163,42 @@ Future startDownloadMedia(Message message, bool force,
|
||||||
String apiUrl =
|
String apiUrl =
|
||||||
"http${apiService.apiSecure}://${apiService.apiHost}/api/download/$downloadToken";
|
"http${apiService.apiSecure}://${apiService.apiHost}/api/download/$downloadToken";
|
||||||
|
|
||||||
var httpClient = http.Client();
|
try {
|
||||||
var request = http.Request('GET', Uri.parse(apiUrl));
|
final task = DownloadTask(
|
||||||
var response = httpClient.send(request);
|
url: apiUrl,
|
||||||
|
taskId: "download_${media.messageId}",
|
||||||
|
directory: "media/received/",
|
||||||
|
baseDirectory: BaseDirectory.applicationSupport,
|
||||||
|
filename: "${media.messageId}.encrypted",
|
||||||
|
priority: 0,
|
||||||
|
retries: 10,
|
||||||
|
);
|
||||||
|
|
||||||
List<List<int>> chunks = [];
|
Log.info(
|
||||||
int downloaded = 0;
|
"Got media file. Starting download: ${downloadToken.substring(0, 10)}");
|
||||||
|
|
||||||
response.asStream().listen((http.StreamedResponse r) {
|
final result = await FileDownloader().enqueue(task);
|
||||||
r.stream.listen((List<int> chunk) {
|
|
||||||
// Display percentage of completion
|
|
||||||
Log.info(
|
|
||||||
'downloadPercentage: ${downloaded / (r.contentLength ?? 0) * 100}');
|
|
||||||
|
|
||||||
chunks.add(chunk);
|
return result;
|
||||||
downloaded += chunk.length;
|
} catch (e) {
|
||||||
}, onDone: () async {
|
Log.error("Exception during upload: $e");
|
||||||
if (r.statusCode != 200) {
|
}
|
||||||
Log.error("Download error: $r");
|
|
||||||
if (r.statusCode == 418) {
|
|
||||||
Log.error("Got custom error code: ${chunks.toList()}");
|
|
||||||
handleMediaError(message);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Display percentage of completion
|
|
||||||
Log.info(
|
|
||||||
'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, retryCounter: retryCounter);
|
|
||||||
return;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future handleEncryptedFile(
|
Future handleEncryptedFile(int messageId) async {
|
||||||
Message msg, {
|
Message? msg = await twonlyDB.messagesDao
|
||||||
Uint8List? encryptedBytesTmp,
|
.getMessageByMessageId(messageId)
|
||||||
int retryCounter = 0,
|
.getSingleOrNull();
|
||||||
}) async {
|
if (msg == null) {
|
||||||
Uint8List? encryptedBytes =
|
Log.error("Not message for downloaded file found: $messageId");
|
||||||
encryptedBytesTmp ?? await readMediaFile(msg.messageId, "encrypted");
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Uint8List? encryptedBytes = await readMediaFile(msg.messageId, "encrypted");
|
||||||
|
|
||||||
if (encryptedBytes == null) {
|
if (encryptedBytes == null) {
|
||||||
Log.error("encrypted bytes are not found for ${msg.messageId}");
|
Log.error("encrypted bytes are not found for ${msg.messageId}");
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
MediaMessageContent content =
|
MediaMessageContent content =
|
||||||
|
|
@ -198,7 +208,7 @@ Future handleEncryptedFile(
|
||||||
SecretKeyData secretKeyData = SecretKeyData(content.encryptionKey!);
|
SecretKeyData secretKeyData = SecretKeyData(content.encryptionKey!);
|
||||||
|
|
||||||
SecretBox secretBox = SecretBox(
|
SecretBox secretBox = SecretBox(
|
||||||
encryptedBytes!,
|
encryptedBytes,
|
||||||
nonce: content.encryptionNonce!,
|
nonce: content.encryptionNonce!,
|
||||||
mac: Mac(content.encryptionMac!),
|
mac: Mac(content.encryptionMac!),
|
||||||
);
|
);
|
||||||
|
|
@ -216,15 +226,9 @@ Future handleEncryptedFile(
|
||||||
|
|
||||||
await writeMediaFile(msg.messageId, "png", imageBytes);
|
await writeMediaFile(msg.messageId, "png", imageBytes);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (retryCounter >= 1) {
|
Log.error(
|
||||||
Log.error(
|
"could not decrypt the media file in the second try. reporting error to user: $e");
|
||||||
"could not decrypt the media file in the second try. reporting error to user: $e");
|
handleMediaError(msg);
|
||||||
handleMediaError(msg);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
Log.error("could not decrypt the media file trying again: $e");
|
|
||||||
startDownloadMedia(msg, true, retryCounter: retryCounter + 1);
|
|
||||||
// try downloading again....
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -252,6 +256,7 @@ Future<File?> getVideoPath(int mediaId) async {
|
||||||
Future<Uint8List?> readMediaFile(int mediaId, String type) async {
|
Future<Uint8List?> readMediaFile(int mediaId, String type) async {
|
||||||
String basePath = await getMediaFilePath(mediaId, "received");
|
String basePath = await getMediaFilePath(mediaId, "received");
|
||||||
File file = File("$basePath.$type");
|
File file = File("$basePath.$type");
|
||||||
|
Log.info("Reading: ${file}");
|
||||||
if (!await file.exists()) {
|
if (!await file.exists()) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
@ -365,3 +370,6 @@ Future<void> purgeMediaFiles(Directory directory) async {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// /data/user/0/eu.twonly.testing/files/media/received/27.encrypted
|
||||||
|
// /data/user/0/eu.twonly.testing/app_flutter/data/user/0/eu.twonly.testing/files/media/received/27.encrypted
|
||||||
|
|
|
||||||
|
|
@ -52,73 +52,18 @@ Future<ErrorCode?> isAllowedToSend() async {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future initMediaUploader() async {
|
Future initFileDownloader() async {
|
||||||
FileDownloader().updates.listen((update) async {
|
FileDownloader().updates.listen((update) async {
|
||||||
switch (update) {
|
switch (update) {
|
||||||
case TaskStatusUpdate():
|
case TaskStatusUpdate():
|
||||||
bool failed = false;
|
if (update.task.taskId.contains("upload_")) {
|
||||||
int mediaUploadId = int.parse(update.task.taskId);
|
await handleUploadStatusUpdate(update);
|
||||||
MediaUpload? media = await twonlyDB.mediaUploadsDao
|
|
||||||
.getMediaUploadById(mediaUploadId)
|
|
||||||
.getSingleOrNull();
|
|
||||||
if (media == null) {
|
|
||||||
Log.error(
|
|
||||||
"Got an upload task but no upload media in the media upload database",
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
if (update.task.taskId.contains("download_")) {
|
||||||
if (update.status == TaskStatus.failed ||
|
await handleDownloadStatusUpdate(update);
|
||||||
update.status == TaskStatus.canceled) {
|
|
||||||
Log.error("Upload failed: ${update.status}");
|
|
||||||
failed = true;
|
|
||||||
} else if (update.status == TaskStatus.complete) {
|
|
||||||
if (update.responseStatusCode == 200) {
|
|
||||||
Log.info("Upload of $mediaUploadId success!");
|
|
||||||
|
|
||||||
await twonlyDB.mediaUploadsDao.updateMediaUpload(
|
|
||||||
mediaUploadId,
|
|
||||||
MediaUploadsCompanion(
|
|
||||||
state: Value(UploadState.receiverNotified),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
for (final messageId in media.messageIds!) {
|
|
||||||
await twonlyDB.messagesDao.updateMessageByMessageId(
|
|
||||||
messageId,
|
|
||||||
MessagesCompanion(
|
|
||||||
acknowledgeByServer: Value(true),
|
|
||||||
errorWhileSending: Value(false),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
} else if (update.responseStatusCode != null) {
|
|
||||||
if (update.responseStatusCode! >= 400 &&
|
|
||||||
update.responseStatusCode! < 500) {
|
|
||||||
failed = true;
|
|
||||||
}
|
|
||||||
Log.error(
|
|
||||||
"Got error while uploading: ${update.responseStatusCode}",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (failed) {
|
|
||||||
for (final messageId in media.messageIds!) {
|
|
||||||
await twonlyDB.messagesDao.updateMessageByMessageId(
|
|
||||||
messageId,
|
|
||||||
MessagesCompanion(
|
|
||||||
acknowledgeByServer: Value(true),
|
|
||||||
errorWhileSending: Value(true),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
print('Status update for ${update.task} with status ${update.status}');
|
|
||||||
case TaskProgressUpdate():
|
case TaskProgressUpdate():
|
||||||
print(
|
Log.info(
|
||||||
'Progress update for ${update.task} with progress ${update.progress}');
|
'Progress update for ${update.task} with progress ${update.progress}');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -132,8 +77,8 @@ Future initMediaUploader() async {
|
||||||
if (kDebugMode) {
|
if (kDebugMode) {
|
||||||
FileDownloader().configureNotification(
|
FileDownloader().configureNotification(
|
||||||
running: TaskNotification(
|
running: TaskNotification(
|
||||||
'Uploading',
|
'Uploading/Downloading',
|
||||||
'Uploading your {filename} ({progress}).',
|
'{filename} ({progress}).',
|
||||||
),
|
),
|
||||||
complete: null,
|
complete: null,
|
||||||
progressBar: true,
|
progressBar: true,
|
||||||
|
|
@ -325,8 +270,7 @@ Future encryptMediaFiles(
|
||||||
|
|
||||||
state.encryptionMac = secretBox.mac.bytes;
|
state.encryptionMac = secretBox.mac.bytes;
|
||||||
|
|
||||||
final algorithm = Sha256();
|
state.sha2Hash = (await Sha256().hash(secretBox.cipherText)).bytes;
|
||||||
state.sha2Hash = (await algorithm.hash(secretBox.cipherText)).bytes;
|
|
||||||
|
|
||||||
final encryptedBytes = Uint8List.fromList(secretBox.cipherText);
|
final encryptedBytes = Uint8List.fromList(secretBox.cipherText);
|
||||||
await writeSendMediaFile(
|
await writeSendMediaFile(
|
||||||
|
|
@ -444,6 +388,72 @@ Future handleNextMediaUploadSteps(int mediaUploadId) async {
|
||||||
///
|
///
|
||||||
///
|
///
|
||||||
///
|
///
|
||||||
|
|
||||||
|
Future handleUploadStatusUpdate(TaskStatusUpdate update) async {
|
||||||
|
bool failed = false;
|
||||||
|
int mediaUploadId = int.parse(update.task.taskId.replaceAll("upload_", ""));
|
||||||
|
|
||||||
|
MediaUpload? media = await twonlyDB.mediaUploadsDao
|
||||||
|
.getMediaUploadById(mediaUploadId)
|
||||||
|
.getSingleOrNull();
|
||||||
|
if (media == null) {
|
||||||
|
Log.error(
|
||||||
|
"Got an upload task but no upload media in the media upload database",
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (update.status == TaskStatus.failed ||
|
||||||
|
update.status == TaskStatus.canceled) {
|
||||||
|
Log.error("Upload failed: ${update.status}");
|
||||||
|
failed = true;
|
||||||
|
} else if (update.status == TaskStatus.complete) {
|
||||||
|
if (update.responseStatusCode == 200) {
|
||||||
|
Log.info("Upload of $mediaUploadId success!");
|
||||||
|
|
||||||
|
await twonlyDB.mediaUploadsDao.updateMediaUpload(
|
||||||
|
mediaUploadId,
|
||||||
|
MediaUploadsCompanion(
|
||||||
|
state: Value(UploadState.receiverNotified),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
for (final messageId in media.messageIds!) {
|
||||||
|
await twonlyDB.messagesDao.updateMessageByMessageId(
|
||||||
|
messageId,
|
||||||
|
MessagesCompanion(
|
||||||
|
acknowledgeByServer: Value(true),
|
||||||
|
errorWhileSending: Value(false),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
} else if (update.responseStatusCode != null) {
|
||||||
|
if (update.responseStatusCode! >= 400 &&
|
||||||
|
update.responseStatusCode! < 500) {
|
||||||
|
failed = true;
|
||||||
|
}
|
||||||
|
Log.error(
|
||||||
|
"Got error while uploading: ${update.responseStatusCode}",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (failed) {
|
||||||
|
for (final messageId in media.messageIds!) {
|
||||||
|
await twonlyDB.messagesDao.updateMessageByMessageId(
|
||||||
|
messageId,
|
||||||
|
MessagesCompanion(
|
||||||
|
acknowledgeByServer: Value(true),
|
||||||
|
errorWhileSending: Value(true),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Log.info(
|
||||||
|
'Status update for ${update.task.taskId} with status ${update.status}');
|
||||||
|
}
|
||||||
|
|
||||||
Future handleUploadError(MediaUpload mediaUpload) async {
|
Future handleUploadError(MediaUpload mediaUpload) async {
|
||||||
// if the messageIds are already there notify the user about this error...
|
// if the messageIds are already there notify the user about this error...
|
||||||
if (mediaUpload.messageIds != null) {
|
if (mediaUpload.messageIds != null) {
|
||||||
|
|
@ -496,6 +506,20 @@ Future<bool> handleMediaUpload(MediaUpload media) async {
|
||||||
|
|
||||||
if (message == null) continue;
|
if (message == null) continue;
|
||||||
|
|
||||||
|
Contact? contact = await twonlyDB.contactsDao
|
||||||
|
.getContactByUserId(message.contactId)
|
||||||
|
.getSingleOrNull();
|
||||||
|
|
||||||
|
if (contact == null || contact.deleted) {
|
||||||
|
Log.warn(
|
||||||
|
"Contact deleted ${message.contactId} or not found in database.");
|
||||||
|
await twonlyDB.messagesDao.updateMessageByMessageId(
|
||||||
|
message.messageId,
|
||||||
|
MessagesCompanion(errorWhileSending: Value(true)),
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
await twonlyDB.contactsDao.incFlameCounter(
|
await twonlyDB.contactsDao.incFlameCounter(
|
||||||
message.contactId,
|
message.contactId,
|
||||||
false,
|
false,
|
||||||
|
|
@ -552,7 +576,7 @@ Future<bool> handleMediaUpload(MediaUpload media) async {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final task = UploadTask.fromFile(
|
final task = UploadTask.fromFile(
|
||||||
taskId: "${media.mediaUploadId}",
|
taskId: "upload_${media.mediaUploadId}",
|
||||||
displayName: (media.metadata?.isVideo ?? false) ? "image" : "video",
|
displayName: (media.metadata?.isVideo ?? false) ? "image" : "video",
|
||||||
file: uploadRequestFile,
|
file: uploadRequestFile,
|
||||||
url: apiUrl,
|
url: apiUrl,
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import 'dart:async';
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'package:drift/drift.dart';
|
import 'package:drift/drift.dart';
|
||||||
|
|
@ -13,6 +14,43 @@ import 'package:twonly/src/services/notification.service.dart';
|
||||||
import 'package:twonly/src/utils/log.dart';
|
import 'package:twonly/src/utils/log.dart';
|
||||||
import 'package:twonly/src/utils/storage.dart';
|
import 'package:twonly/src/utils/storage.dart';
|
||||||
|
|
||||||
|
class DirtyResendingItem {
|
||||||
|
DirtyResendingItem({required this.gotLastAck});
|
||||||
|
DateTime gotLastAck;
|
||||||
|
Timer? timer;
|
||||||
|
}
|
||||||
|
|
||||||
|
class DirtyResending {
|
||||||
|
static final Map<int, DirtyResendingItem> _gotLastAck = {};
|
||||||
|
|
||||||
|
static Future gotAckFromUser(int contactID) async {
|
||||||
|
_gotLastAck[contactID]?.timer?.cancel();
|
||||||
|
|
||||||
|
_gotLastAck[contactID] = DirtyResendingItem(gotLastAck: DateTime.now());
|
||||||
|
_gotLastAck[contactID]?.timer = Timer(Duration(seconds: 10), () async {
|
||||||
|
_gotLastAck.remove(contactID);
|
||||||
|
_handleNonACKMessagesForUser(contactID);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future _handleNonACKMessagesForUser(int contactID) async {
|
||||||
|
final List<Message> toResendMessages =
|
||||||
|
await twonlyDB.messagesDao.getAllNonACKMessagesFromUser();
|
||||||
|
|
||||||
|
for (final Message message in toResendMessages) {
|
||||||
|
Log.info("Got newer ACKs from user ${message.messageId}");
|
||||||
|
await twonlyDB.messagesDao.updateMessageByMessageId(
|
||||||
|
message.messageId,
|
||||||
|
MessagesCompanion(
|
||||||
|
errorWhileSending: Value(true),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future handleOlderNonAckMessages() async {}
|
||||||
|
|
||||||
Future tryTransmitMessages() async {
|
Future tryTransmitMessages() async {
|
||||||
final retransIds =
|
final retransIds =
|
||||||
await twonlyDB.messageRetransmissionDao.getRetransmitAbleMessages();
|
await twonlyDB.messageRetransmissionDao.getRetransmitAbleMessages();
|
||||||
|
|
@ -36,6 +74,20 @@ Future sendRetransmitMessage(int retransId) async {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Contact? contact = await twonlyDB.contactsDao
|
||||||
|
.getContactByUserId(retrans.contactId)
|
||||||
|
.getSingleOrNull();
|
||||||
|
if (contact == null || contact.deleted) {
|
||||||
|
Log.warn("Contact deleted $retransId or not found in database.");
|
||||||
|
if (retrans.messageId != null) {
|
||||||
|
await twonlyDB.messagesDao.updateMessageByMessageId(
|
||||||
|
retrans.messageId!,
|
||||||
|
MessagesCompanion(errorWhileSending: Value(true)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
Uint8List? encryptedBytes = await signalEncryptMessage(
|
Uint8List? encryptedBytes = await signalEncryptMessage(
|
||||||
retrans.contactId,
|
retrans.contactId,
|
||||||
retrans.plaintextContent,
|
retrans.plaintextContent,
|
||||||
|
|
@ -155,7 +207,9 @@ Future sendTextMessage(
|
||||||
}
|
}
|
||||||
|
|
||||||
Future notifyContactAboutOpeningMessage(
|
Future notifyContactAboutOpeningMessage(
|
||||||
int fromUserId, List<int> messageOtherIds) async {
|
int fromUserId,
|
||||||
|
List<int> messageOtherIds,
|
||||||
|
) async {
|
||||||
for (final messageOtherId in messageOtherIds) {
|
for (final messageOtherId in messageOtherIds) {
|
||||||
await encryptAndSendMessageAsync(
|
await encryptAndSendMessageAsync(
|
||||||
null,
|
null,
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
import 'package:cryptography_plus/cryptography_plus.dart';
|
||||||
import 'package:drift/drift.dart';
|
import 'package:drift/drift.dart';
|
||||||
import 'package:fixnum/fixnum.dart';
|
import 'package:fixnum/fixnum.dart';
|
||||||
import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart';
|
import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart';
|
||||||
|
|
@ -13,6 +14,7 @@ import 'package:twonly/src/model/protobuf/api/websocket/client_to_server.pbserve
|
||||||
import 'package:twonly/src/model/protobuf/api/websocket/error.pb.dart';
|
import 'package:twonly/src/model/protobuf/api/websocket/error.pb.dart';
|
||||||
import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pb.dart'
|
import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pb.dart'
|
||||||
as server;
|
as server;
|
||||||
|
import 'package:twonly/src/services/api/media_send.dart';
|
||||||
import 'package:twonly/src/services/api/messages.dart';
|
import 'package:twonly/src/services/api/messages.dart';
|
||||||
import 'package:twonly/src/services/api/utils.dart';
|
import 'package:twonly/src/services/api/utils.dart';
|
||||||
import 'package:twonly/src/services/api/media_received.dart';
|
import 'package:twonly/src/services/api/media_received.dart';
|
||||||
|
|
@ -34,6 +36,9 @@ Future handleServerMessage(server.ServerToClient msg) async {
|
||||||
} else if (msg.v0.hasNewMessage()) {
|
} else if (msg.v0.hasNewMessage()) {
|
||||||
Uint8List body = Uint8List.fromList(msg.v0.newMessage.body);
|
Uint8List body = Uint8List.fromList(msg.v0.newMessage.body);
|
||||||
int fromUserId = msg.v0.newMessage.fromUserId.toInt();
|
int fromUserId = msg.v0.newMessage.fromUserId.toInt();
|
||||||
|
var hash = uint8ListToHex(Uint8List.fromList(
|
||||||
|
(await Sha256().hash(msg.v0.newMessage.body)).bytes));
|
||||||
|
Log.info("Got new message from server: ${hash.substring(0, 10)}");
|
||||||
response = await handleNewMessage(fromUserId, body);
|
response = await handleNewMessage(fromUserId, body);
|
||||||
} else {
|
} else {
|
||||||
Log.error("Got a new message from the server: $msg");
|
Log.error("Got a new message from the server: $msg");
|
||||||
|
|
@ -59,6 +64,8 @@ Future<client.Response> handleNewMessage(int fromUserId, Uint8List body) async {
|
||||||
return client.Response()..ok = ok;
|
return client.Response()..ok = ok;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Log.info("Got: ${message.kind}");
|
||||||
|
|
||||||
switch (message.kind) {
|
switch (message.kind) {
|
||||||
case MessageKind.contactRequest:
|
case MessageKind.contactRequest:
|
||||||
return handleContactRequest(fromUserId, message);
|
return handleContactRequest(fromUserId, message);
|
||||||
|
|
@ -151,6 +158,10 @@ Future<client.Response> handleNewMessage(int fromUserId, Uint8List body) async {
|
||||||
message.messageId!,
|
message.messageId!,
|
||||||
update,
|
update,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// search for older messages, that where not yet ack by the other party
|
||||||
|
DirtyResending.gotAckFromUser(fromUserId);
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case MessageKind.pushKey:
|
case MessageKind.pushKey:
|
||||||
|
|
@ -186,6 +197,8 @@ Future<client.Response> handleNewMessage(int fromUserId, Uint8List body) async {
|
||||||
// when a message is received doubled ignore it...
|
// when a message is received doubled ignore it...
|
||||||
if ((await twonlyDB.messagesDao
|
if ((await twonlyDB.messagesDao
|
||||||
.containsOtherMessageId(fromUserId, message.messageId!))) {
|
.containsOtherMessageId(fromUserId, message.messageId!))) {
|
||||||
|
Log.error(
|
||||||
|
"Got a duplicated message from other user: ${message.messageId!}");
|
||||||
var ok = client.Response_Ok()..none = true;
|
var ok = client.Response_Ok()..none = true;
|
||||||
return client.Response()..ok = ok;
|
return client.Response()..ok = ok;
|
||||||
}
|
}
|
||||||
|
|
@ -215,11 +228,11 @@ Future<client.Response> handleNewMessage(int fromUserId, Uint8List body) async {
|
||||||
fromUserId,
|
fromUserId,
|
||||||
responseToMessageId,
|
responseToMessageId,
|
||||||
MessagesCompanion(
|
MessagesCompanion(
|
||||||
errorWhileSending: Value(false),
|
errorWhileSending: Value(false),
|
||||||
openedAt: Value(
|
openedAt: Value(
|
||||||
DateTime.now(),
|
DateTime.now(),
|
||||||
) // when a user reacted to the media file, it should be marked as opened
|
), // when a user reacted to the media file, it should be marked as opened
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -247,6 +260,7 @@ Future<client.Response> handleNewMessage(int fromUserId, Uint8List body) async {
|
||||||
if (messageId == null) {
|
if (messageId == null) {
|
||||||
return client.Response()..error = ErrorCode.InternalError;
|
return client.Response()..error = ErrorCode.InternalError;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (message.kind == MessageKind.media) {
|
if (message.kind == MessageKind.media) {
|
||||||
twonlyDB.contactsDao.incFlameCounter(
|
twonlyDB.contactsDao.incFlameCounter(
|
||||||
fromUserId,
|
fromUserId,
|
||||||
|
|
|
||||||
|
|
@ -26,9 +26,7 @@ Future<Uint8List?> signalEncryptMessage(
|
||||||
|
|
||||||
SignalContactPreKey? preKey = await getPreKeyByContactId(target);
|
SignalContactPreKey? preKey = await getPreKeyByContactId(target);
|
||||||
SignalContactSignedPreKey? signedPreKey =
|
SignalContactSignedPreKey? signedPreKey =
|
||||||
await getSignedPreKeyByContactId(
|
await getSignedPreKeyByContactId(target);
|
||||||
target,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (signedPreKey != null) {
|
if (signedPreKey != null) {
|
||||||
SessionBuilder sessionBuilder = SessionBuilder.fromSignalStore(
|
SessionBuilder sessionBuilder = SessionBuilder.fromSignalStore(
|
||||||
|
|
@ -127,10 +125,6 @@ Future<MessageJson?> signalDecryptMessage(int source, Uint8List msg) async {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
} on InvalidKeyIdException catch (_) {
|
|
||||||
return null; // got the same message again
|
|
||||||
} on DuplicateMessageException catch (_) {
|
|
||||||
return null; // to the same message again
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Log.error(e.toString());
|
Log.error(e.toString());
|
||||||
return null;
|
return null;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
|
import 'package:mutex/mutex.dart';
|
||||||
import 'package:path_provider/path_provider.dart';
|
import 'package:path_provider/path_provider.dart';
|
||||||
|
|
||||||
void initLogger() {
|
void initLogger() {
|
||||||
|
|
@ -28,6 +29,8 @@ class Log {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Mutex writeToLogGuard = Mutex();
|
||||||
|
|
||||||
Future<void> _writeLogToFile(LogRecord record) async {
|
Future<void> _writeLogToFile(LogRecord record) async {
|
||||||
final directory = await getApplicationSupportDirectory();
|
final directory = await getApplicationSupportDirectory();
|
||||||
final logFile = File('${directory.path}/app.log');
|
final logFile = File('${directory.path}/app.log');
|
||||||
|
|
@ -36,8 +39,10 @@ Future<void> _writeLogToFile(LogRecord record) async {
|
||||||
final logMessage =
|
final logMessage =
|
||||||
'${DateTime.now().toString().split(".")[0]} ${record.level.name} [twonly] ${record.loggerName} > ${record.message}\n';
|
'${DateTime.now().toString().split(".")[0]} ${record.level.name} [twonly] ${record.loggerName} > ${record.message}\n';
|
||||||
|
|
||||||
// Append the log message to the file
|
writeToLogGuard.protect(() async {
|
||||||
await logFile.writeAsString(logMessage, mode: FileMode.append);
|
// Append the log message to the file
|
||||||
|
await logFile.writeAsString(logMessage, mode: FileMode.append);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
String _getCallerSourceCodeFilename() {
|
String _getCallerSourceCodeFilename() {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue