fix: self-dos because of Skill Issues

This commit is contained in:
otsmr 2025-06-20 22:46:27 +02:00
parent 385cf11654
commit 1f51d3174f
7 changed files with 330 additions and 305 deletions

View file

@ -12,6 +12,7 @@ enum UploadState {
pending,
readyToUpload,
uploadTaskStarted,
receiverNotified,
// after all users notified all media files that are not storable by the other person will be deleted
}

View file

@ -357,13 +357,14 @@ Future finalizeUpload(int mediaUploadId, List<int> contactIds,
final lockingHandleNextMediaUploadStep = Mutex();
Future handleNextMediaUploadSteps(int mediaUploadId) async {
bool rerun = await lockingHandleNextMediaUploadStep.protect<bool>(() async {
await lockingHandleNextMediaUploadStep.protect(() async {
var mediaUpload = await twonlyDB.mediaUploadsDao
.getMediaUploadById(mediaUploadId)
.getSingleOrNull();
if (mediaUpload == null) return false;
if (mediaUpload.state == UploadState.receiverNotified) {
if (mediaUpload.state == UploadState.receiverNotified ||
mediaUpload.state == UploadState.uploadTaskStarted) {
/// Upload done and all users are notified :)
Log.info("$mediaUploadId is already done");
return false;
@ -380,16 +381,13 @@ Future handleNextMediaUploadSteps(int mediaUploadId) async {
return false;
}
return await handleMediaUpload(mediaUpload);
await handleMediaUpload(mediaUpload);
} catch (e) {
Log.error("Non recoverable error while sending media file: $e");
await handleUploadError(mediaUpload);
}
return false;
});
if (rerun) {
handleNextMediaUploadSteps(mediaUploadId);
}
}
///
@ -402,6 +400,8 @@ Future handleUploadStatusUpdate(TaskStatusUpdate update) async {
bool failed = false;
int mediaUploadId = int.parse(update.task.taskId.replaceAll("upload_", ""));
Log.info("Upload $mediaUploadId done.");
MediaUpload? media = await twonlyDB.mediaUploadsDao
.getMediaUploadById(mediaUploadId)
.getSingleOrNull();
@ -478,11 +478,11 @@ Future handleUploadError(MediaUpload mediaUpload) async {
await twonlyDB.mediaUploadsDao.deleteMediaUpload(mediaUpload.mediaUploadId);
}
Future<bool> handleMediaUpload(MediaUpload media) async {
Future handleMediaUpload(MediaUpload media) async {
Uint8List bytesToUpload =
await readSendMediaFile(media.mediaUploadId, "encrypted");
if (media.messageIds == null) return false;
if (media.messageIds == null) return;
List<Uint8List> downloadTokens =
createDownloadTokens(media.messageIds!.length);
@ -571,7 +571,7 @@ Future<bool> handleMediaUpload(MediaUpload media) async {
await FlutterSecureStorage().read(key: SecureStorageKeys.apiAuthToken);
if (apiAuthToken == null) {
Log.error("api auth token not defined.");
return false;
return;
}
File uploadRequestFile = await writeSendMediaFile(
@ -600,11 +600,17 @@ Future<bool> handleMediaUpload(MediaUpload media) async {
final result = await FileDownloader().enqueue(task);
return result;
if (result) {
await twonlyDB.mediaUploadsDao.updateMediaUpload(
media.mediaUploadId,
MediaUploadsCompanion(
state: Value(UploadState.uploadTaskStarted),
),
);
}
} catch (e) {
Log.error("Exception during upload: $e");
}
return false;
}
Future<bool> compressVideoIfExists(int mediaUploadId) async {

View file

@ -206,8 +206,9 @@ Future<client.Response> handleNewMessage(int fromUserId, Uint8List body) async {
avatarSvg: Value(content.avatarSvg),
displayName: Value(content.displayName),
);
twonlyDB.contactsDao.updateContact(fromUserId, update);
await twonlyDB.contactsDao.updateContact(fromUserId, update);
}
createPushAvatars();
break;
case MessageKind.requestPushKey:

View file

@ -3,8 +3,7 @@ import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/constants/secure_storage_keys.dart';
import 'package:twonly/src/database/twonly_database.dart';
import 'package:twonly/src/services/notification.service.dart';
import 'package:twonly/src/services/notification.background.service.dart';
import 'package:twonly/src/utils/log.dart';
import 'dart:io' show Platform;
import '../../firebase_options.dart';
@ -79,9 +78,7 @@ Future initFCMService() async {
Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
initLogger();
Log.info('Handling a background message: ${message.messageId}');
twonlyDB = TwonlyDatabase();
await handleRemoteMessage(message);
// make sure every thing run...
await Future.delayed(Duration(milliseconds: 2000));
}

View file

@ -0,0 +1,282 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'dart:math';
import 'package:cryptography_plus/cryptography_plus.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:path_provider/path_provider.dart';
import 'package:twonly/src/services/notification.service.dart';
import 'package:twonly/src/utils/log.dart';
final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin =
FlutterLocalNotificationsPlugin();
Future customLocalPushNotification(String title, String msg) async {
const AndroidNotificationDetails androidNotificationDetails =
AndroidNotificationDetails(
'1',
'System',
channelDescription: 'System messages.',
importance: Importance.max,
priority: Priority.max,
);
const DarwinNotificationDetails darwinNotificationDetails =
DarwinNotificationDetails();
const NotificationDetails notificationDetails = NotificationDetails(
android: androidNotificationDetails, iOS: darwinNotificationDetails);
await flutterLocalNotificationsPlugin.show(
999999 + Random.secure().nextInt(9999),
title,
msg,
notificationDetails,
);
}
Future handlePushData(String pushDataJson) async {
try {
String jsonString = utf8.decode(base64.decode(pushDataJson));
final pushData = PushNotification.fromJson(jsonDecode(jsonString));
PushKind? pushKind;
PushUser? pushUser;
int? fromUserId;
if (pushData.keyId == 0) {
List<int> key = "InsecureOnlyUsedForAddingContact".codeUnits;
pushKind = await tryDecryptMessage(key, pushData);
} else {
var pushKeys = await getPushKeys("receivingPushKeys");
for (final userId in pushKeys.keys) {
for (final key in pushKeys[userId]!.keys) {
if (key.id == pushData.keyId) {
pushKind = await tryDecryptMessage(key.key, pushData);
if (pushKind != null) {
pushUser = pushKeys[userId]!;
fromUserId = userId;
break;
}
}
}
// found correct key and user
if (pushUser != null) break;
}
}
if (pushKind != null) {
if (pushKind == PushKind.testNotification) {
await customLocalPushNotification(
"Test notification", "This is a test notification.");
} else if (pushUser != null && fromUserId != null) {
await showLocalPushNotification(pushUser, fromUserId, pushKind);
} else {
await showLocalPushNotificationWithoutUserId(pushKind);
}
}
} catch (e) {
Log.error(e);
}
}
Future<PushKind?> tryDecryptMessage(
List<int> key, PushNotification noti) async {
try {
final chacha20 = Chacha20.poly1305Aead();
SecretKeyData secretKeyData = SecretKeyData(key);
SecretBox secretBox = SecretBox(
noti.cipherText,
nonce: noti.nonce,
mac: Mac(noti.mac),
);
final plaintext =
await chacha20.decrypt(secretBox, secretKey: secretKeyData);
final plaintextString = utf8.decode(plaintext);
return PushKindExtension.fromString(plaintextString);
} catch (e) {
// this error is allowed to happen...
return null;
}
}
Future showLocalPushNotification(
PushUser pushUser,
int fromUserId,
PushKind pushKind,
) async {
String? title;
String? body;
// do not show notification for blocked users...
if (pushUser.blocked) {
Log.info("Blocked a message from a blocked user!");
return;
}
title = pushUser.displayName;
body = getPushNotificationText(pushKind);
if (body == "") {
Log.error("No push notification type defined!");
}
FilePathAndroidBitmap? styleInformation;
String? avatarPath = await getAvatarIcon(fromUserId);
if (avatarPath != null) {
styleInformation = FilePathAndroidBitmap(avatarPath);
}
AndroidNotificationDetails androidNotificationDetails =
AndroidNotificationDetails('0', 'Messages',
channelDescription: 'Messages from other users.',
importance: Importance.max,
priority: Priority.max,
ticker: 'You got a new message.',
largeIcon: styleInformation);
const DarwinNotificationDetails darwinNotificationDetails =
DarwinNotificationDetails();
NotificationDetails notificationDetails = NotificationDetails(
android: androidNotificationDetails, iOS: darwinNotificationDetails);
await flutterLocalNotificationsPlugin.show(
fromUserId,
title,
body,
notificationDetails,
payload: pushKind.name,
);
}
Future showLocalPushNotificationWithoutUserId(
PushKind pushKind,
) async {
String? title;
String? body;
body = getPushNotificationTextWithoutUserId(pushKind);
if (body == "") {
Log.error("No push notification type defined!");
}
AndroidNotificationDetails androidNotificationDetails =
AndroidNotificationDetails('0', 'Messages',
channelDescription: 'Messages from other users.',
importance: Importance.max,
priority: Priority.max,
ticker: 'You got a new message.');
const DarwinNotificationDetails darwinNotificationDetails =
DarwinNotificationDetails();
NotificationDetails notificationDetails = NotificationDetails(
android: androidNotificationDetails, iOS: darwinNotificationDetails);
await flutterLocalNotificationsPlugin.show(
2,
title,
body,
notificationDetails,
payload: pushKind.name,
);
}
Future<String?> getAvatarIcon(int contactId) async {
final directory = await getApplicationCacheDirectory();
final avatarsDirectory = Directory('${directory.path}/avatars');
final filePath = '${avatarsDirectory.path}/$contactId.png';
final file = File(filePath);
if (file.existsSync()) {
return filePath;
}
return null;
}
String getPushNotificationTextWithoutUserId(PushKind pushKind) {
Map<String, String> pushNotificationText;
String systemLanguage = Platform.localeName;
if (systemLanguage.contains("de")) {
pushNotificationText = {
PushKind.text.name: "Du hast eine neue Nachricht erhalten.",
PushKind.twonly.name: "Du hast ein neues twonly erhalten.",
PushKind.video.name: "Du hast ein neues Video erhalten.",
PushKind.image.name: "Du hast ein neues Bild erhalten.",
PushKind.contactRequest.name:
"Du hast eine neue Kontaktanfrage erhalten.",
PushKind.acceptRequest.name: "Deine Kontaktanfrage wurde angenommen.",
PushKind.storedMediaFile.name: "Dein Bild wurde gespeichert.",
PushKind.reaction.name: "Du hast eine Reaktion auf dein Bild erhalten.",
PushKind.reopenedMedia.name: "Dein Bild wurde erneut geöffnet.",
PushKind.reactionToVideo.name:
"Du hast eine Reaktion auf dein Video erhalten.",
PushKind.reactionToText.name:
"Du hast eine Reaktion auf deinen Text erhalten.",
PushKind.reactionToImage.name:
"Du hast eine Reaktion auf dein Bild erhalten.",
PushKind.response.name: "Du hast eine Antwort erhalten.",
};
} else {
pushNotificationText = {
PushKind.text.name: "You have received a new message.",
PushKind.twonly.name: "You have received a new twonly.",
PushKind.video.name: "You have received a new video.",
PushKind.image.name: "You have received a new image.",
PushKind.contactRequest.name: "You have received a new contact request.",
PushKind.acceptRequest.name: "Your contact request has been accepted.",
PushKind.storedMediaFile.name: "Your image has been saved.",
PushKind.reaction.name: "You have received a reaction to your image.",
PushKind.reopenedMedia.name: "Your image has been reopened.",
PushKind.reactionToVideo.name:
"You have received a reaction to your video.",
PushKind.reactionToText.name:
"You have received a reaction to your text.",
PushKind.reactionToImage.name:
"You have received a reaction to your image.",
PushKind.response.name: "You have received a response.",
};
}
return pushNotificationText[pushKind.name] ?? "";
}
String getPushNotificationText(PushKind pushKind) {
String systemLanguage = Platform.localeName;
Map<String, String> pushNotificationText;
if (systemLanguage.contains("de")) {
pushNotificationText = {
PushKind.text.name: "hat dir eine Nachricht gesendet.",
PushKind.twonly.name: "hat dir ein twonly gesendet.",
PushKind.video.name: "hat dir ein Video gesendet.",
PushKind.image.name: "hat dir ein Bild gesendet.",
PushKind.contactRequest.name: "möchte sich mit dir vernetzen.",
PushKind.acceptRequest.name: "ist jetzt mit dir vernetzt.",
PushKind.storedMediaFile.name: "hat dein Bild gespeichert.",
PushKind.reaction.name: "hat auf dein Bild reagiert.",
PushKind.reopenedMedia.name: "hat dein Bild erneut geöffnet.",
PushKind.reactionToVideo.name: "hat auf dein Video reagiert.",
PushKind.reactionToText.name: "hat auf deinen Text reagiert.",
PushKind.reactionToImage.name: "hat auf dein Bild reagiert.",
PushKind.response.name: "hat dir geantwortet.",
};
} else {
pushNotificationText = {
PushKind.text.name: "has sent you a message.",
PushKind.twonly.name: "has sent you a twonly.",
PushKind.video.name: "has sent you a video.",
PushKind.image.name: "has sent you an image.",
PushKind.contactRequest.name: "wants to connect with you.",
PushKind.acceptRequest.name: "is now connected with you.",
PushKind.storedMediaFile.name: "has stored your image.",
PushKind.reaction.name: "has reacted to your image.",
PushKind.reopenedMedia.name: "has reopened your image.",
PushKind.reactionToVideo.name: "has reacted to your video.",
PushKind.reactionToText.name: "has reacted to your text.",
PushKind.reactionToImage.name: "has reacted to your image.",
PushKind.response.name: "has responded.",
};
}
return pushNotificationText[pushKind.name] ?? "";
}

View file

@ -290,72 +290,6 @@ Future<Uint8List?> getPushData(int toUserId, PushKind kind) async {
return Utf8Encoder().convert(jsonEncode(res.toJson()));
}
Future<PushKind?> tryDecryptMessage(
List<int> key, PushNotification noti) async {
try {
final chacha20 = Chacha20.poly1305Aead();
SecretKeyData secretKeyData = SecretKeyData(key);
SecretBox secretBox = SecretBox(
noti.cipherText,
nonce: noti.nonce,
mac: Mac(noti.mac),
);
final plaintext =
await chacha20.decrypt(secretBox, secretKey: secretKeyData);
final plaintextString = utf8.decode(plaintext);
return PushKindExtension.fromString(plaintextString);
} catch (e) {
// this error is allowed to happen...
return null;
}
}
Future handlePushData(String pushDataJson) async {
try {
String jsonString = utf8.decode(base64.decode(pushDataJson));
final pushData = PushNotification.fromJson(jsonDecode(jsonString));
PushKind? pushKind;
int? fromUserId;
if (pushData.keyId == 0) {
List<int> key = "InsecureOnlyUsedForAddingContact".codeUnits;
pushKind = await tryDecryptMessage(key, pushData);
} else {
var pushKeys = await getPushKeys("receivingPushKeys");
for (final userId in pushKeys.keys) {
for (final key in pushKeys[userId]!.keys) {
if (key.id == pushData.keyId) {
pushKind = await tryDecryptMessage(key.key, pushData);
if (pushKind != null) {
fromUserId = userId;
break;
}
}
}
// found correct key and user
if (fromUserId != null) break;
}
}
if (pushKind != null) {
if (pushKind == PushKind.testNotification) {
await customLocalPushNotification(
"Test notification", "This is a test notification.");
} else if (fromUserId != null) {
await showLocalPushNotification(fromUserId, pushKind);
} else {
await showLocalPushNotificationWithoutUserId(pushKind);
await setupNotificationWithUsers();
}
}
} catch (e) {
Log.error(e);
}
}
Future<Map<int, PushUser>> getPushKeys(String storageKey) async {
var storage = FlutterSecureStorage();
String? pushKeysJson = await storage.read(
@ -455,208 +389,17 @@ Future<void> setupPushNotification() async {
);
}
Future showLocalPushNotification(
int fromUserId,
PushKind pushKind,
) async {
String? title;
String? body;
Contact? user = await twonlyDB.contactsDao
.getContactByUserId(fromUserId)
.getSingleOrNull();
if (user == null) return;
// do not show notification for blocked users...
if (user.blocked) {
Log.info("Blocked a message from a blocked user!");
return;
Future createPushAvatars() async {
if (!Platform.isAndroid) {
return; // avatars currently only shown in Android...
}
final contacts = await twonlyDB.contactsDao.getAllNotBlockedContacts();
title = getContactDisplayName(user);
body = getPushNotificationText(pushKind);
if (body == "") {
Log.error("No push notification type defined!");
}
FilePathAndroidBitmap? styleInformation;
String? avatarPath = await getAvatarIcon(user);
if (avatarPath != null) {
styleInformation = FilePathAndroidBitmap(avatarPath);
}
AndroidNotificationDetails androidNotificationDetails =
AndroidNotificationDetails('0', 'Messages',
channelDescription: 'Messages from other users.',
importance: Importance.max,
priority: Priority.max,
ticker: 'You got a new message.',
largeIcon: styleInformation);
const DarwinNotificationDetails darwinNotificationDetails =
DarwinNotificationDetails();
NotificationDetails notificationDetails = NotificationDetails(
android: androidNotificationDetails, iOS: darwinNotificationDetails);
await flutterLocalNotificationsPlugin.show(
fromUserId,
title,
body,
notificationDetails,
payload: pushKind.name,
);
}
Future showLocalPushNotificationWithoutUserId(
PushKind pushKind,
) async {
String? title;
String? body;
body = getPushNotificationTextWithoutUserId(pushKind);
if (body == "") {
Log.error("No push notification type defined!");
}
AndroidNotificationDetails androidNotificationDetails =
AndroidNotificationDetails('0', 'Messages',
channelDescription: 'Messages from other users.',
importance: Importance.max,
priority: Priority.max,
ticker: 'You got a new message.');
const DarwinNotificationDetails darwinNotificationDetails =
DarwinNotificationDetails();
NotificationDetails notificationDetails = NotificationDetails(
android: androidNotificationDetails, iOS: darwinNotificationDetails);
await flutterLocalNotificationsPlugin.show(
2,
title,
body,
notificationDetails,
payload: pushKind.name,
);
}
Future customLocalPushNotification(String title, String msg) async {
const AndroidNotificationDetails androidNotificationDetails =
AndroidNotificationDetails(
'1',
'System',
channelDescription: 'System messages.',
importance: Importance.max,
priority: Priority.max,
);
const DarwinNotificationDetails darwinNotificationDetails =
DarwinNotificationDetails();
const NotificationDetails notificationDetails = NotificationDetails(
android: androidNotificationDetails, iOS: darwinNotificationDetails);
await flutterLocalNotificationsPlugin.show(
999999 + Random.secure().nextInt(9999),
title,
msg,
notificationDetails,
);
}
String getPushNotificationTextWithoutUserId(PushKind pushKind) {
Map<String, String> pushNotificationText;
String systemLanguage = Platform.localeName;
if (systemLanguage.contains("de")) {
pushNotificationText = {
PushKind.text.name: "Du hast eine neue Nachricht erhalten.",
PushKind.twonly.name: "Du hast ein neues twonly erhalten.",
PushKind.video.name: "Du hast ein neues Video erhalten.",
PushKind.image.name: "Du hast ein neues Bild erhalten.",
PushKind.contactRequest.name:
"Du hast eine neue Kontaktanfrage erhalten.",
PushKind.acceptRequest.name: "Deine Kontaktanfrage wurde angenommen.",
PushKind.storedMediaFile.name: "Dein Bild wurde gespeichert.",
PushKind.reaction.name: "Du hast eine Reaktion auf dein Bild erhalten.",
PushKind.reopenedMedia.name: "Dein Bild wurde erneut geöffnet.",
PushKind.reactionToVideo.name:
"Du hast eine Reaktion auf dein Video erhalten.",
PushKind.reactionToText.name:
"Du hast eine Reaktion auf deinen Text erhalten.",
PushKind.reactionToImage.name:
"Du hast eine Reaktion auf dein Bild erhalten.",
PushKind.response.name: "Du hast eine Antwort erhalten.",
};
} else {
pushNotificationText = {
PushKind.text.name: "You have received a new message.",
PushKind.twonly.name: "You have received a new twonly.",
PushKind.video.name: "You have received a new video.",
PushKind.image.name: "You have received a new image.",
PushKind.contactRequest.name: "You have received a new contact request.",
PushKind.acceptRequest.name: "Your contact request has been accepted.",
PushKind.storedMediaFile.name: "Your image has been saved.",
PushKind.reaction.name: "You have received a reaction to your image.",
PushKind.reopenedMedia.name: "Your image has been reopened.",
PushKind.reactionToVideo.name:
"You have received a reaction to your video.",
PushKind.reactionToText.name:
"You have received a reaction to your text.",
PushKind.reactionToImage.name:
"You have received a reaction to your image.",
PushKind.response.name: "You have received a response.",
};
}
return pushNotificationText[pushKind.name] ?? "";
}
String getPushNotificationText(PushKind pushKind) {
String systemLanguage = Platform.localeName;
Map<String, String> pushNotificationText;
if (systemLanguage.contains("de")) {
pushNotificationText = {
PushKind.text.name: "hat dir eine Nachricht gesendet.",
PushKind.twonly.name: "hat dir ein twonly gesendet.",
PushKind.video.name: "hat dir ein Video gesendet.",
PushKind.image.name: "hat dir ein Bild gesendet.",
PushKind.contactRequest.name: "möchte sich mit dir vernetzen.",
PushKind.acceptRequest.name: "ist jetzt mit dir vernetzt.",
PushKind.storedMediaFile.name: "hat dein Bild gespeichert.",
PushKind.reaction.name: "hat auf dein Bild reagiert.",
PushKind.reopenedMedia.name: "hat dein Bild erneut geöffnet.",
PushKind.reactionToVideo.name: "hat auf dein Video reagiert.",
PushKind.reactionToText.name: "hat auf deinen Text reagiert.",
PushKind.reactionToImage.name: "hat auf dein Bild reagiert.",
PushKind.response.name: "hat dir geantwortet.",
};
} else {
pushNotificationText = {
PushKind.text.name: "has sent you a message.",
PushKind.twonly.name: "has sent you a twonly.",
PushKind.video.name: "has sent you a video.",
PushKind.image.name: "has sent you an image.",
PushKind.contactRequest.name: "wants to connect with you.",
PushKind.acceptRequest.name: "is now connected with you.",
PushKind.storedMediaFile.name: "has stored your image.",
PushKind.reaction.name: "has reacted to your image.",
PushKind.reopenedMedia.name: "has reopened your image.",
PushKind.reactionToVideo.name: "has reacted to your video.",
PushKind.reactionToText.name: "has reacted to your text.",
PushKind.reactionToImage.name: "has reacted to your image.",
PushKind.response.name: "has responded.",
};
}
return pushNotificationText[pushKind.name] ?? "";
}
Future<String?> getAvatarIcon(Contact user) async {
if (user.avatarSvg == null) return null;
for (final contact in contacts) {
if (contact.avatarSvg == null) return null;
final PictureInfo pictureInfo =
await vg.loadPicture(SvgStringLoader(user.avatarSvg!), null);
await vg.loadPicture(SvgStringLoader(contact.avatarSvg!), null);
final ui.Image image = await pictureInfo.picture.toImage(300, 300);
@ -672,12 +415,8 @@ Future<String?> getAvatarIcon(Contact user) async {
if (!await avatarsDirectory.exists()) {
await avatarsDirectory.create(recursive: true);
}
final filePath = '${avatarsDirectory.path}/${user.userId}.png';
final file = File(filePath);
await file.writeAsBytes(pngBytes);
final filePath = '${avatarsDirectory.path}/${contact.userId}.png';
await File(filePath).writeAsBytes(pngBytes);
pictureInfo.picture.dispose();
return filePath;
}
}

View file

@ -21,7 +21,6 @@ Future performTwonlySafeBackup({bool force = false}) async {
final user = await getUser();
if (user == null || user.twonlySafeBackup == null || user.isDemoUser) {
Log.warn("perform twonly safe backup was called while it is disabled");
return;
}