twonly-app/lib/src/services/notifications/background.notifications.dart
2026-04-06 00:24:17 +02:00

323 lines
10 KiB
Dart

import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'dart:math';
import 'package:cryptography_flutter_plus/cryptography_flutter_plus.dart';
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/constants/routes.keys.dart';
import 'package:twonly/src/constants/secure_storage_keys.dart';
import 'package:twonly/src/localization/generated/app_localizations.dart';
import 'package:twonly/src/localization/generated/app_localizations_de.dart';
import 'package:twonly/src/localization/generated/app_localizations_en.dart';
import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart';
import 'package:twonly/src/model/protobuf/client/generated/push_notification.pb.dart';
import 'package:twonly/src/services/notifications/pushkeys.notifications.dart';
import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/misc.dart';
final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin =
FlutterLocalNotificationsPlugin();
Future<void> customLocalPushNotification(String title, String msg) async {
final androidNotificationDetails = AndroidNotificationDetails(
'1',
'System',
channelDescription: 'System messages.',
importance: Importance.high,
priority: Priority.high,
styleInformation: BigTextStyleInformation(msg),
icon: 'ic_launcher_foreground',
);
const darwinNotificationDetails = DarwinNotificationDetails();
final notificationDetails = NotificationDetails(
android: androidNotificationDetails,
iOS: darwinNotificationDetails,
);
final id = Random.secure().nextInt(9999);
await flutterLocalNotificationsPlugin.show(
id,
title,
msg,
notificationDetails,
);
}
Future<void> showPushNotificationFromServerMessages(
int fromUserId,
EncryptedContent encryptedContent,
) async {
final pushData = await getPushNotificationFromEncryptedContent(
null, // this is the toUserID which must be null as this means that the targetMessageId was send from this user.
null,
encryptedContent,
);
if (pushData != null) {
final pushUsers = await getPushKeys(SecureStorageKeys.receivingPushKeys);
for (final pushUser in pushUsers) {
if (pushUser.userId.toInt() == fromUserId) {
String? groupId;
if (encryptedContent.hasGroupId()) {
groupId = encryptedContent.groupId;
}
return showLocalPushNotification(pushUser, pushData, groupId: groupId);
}
}
}
}
Future<void> handlePushData(String pushDataB64) async {
try {
final pushData = EncryptedPushNotification.fromBuffer(
base64.decode(pushDataB64),
);
PushNotification? pushNotification;
PushUser? foundPushUser;
if (pushData.keyId == 0) {
final key = 'InsecureOnlyUsedForAddingContact'.codeUnits;
pushNotification = await tryDecryptMessage(key, pushData);
} else {
final pushUsers = await getPushKeys(SecureStorageKeys.receivingPushKeys);
for (final pushUser in pushUsers) {
for (final key in pushUser.pushKeys) {
if (key.id == pushData.keyId) {
pushNotification = await tryDecryptMessage(key.key, pushData);
if (pushNotification != null) {
foundPushUser = pushUser;
break;
}
}
}
// found correct key and user
if (foundPushUser != null) break;
}
}
if (pushNotification != null) {
if (pushNotification.kind == PushKind.testNotification) {
await customLocalPushNotification(
'Test notification',
'This is a test notification.',
);
} else if (foundPushUser != null) {
if (pushNotification.hasMessageId()) {
if (isUUIDNewer(
foundPushUser.lastMessageId,
pushNotification.messageId,
)) {
Log.info(
'Got a push notification for a message which was already opened.',
);
return;
}
}
await showLocalPushNotification(foundPushUser, pushNotification);
} else {
await showLocalPushNotificationWithoutUserId(pushNotification);
}
}
} catch (e) {
Log.error(e);
final lang = getLocalizations();
await customLocalPushNotification(
lang.notificationTitleUnknown,
lang.notificationBodyUnknown,
);
}
}
Future<PushNotification?> tryDecryptMessage(
List<int> key,
EncryptedPushNotification push,
) async {
try {
final chacha20 = FlutterChacha20.poly1305Aead();
final secretKeyData = SecretKeyData(key);
final secretBox = SecretBox(
push.ciphertext,
nonce: push.nonce,
mac: Mac(push.mac),
);
final plaintext = await chacha20.decrypt(
secretBox,
secretKey: secretKeyData,
);
return PushNotification.fromBuffer(plaintext);
} catch (e) {
// this error is allowed to happen...
return null;
}
}
Future<void> showLocalPushNotification(
PushUser pushUser,
PushNotification pushNotification, {
String? groupId,
}) 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(pushNotification);
if (body == '') {
Log.error('No push notification type defined!');
}
FilePathAndroidBitmap? styleInformation;
final avatarPath = await getAvatarIcon(pushUser.userId.toInt());
if (avatarPath != null) {
styleInformation = FilePathAndroidBitmap(avatarPath);
}
final lang = getLocalizations();
final androidNotificationDetails = AndroidNotificationDetails(
'0',
lang.notificationCategoryMessageTitle,
channelDescription: lang.notificationCategoryMessageDesc,
importance: Importance.max,
priority: Priority.max,
ticker: 'You got a new message.',
largeIcon: styleInformation,
icon: 'ic_launcher_foreground',
);
const darwinNotificationDetails = DarwinNotificationDetails();
final notificationDetails = NotificationDetails(
android: androidNotificationDetails,
iOS: darwinNotificationDetails,
);
String? payload;
if (groupId != null &&
(pushNotification.kind == PushKind.text ||
pushNotification.kind == PushKind.response ||
pushNotification.kind == PushKind.reactionToAudio ||
pushNotification.kind == PushKind.storedMediaFile ||
pushNotification.kind == PushKind.reactionToImage ||
pushNotification.kind == PushKind.reactionToText ||
pushNotification.kind == PushKind.reactionToAudio)) {
payload = Routes.chatsMessages(groupId);
}
await flutterLocalNotificationsPlugin.show(
// Invalid argument (id): must fit within the size of a 32-bit integer
pushUser.userId.toInt() % 2147483647,
title,
body,
notificationDetails,
payload: payload,
);
}
Future<void> showLocalPushNotificationWithoutUserId(
PushNotification pushNotification,
) async {
final lang = getLocalizations();
var title = lang.notificationTitleUnknown;
var body = lang.notificationBodyUnknown;
if (pushNotification.kind == PushKind.contactRequest) {
title = lang.you;
body = lang.notificationContactRequestUnknownUser;
}
final androidNotificationDetails = AndroidNotificationDetails(
'0',
lang.notificationCategoryMessageTitle,
channelDescription: lang.notificationCategoryMessageDesc,
importance: Importance.max,
priority: Priority.max,
ticker: 'You got a new message.',
);
const darwinNotificationDetails = DarwinNotificationDetails();
final notificationDetails = NotificationDetails(
android: androidNotificationDetails,
iOS: darwinNotificationDetails,
);
await flutterLocalNotificationsPlugin.show(
2,
title,
body,
notificationDetails,
payload: pushNotification.kind.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;
}
AppLocalizations getLocalizations() {
final systemLanguage = Platform.localeName;
if (systemLanguage.contains('de')) return AppLocalizationsDe();
return AppLocalizationsEn();
}
String getPushNotificationText(PushNotification pushNotification) {
final lang = getLocalizations();
var inGroup = '';
if (pushNotification.hasAdditionalContent()) {
inGroup =
' ${lang.notificationFillerIn} ${pushNotification.additionalContent}';
}
final pushNotificationText = {
PushKind.text.name: lang.notificationText(inGroup),
PushKind.twonly.name: lang.notificationTwonly(inGroup),
PushKind.video.name: lang.notificationVideo(inGroup),
PushKind.image.name: lang.notificationImage(inGroup),
PushKind.audio.name: lang.notificationAudio(inGroup),
PushKind.contactRequest.name: lang.notificationContactRequest,
PushKind.acceptRequest.name: lang.notificationAcceptRequest,
PushKind.storedMediaFile.name: lang.notificationStoredMediaFile,
PushKind.reaction.name: lang.notificationReaction,
PushKind.reopenedMedia.name: lang.notificationReopenedMedia,
PushKind.reactionToVideo.name: lang.notificationReactionToVideo(
pushNotification.additionalContent,
),
PushKind.reactionToAudio.name: lang.notificationReactionToAudio(
pushNotification.additionalContent,
),
PushKind.reactionToText.name: lang.notificationReactionToText(
pushNotification.additionalContent,
),
PushKind.reactionToImage.name: lang.notificationReactionToImage(
pushNotification.additionalContent,
),
PushKind.response.name: lang.notificationResponse(inGroup),
PushKind.addedToGroup.name: lang.notificationAddedToGroup(
pushNotification.additionalContent,
),
};
return pushNotificationText[pushNotification.kind.name] ?? '';
}