mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-01-15 12:48:41 +00:00
new push system does work on android #53
This commit is contained in:
parent
773f076abd
commit
85dbac37fb
16 changed files with 601 additions and 122 deletions
|
|
@ -41,6 +41,7 @@ void main() async {
|
|||
|
||||
apiProvider = ApiProvider();
|
||||
twonlyDatabase = TwonlyDatabase();
|
||||
setupNotificationWithUsers();
|
||||
|
||||
runApp(
|
||||
MultiProvider(
|
||||
|
|
|
|||
|
|
@ -11,8 +11,7 @@ enum MessageKind {
|
|||
acceptRequest,
|
||||
opened,
|
||||
ack,
|
||||
pushKey,
|
||||
pushKeyAck,
|
||||
pushKey
|
||||
}
|
||||
|
||||
enum DownloadState {
|
||||
|
|
|
|||
|
|
@ -88,6 +88,8 @@ class MessageContent {
|
|||
return ProfileContent.fromJson(json);
|
||||
case MessageKind.storedMediaFile:
|
||||
return StoredMediaFileContent.fromJson(json);
|
||||
case MessageKind.pushKey:
|
||||
return PushKeyContent.fromJson(json);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
|
@ -203,3 +205,24 @@ class ProfileContent extends MessageContent {
|
|||
return {'avatarSvg': avatarSvg, 'displayName': displayName};
|
||||
}
|
||||
}
|
||||
|
||||
class PushKeyContent extends MessageContent {
|
||||
int keyId;
|
||||
List<int> key;
|
||||
PushKeyContent({required this.keyId, required this.key});
|
||||
|
||||
static PushKeyContent fromJson(Map json) {
|
||||
return PushKeyContent(
|
||||
keyId: json['keyId'],
|
||||
key: List<int>.from(json['key']),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Map toJson() {
|
||||
return {
|
||||
'keyId': keyId,
|
||||
'key': key,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -65,6 +65,10 @@
|
|||
"settingsPrivacyBlockUsersDesc": "Blockierte Benutzer können nicht mit dir kommunizieren. Du kannst einen blockierten Benutzer jederzeit wieder entsperren.",
|
||||
"settingsPrivacyBlockUsersCount": "{len} Kontakt(e)",
|
||||
"settingsNotification": "Benachrichtigung",
|
||||
"settingsNotifyTroubleshooting": "Fehlersuche",
|
||||
"settingsNotifyTroubleshootingDesc": "Hier klicken, wenn Probleme beim Empfang von Push-Benachrichtigungen auftreten.",
|
||||
"settingsNotifyTroubleshootingNoProblem": "Kein Problem festgestellt",
|
||||
"settingsNotifyTroubleshootingNoProblemDesc": "Klicke auf OK, um eine Testbenachrichtigung zu erhalten. Wenn du auch nach 10 Minuten warten keine Nachricht erhältst, sende uns bitte dein Diagnoseprotokoll unter Einstellungen > Hilfe > Diagnoseprotokoll, damit wir uns das Problem ansehen können.",
|
||||
"settingsHelp": "Hilfe",
|
||||
"settingsHelpSupport": "Support-Center",
|
||||
"settingsHelpDiagnostics": "Diagnoseprotokoll",
|
||||
|
|
|
|||
|
|
@ -138,6 +138,14 @@
|
|||
},
|
||||
"settingsNotification": "Notification",
|
||||
"@settingsNotification": {},
|
||||
"settingsNotifyTroubleshooting": "Troubleshooting",
|
||||
"@settingsNotifyTroubleshooting": {},
|
||||
"settingsNotifyTroubleshootingDesc": "Click here if you have problems receiving push notifications.",
|
||||
"@settingsNotifyTroubleshootingDesc": {},
|
||||
"settingsNotifyTroubleshootingNoProblem": "No problem detected",
|
||||
"@settingsNotifyTroubleshootingNoProblem": {},
|
||||
"settingsNotifyTroubleshootingNoProblemDesc": "Press OK to receive a test notification. When you receive no message even after waiting for 10 minutes, please send us your debug log in Settings > Help > Debug log, so we can look at that issue.",
|
||||
"@settingsNotifyTroubleshootingNoProblemDesc": {},
|
||||
"settingsHelp": "Help",
|
||||
"@settingsHelp": {},
|
||||
"settingsHelpDiagnostics": "Diagnostic protocol",
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import 'package:twonly/src/json_models/userdata.dart';
|
|||
import 'package:twonly/src/proto/api/error.pb.dart';
|
||||
import 'package:twonly/src/providers/api/api_utils.dart';
|
||||
import 'package:twonly/src/providers/hive.dart';
|
||||
import 'package:twonly/src/services/notification_service.dart';
|
||||
// ignore: library_prefixes
|
||||
import 'package:twonly/src/utils/signal.dart' as SignalHelper;
|
||||
import 'package:twonly/src/utils/storage.dart';
|
||||
|
|
@ -31,7 +32,9 @@ Future tryTransmitMessages() async {
|
|||
Result resp = await apiProvider.sendTextMessage(
|
||||
msg.userId,
|
||||
msg.bytes,
|
||||
msg.pushData,
|
||||
);
|
||||
|
||||
if (resp.isSuccess) {
|
||||
if (msg.messageId != null) {
|
||||
await twonlyDatabase.messagesDao.updateMessageByMessageId(
|
||||
|
|
@ -53,8 +56,13 @@ class RetransmitMessage {
|
|||
int? messageId;
|
||||
int userId;
|
||||
Uint8List bytes;
|
||||
RetransmitMessage(
|
||||
{this.messageId, required this.userId, required this.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) {
|
||||
|
|
@ -62,6 +70,7 @@ class RetransmitMessage {
|
|||
messageId: json['messageId'],
|
||||
userId: json['userId'],
|
||||
bytes: base64Decode(json['bytes']),
|
||||
pushData: json['pushData'],
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -71,6 +80,7 @@ class RetransmitMessage {
|
|||
'messageId': messageId,
|
||||
'userId': userId,
|
||||
'bytes': base64Encode(bytes),
|
||||
'pushData': pushData,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -88,7 +98,8 @@ Future<Map<String, dynamic>> getAllMessagesForRetransmitting() async {
|
|||
|
||||
// this functions ensures that the message is received by the server and in case of errors will try again later
|
||||
Future<Result> encryptAndSendMessage(
|
||||
int? messageId, int userId, MessageJson msg) async {
|
||||
int? messageId, int userId, MessageJson msg,
|
||||
{PushKind? pushKind}) async {
|
||||
Uint8List? bytes = await SignalHelper.encryptMessage(msg, userId);
|
||||
|
||||
if (bytes == null) {
|
||||
|
|
@ -99,6 +110,11 @@ Future<Result> encryptAndSendMessage(
|
|||
String stateId = (messageId ?? (60001 + Random().nextInt(100000))).toString();
|
||||
Box box = await getMediaStorage();
|
||||
|
||||
List<int>? pushData;
|
||||
if (pushKind != null) {
|
||||
pushData = await getPushData(userId, pushKind);
|
||||
}
|
||||
|
||||
{
|
||||
var retransmit = await getAllMessagesForRetransmitting();
|
||||
|
||||
|
|
@ -106,12 +122,13 @@ Future<Result> encryptAndSendMessage(
|
|||
messageId: messageId,
|
||||
userId: userId,
|
||||
bytes: bytes,
|
||||
pushData: pushData,
|
||||
).toJson());
|
||||
|
||||
box.put("messages-to-retransmit", jsonEncode(retransmit));
|
||||
}
|
||||
|
||||
Result resp = await apiProvider.sendTextMessage(userId, bytes);
|
||||
Result resp = await apiProvider.sendTextMessage(userId, bytes, pushData);
|
||||
|
||||
if (resp.isSuccess) {
|
||||
if (messageId != null) {
|
||||
|
|
@ -133,7 +150,8 @@ Future<Result> encryptAndSendMessage(
|
|||
return resp;
|
||||
}
|
||||
|
||||
Future sendTextMessage(int target, TextMessageContent content) async {
|
||||
Future sendTextMessage(
|
||||
int target, TextMessageContent content, PushKind? pushKind) async {
|
||||
DateTime messageSendAt = DateTime.now();
|
||||
|
||||
int? messageId = await twonlyDatabase.messagesDao.insertMessage(
|
||||
|
|
@ -158,7 +176,7 @@ Future sendTextMessage(int target, TextMessageContent content) async {
|
|||
timestamp: messageSendAt,
|
||||
);
|
||||
|
||||
encryptAndSendMessage(messageId, target, msg);
|
||||
encryptAndSendMessage(messageId, target, msg, pushKind: pushKind);
|
||||
}
|
||||
|
||||
Future notifyContactAboutOpeningMessage(
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import 'package:twonly/src/proto/api/server_to_client.pb.dart';
|
|||
import 'package:twonly/src/providers/api/api.dart';
|
||||
import 'package:twonly/src/providers/api/api_utils.dart';
|
||||
import 'package:twonly/src/providers/hive.dart';
|
||||
import 'package:twonly/src/services/notification_service.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
|
||||
Future tryDownloadAllMediaFiles() async {
|
||||
|
|
@ -280,6 +281,7 @@ class ImageUploader {
|
|||
),
|
||||
timestamp: metadata.messageSendAt,
|
||||
),
|
||||
pushKind: PushKind.image,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -190,7 +190,6 @@ Future<client.Response> handleNewMessage(int fromUserId, Uint8List body) async {
|
|||
case MessageKind.acceptRequest:
|
||||
final update = ContactsCompanion(accepted: Value(true));
|
||||
await twonlyDatabase.contactsDao.updateContact(fromUserId, update);
|
||||
localPushNotificationNewMessage(fromUserId.toInt(), message, 8888888);
|
||||
notifyContactsAboutProfileChange();
|
||||
break;
|
||||
|
||||
|
|
@ -214,6 +213,14 @@ Future<client.Response> handleNewMessage(int fromUserId, Uint8List body) async {
|
|||
);
|
||||
break;
|
||||
|
||||
case MessageKind.pushKey:
|
||||
if (message.content != null) {
|
||||
final pushKey = message.content!;
|
||||
if (pushKey is PushKeyContent) {
|
||||
await handleNewPushKey(fromUserId, pushKey);
|
||||
}
|
||||
}
|
||||
|
||||
default:
|
||||
if (message.kind != MessageKind.textMessage &&
|
||||
message.kind != MessageKind.media &&
|
||||
|
|
@ -299,7 +306,6 @@ Future<client.Response> handleNewMessage(int fromUserId, Uint8List body) async {
|
|||
}
|
||||
}
|
||||
}
|
||||
localPushNotificationNewMessage(fromUserId, message, messageId);
|
||||
}
|
||||
}
|
||||
var ok = client.Response_Ok()..none = true;
|
||||
|
|
@ -328,21 +334,13 @@ Future<client.Response> handleContactRequest(
|
|||
if (username.isSuccess) {
|
||||
Uint8List name = username.value.userdata.username;
|
||||
|
||||
int added = await twonlyDatabase.contactsDao.insertContact(
|
||||
await twonlyDatabase.contactsDao.insertContact(
|
||||
ContactsCompanion(
|
||||
username: Value(utf8.decode(name)),
|
||||
userId: Value(fromUserId),
|
||||
requested: Value(true),
|
||||
),
|
||||
);
|
||||
|
||||
if (added > 0) {
|
||||
localPushNotificationNewMessage(
|
||||
fromUserId,
|
||||
message,
|
||||
999999,
|
||||
);
|
||||
}
|
||||
}
|
||||
var ok = client.Response_Ok()..none = true;
|
||||
return client.Response()..ok = ok;
|
||||
|
|
|
|||
|
|
@ -385,11 +385,16 @@ class ApiProvider {
|
|||
return await sendRequestSync(req);
|
||||
}
|
||||
|
||||
Future<Result> sendTextMessage(int target, Uint8List msg) async {
|
||||
Future<Result> sendTextMessage(
|
||||
int target, Uint8List msg, List<int>? pushData) async {
|
||||
var testMessage = ApplicationData_TextMessage()
|
||||
..userId = Int64(target)
|
||||
..body = msg;
|
||||
|
||||
if (pushData != null) {
|
||||
testMessage.pushData = pushData;
|
||||
}
|
||||
|
||||
var appData = ApplicationData()..textmessage = testMessage;
|
||||
var req = createClientToServerFromApplicationData(appData);
|
||||
|
||||
|
|
|
|||
|
|
@ -3,8 +3,7 @@ import 'package:firebase_messaging/firebase_messaging.dart';
|
|||
import 'package:logging/logging.dart';
|
||||
import 'package:twonly/globals.dart';
|
||||
import 'package:twonly/src/app.dart';
|
||||
import 'package:twonly/src/database/twonly_database.dart';
|
||||
import 'package:twonly/src/providers/api_provider.dart';
|
||||
import 'package:twonly/src/services/notification_service.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
import 'dart:io' show Platform;
|
||||
import '../../firebase_options.dart';
|
||||
|
|
@ -70,38 +69,38 @@ Future initFCMService() async {
|
|||
}
|
||||
}
|
||||
|
||||
// APNS token is available, make FCM plugin API requests...
|
||||
|
||||
FirebaseMessaging.onMessage.listen((RemoteMessage message) {
|
||||
print('Got a message whilst in the foreground!');
|
||||
if (!Platform.isAndroid) {
|
||||
Logger("firebase-notification").shout("Got message in Dart while on iOS");
|
||||
}
|
||||
|
||||
Logger("firebase-notification")
|
||||
.finer('Got a message while in the foreground!');
|
||||
|
||||
print('Message data: ${message.data}');
|
||||
|
||||
if (message.notification != null) {
|
||||
print('Message also contained a notification: ${message.notification}');
|
||||
String title = message.notification!.title ?? "";
|
||||
String body = message.notification!.body ?? "";
|
||||
customLocalPushNotification(title, body);
|
||||
}
|
||||
if (message.data["push_data"] != null) {
|
||||
handlePushData(message.data["push_data"]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@pragma('vm:entry-point')
|
||||
Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
|
||||
// Wenn Tasks länger als 30 Sekunden ausgeführt werden, wird der Prozess möglicherweise automatisch vom Gerät beendet.
|
||||
// -> offer backend via http?
|
||||
|
||||
Logger("firebase-background")
|
||||
.shout('Handling a background message: ${message.messageId}');
|
||||
|
||||
twonlyDatabase = TwonlyDatabase();
|
||||
if (!Platform.isAndroid) {
|
||||
Logger("firebase-notification").shout("Got message in Dart while on iOS");
|
||||
}
|
||||
|
||||
apiProvider = ApiProvider();
|
||||
await apiProvider.connect();
|
||||
|
||||
final stopwatch = Stopwatch()..start();
|
||||
while (true) {
|
||||
if (stopwatch.elapsed >= Duration(seconds: 20)) {
|
||||
Logger("firebase-background").shout('Exiting background handler');
|
||||
break;
|
||||
}
|
||||
await Future.delayed(Duration(milliseconds: 10));
|
||||
}
|
||||
await apiProvider.close(() {});
|
||||
Logger("firebase-notification")
|
||||
.finer('Got a message while in the background!');
|
||||
print('Message data: ${message.data}');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,13 +1,349 @@
|
|||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'dart:math';
|
||||
import 'dart:ui' as ui;
|
||||
import 'package:cryptography_plus/cryptography_plus.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:twonly/globals.dart';
|
||||
import 'package:twonly/src/database/daos/contacts_dao.dart';
|
||||
import 'package:twonly/src/database/tables/messages_table.dart';
|
||||
import 'package:twonly/src/database/twonly_database.dart';
|
||||
import 'package:twonly/src/json_models/message.dart' as my;
|
||||
import 'package:twonly/src/providers/api/api.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
|
||||
class PushUser {
|
||||
String displayName;
|
||||
List<PushKeyMeta> keys;
|
||||
|
||||
PushUser({
|
||||
required this.displayName,
|
||||
required this.keys,
|
||||
});
|
||||
|
||||
// Factory method to create a User from JSON
|
||||
factory PushUser.fromJson(Map<String, dynamic> json) {
|
||||
return PushUser(
|
||||
displayName: json['displayName'],
|
||||
keys: (json['keys'] as List)
|
||||
.map((keyJson) => PushKeyMeta.fromJson(keyJson))
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
|
||||
// Method to convert User to JSON
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'displayName': displayName,
|
||||
'keys': keys.map((key) => key.toJson()).toList(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class PushKeyMeta {
|
||||
int id;
|
||||
List<int> key;
|
||||
DateTime createdAt;
|
||||
|
||||
PushKeyMeta({
|
||||
required this.id,
|
||||
required this.key,
|
||||
required this.createdAt,
|
||||
});
|
||||
|
||||
// Factory method to create Keys from JSON
|
||||
factory PushKeyMeta.fromJson(Map<String, dynamic> json) {
|
||||
return PushKeyMeta(
|
||||
id: json['id'],
|
||||
key: List<int>.from(json['key']),
|
||||
createdAt: DateTime.fromMillisecondsSinceEpoch(json['createdAt']),
|
||||
);
|
||||
}
|
||||
|
||||
// Method to convert Keys to JSON
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'key': key,
|
||||
'createdAt': createdAt.millisecondsSinceEpoch, // Store as timestamp
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// This function must be called after the database is setup
|
||||
Future setupNotificationWithUsers({bool force = false}) async {
|
||||
var pushKeys = await getPushKeys("receivingPushKeys");
|
||||
|
||||
var wasChanged = false;
|
||||
|
||||
final random = Random.secure();
|
||||
|
||||
final contacts = await twonlyDatabase.contactsDao.getAllNotBlockedContacts();
|
||||
for (final contact in contacts) {
|
||||
if (pushKeys.containsKey(contact.userId)) {
|
||||
// make it harder to predict the change of the key
|
||||
final timeBefore =
|
||||
DateTime.now().subtract(Duration(days: 5 + random.nextInt(5)));
|
||||
final lastKey = pushKeys[contact.userId]!.keys.last;
|
||||
if (force || lastKey.createdAt.isBefore(timeBefore)) {
|
||||
final pushKey = PushKeyMeta(
|
||||
id: lastKey.id + 1,
|
||||
key: List<int>.generate(32, (index) => random.nextInt(256)),
|
||||
createdAt: DateTime.now(),
|
||||
);
|
||||
sendNewPushKey(contact.userId, pushKey);
|
||||
pushKeys[contact.userId]!.keys.add(pushKey);
|
||||
pushKeys[contact.userId]!.displayName = getContactDisplayName(contact);
|
||||
wasChanged = true;
|
||||
}
|
||||
} else {
|
||||
/// Insert a new pushuser
|
||||
final pushKey = PushKeyMeta(
|
||||
id: 1,
|
||||
key: List<int>.generate(32, (index) => random.nextInt(256)),
|
||||
createdAt: DateTime.now(),
|
||||
);
|
||||
sendNewPushKey(contact.userId, pushKey);
|
||||
final pushUser = PushUser(
|
||||
displayName: getContactDisplayName(contact),
|
||||
keys: [pushKey],
|
||||
);
|
||||
pushKeys[contact.userId] = pushUser;
|
||||
wasChanged = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (wasChanged) {
|
||||
await setPushKeys("receivingPushKeys", pushKeys);
|
||||
}
|
||||
}
|
||||
|
||||
Future sendNewPushKey(int userId, PushKeyMeta pushKey) async {
|
||||
await encryptAndSendMessage(
|
||||
null,
|
||||
userId,
|
||||
my.MessageJson(
|
||||
kind: MessageKind.pushKey,
|
||||
content: my.PushKeyContent(keyId: pushKey.id, key: pushKey.key),
|
||||
timestamp: pushKey.createdAt,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future handleNewPushKey(int fromUserId, my.PushKeyContent pushKey) async {
|
||||
var pushKeys = await getPushKeys("sendingPushKeys");
|
||||
|
||||
if (pushKeys[fromUserId] == null) {
|
||||
pushKeys[fromUserId] = PushUser(displayName: "-", keys: []);
|
||||
}
|
||||
|
||||
// only store the newest key...
|
||||
pushKeys[fromUserId]!.keys = [
|
||||
PushKeyMeta(
|
||||
id: pushKey.keyId,
|
||||
key: pushKey.key,
|
||||
createdAt: DateTime.now(),
|
||||
),
|
||||
];
|
||||
|
||||
await setPushKeys("sendingPushKeys", pushKeys);
|
||||
}
|
||||
|
||||
enum PushKind {
|
||||
reaction,
|
||||
text,
|
||||
video,
|
||||
twonly,
|
||||
image,
|
||||
contactRequest,
|
||||
acceptRequest,
|
||||
storedMediaFile,
|
||||
testNotification
|
||||
}
|
||||
|
||||
extension PushKindExtension on PushKind {
|
||||
String get name => toString().split('.').last;
|
||||
|
||||
static PushKind fromString(String name) {
|
||||
return PushKind.values.firstWhere((e) => e.name == name);
|
||||
}
|
||||
}
|
||||
|
||||
class PushNotification {
|
||||
final int keyId;
|
||||
final List<int> nonce;
|
||||
final List<int> cipherText;
|
||||
final List<int> mac;
|
||||
|
||||
PushNotification({
|
||||
required this.keyId,
|
||||
required this.nonce,
|
||||
required this.cipherText,
|
||||
required this.mac,
|
||||
});
|
||||
|
||||
// Convert a PushNotification instance to a Map
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'keyId': keyId,
|
||||
'nonce': base64Encode(nonce),
|
||||
'cipherText': base64Encode(cipherText),
|
||||
'mac': base64Encode(mac),
|
||||
};
|
||||
}
|
||||
|
||||
// Create a PushNotification instance from a Map
|
||||
factory PushNotification.fromJson(Map<String, dynamic> json) {
|
||||
return PushNotification(
|
||||
keyId: json['keyId'],
|
||||
nonce: base64Decode(json['nonce']),
|
||||
cipherText: base64Decode(json['cipherText']),
|
||||
mac: base64Decode(json['mac']),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// this will trigger a push notification
|
||||
/// push notification only containing the message kind and username
|
||||
Future<List<int>?> getPushData(int toUserId, PushKind kind) async {
|
||||
final Map<int, PushUser> pushKeys = await getPushKeys("sendingPushKeys");
|
||||
|
||||
List<int> key = "InsecureOnlyUsedForAddingContact".codeUnits;
|
||||
int keyId = 0;
|
||||
|
||||
if (pushKeys[toUserId] == null) {
|
||||
// user does not have send any push keys
|
||||
// only allow accept request and contactrequest to be send in an insecure way :/
|
||||
// In future find a better way, e.g. use the signal protocol in a native way..
|
||||
if (kind != PushKind.acceptRequest &&
|
||||
kind != PushKind.contactRequest &&
|
||||
kind != PushKind.testNotification) {
|
||||
// this will be enforced after every app uses this system... :/
|
||||
// return null;
|
||||
Logger("notification_service").shout(
|
||||
"Using insecure key as the receiver does not send a push key!");
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
key = pushKeys[toUserId]!.keys.last.key;
|
||||
keyId = pushKeys[toUserId]!.keys.last.id;
|
||||
} catch (e) {
|
||||
Logger("notification_service")
|
||||
.shout("No push notification key found for user $toUserId");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
final chacha20 = Chacha20.poly1305Aead();
|
||||
final nonce = chacha20.newNonce();
|
||||
final secretBox = await chacha20.encrypt(
|
||||
kind.name.codeUnits,
|
||||
secretKey: SecretKeyData(key),
|
||||
nonce: nonce,
|
||||
);
|
||||
final res = PushNotification(
|
||||
keyId: keyId,
|
||||
nonce: nonce,
|
||||
cipherText: secretBox.cipherText,
|
||||
mac: secretBox.mac.bytes,
|
||||
);
|
||||
|
||||
return jsonEncode(res.toJson()).codeUnits;
|
||||
}
|
||||
|
||||
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) {
|
||||
Logger("notification-service").shout(e);
|
||||
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) {
|
||||
customLocalPushNotification(
|
||||
"Test notification", "This is a test notification.");
|
||||
} else if (fromUserId != null) {
|
||||
showLocalPushNotification(fromUserId, pushKind);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
Logger("notification-service").shout(e);
|
||||
}
|
||||
}
|
||||
|
||||
Future<Map<int, PushUser>> getPushKeys(String storageKey) async {
|
||||
var storage = getSecureStorage();
|
||||
String? pushKeysJson = await storage.read(key: storageKey);
|
||||
Map<int, PushUser> pushKeys = <int, PushUser>{};
|
||||
if (pushKeysJson != null) {
|
||||
Map<String, dynamic> jsonMap = jsonDecode(pushKeysJson);
|
||||
jsonMap.forEach((key, value) {
|
||||
pushKeys[int.parse(key)] = PushUser.fromJson(value);
|
||||
});
|
||||
}
|
||||
print("read: $storageKey: $pushKeys");
|
||||
return pushKeys;
|
||||
}
|
||||
|
||||
Future setPushKeys(String storageKey, Map<int, PushUser> pushKeys) async {
|
||||
var storage = getSecureStorage();
|
||||
Map<String, dynamic> jsonToSend = {};
|
||||
pushKeys.forEach((key, value) {
|
||||
jsonToSend[key.toString()] = value.toJson();
|
||||
});
|
||||
|
||||
String jsonString = jsonEncode(jsonToSend);
|
||||
print("write: $storageKey: $pushKeys");
|
||||
await storage.write(key: storageKey, value: jsonString);
|
||||
}
|
||||
|
||||
/// Streams are created so that app can respond to notification-related events
|
||||
/// since the plugin is initialized in the `main` function
|
||||
|
|
@ -96,102 +432,81 @@ Future<void> setupPushNotification() async {
|
|||
);
|
||||
}
|
||||
|
||||
String getPushNotificationText(String key, String userName) {
|
||||
String systemLanguage = Platform.localeName;
|
||||
Future<String?> getAvatarIcon(Contact user) async {
|
||||
if (user.avatarSvg == null) return null;
|
||||
|
||||
Map<String, String> pushNotificationText;
|
||||
final PictureInfo pictureInfo =
|
||||
await vg.loadPicture(SvgStringLoader(user.avatarSvg!), null);
|
||||
|
||||
if (systemLanguage.contains("de")) {
|
||||
pushNotificationText = {
|
||||
"newTextMessage": "%userName% hat dir eine Nachricht gesendet.",
|
||||
"newTwonly": "%userName% hat dir ein twonly gesendet.",
|
||||
"newVideo": "%userName% hat dir ein Video gesendet.",
|
||||
"newImage": "%userName% hat dir ein Bild gesendet.",
|
||||
"contactRequest": "%userName% möchte sich mir dir vernetzen.",
|
||||
"acceptRequest": "%userName% ist jetzt mit dir vernetzt.",
|
||||
"storedMediaFile": "%userName% hat dein Bild gespeichert."
|
||||
};
|
||||
} else {
|
||||
pushNotificationText = {
|
||||
"newTextMessage": "%userName% has sent you a message.",
|
||||
"newTwonly": "%userName% has sent you a twonly.",
|
||||
"newVideo": "%userName% has sent you a video.",
|
||||
"newImage": "%userName% has sent you an image.",
|
||||
"contactRequest": "%userName% wants to connect with you.",
|
||||
"acceptRequest": "%userName% is now connected with you.",
|
||||
"storedMediaFile": "%userName% has stored your image."
|
||||
};
|
||||
final ui.Image image = await pictureInfo.picture.toImage(300, 300);
|
||||
|
||||
final ByteData? byteData =
|
||||
await image.toByteData(format: ui.ImageByteFormat.png);
|
||||
final Uint8List pngBytes = byteData!.buffer.asUint8List();
|
||||
|
||||
// Get the directory to save the image
|
||||
final directory = await getApplicationDocumentsDirectory();
|
||||
final avatarsDirectory = Directory('${directory.path}/avatars');
|
||||
|
||||
// Create the avatars directory if it does not exist
|
||||
if (!await avatarsDirectory.exists()) {
|
||||
await avatarsDirectory.create(recursive: true);
|
||||
}
|
||||
|
||||
// Replace %userName% with the actual user name
|
||||
return pushNotificationText[key]?.replaceAll("%userName%", userName) ?? "";
|
||||
final filePath = '${avatarsDirectory.path}/${user.userId}.png';
|
||||
final file = File(filePath);
|
||||
await file.writeAsBytes(pngBytes);
|
||||
|
||||
pictureInfo.picture.dispose();
|
||||
|
||||
return filePath;
|
||||
}
|
||||
|
||||
Future localPushNotificationNewMessage(
|
||||
int fromUserId, my.MessageJson message, int messageId) async {
|
||||
Future showLocalPushNotification(
|
||||
int fromUserId,
|
||||
PushKind pushKind,
|
||||
) async {
|
||||
String? title;
|
||||
String? body;
|
||||
|
||||
Contact? user = await twonlyDatabase.contactsDao
|
||||
.getContactByUserId(fromUserId)
|
||||
.getSingleOrNull();
|
||||
|
||||
if (user == null) return;
|
||||
|
||||
String msg = "";
|
||||
|
||||
final content = message.content;
|
||||
|
||||
if (content is my.TextMessageContent) {
|
||||
msg =
|
||||
getPushNotificationText("newTextMessage", getContactDisplayName(user));
|
||||
} else if (content is my.MediaMessageContent) {
|
||||
if (content.isRealTwonly) {
|
||||
msg = getPushNotificationText("newTwonly", getContactDisplayName(user));
|
||||
} else if (content.isVideo) {
|
||||
msg = getPushNotificationText("newVideo", getContactDisplayName(user));
|
||||
} else {
|
||||
msg = getPushNotificationText("newImage", getContactDisplayName(user));
|
||||
}
|
||||
}
|
||||
|
||||
if (message.kind == MessageKind.contactRequest) {
|
||||
msg =
|
||||
getPushNotificationText("contactRequest", getContactDisplayName(user));
|
||||
}
|
||||
|
||||
if (message.kind == MessageKind.acceptRequest) {
|
||||
msg = getPushNotificationText("acceptRequest", getContactDisplayName(user));
|
||||
}
|
||||
|
||||
if (message.kind == MessageKind.storedMediaFile) {
|
||||
msg =
|
||||
getPushNotificationText("storedMediaFile", getContactDisplayName(user));
|
||||
}
|
||||
|
||||
if (msg == "") {
|
||||
title = getContactDisplayName(user);
|
||||
body = getPushNotificationText(pushKind);
|
||||
if (body == "") {
|
||||
Logger("localPushNotificationNewMessage")
|
||||
.shout("No push notification type defined!");
|
||||
}
|
||||
|
||||
const AndroidNotificationDetails androidNotificationDetails =
|
||||
AndroidNotificationDetails(
|
||||
'0',
|
||||
'Messages',
|
||||
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();
|
||||
const NotificationDetails notificationDetails = NotificationDetails(
|
||||
NotificationDetails notificationDetails = NotificationDetails(
|
||||
android: androidNotificationDetails, iOS: darwinNotificationDetails);
|
||||
|
||||
await flutterLocalNotificationsPlugin.show(
|
||||
messageId,
|
||||
getContactDisplayName(user),
|
||||
msg,
|
||||
fromUserId,
|
||||
title,
|
||||
body,
|
||||
notificationDetails,
|
||||
payload: message.kind.index.toString(),
|
||||
payload: pushKind.name,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -199,8 +514,8 @@ Future customLocalPushNotification(String title, String msg) async {
|
|||
const AndroidNotificationDetails androidNotificationDetails =
|
||||
AndroidNotificationDetails(
|
||||
'1',
|
||||
'Error',
|
||||
channelDescription: 'Error messages.',
|
||||
'System',
|
||||
channelDescription: 'System messages.',
|
||||
importance: Importance.max,
|
||||
priority: Priority.max,
|
||||
);
|
||||
|
|
@ -211,9 +526,40 @@ Future customLocalPushNotification(String title, String msg) async {
|
|||
android: androidNotificationDetails, iOS: darwinNotificationDetails);
|
||||
|
||||
await flutterLocalNotificationsPlugin.show(
|
||||
897898,
|
||||
999999 + Random.secure().nextInt(9999),
|
||||
title,
|
||||
msg,
|
||||
notificationDetails,
|
||||
);
|
||||
}
|
||||
|
||||
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 mir 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."
|
||||
};
|
||||
} 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."
|
||||
};
|
||||
}
|
||||
return pushNotificationText[pushKind.name] ?? "";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -280,6 +280,7 @@ class _ChatItemDetailsViewState extends State<ChatItemDetailsView> {
|
|||
TextMessageContent(
|
||||
text: newMessageController.text,
|
||||
),
|
||||
PushKind.text,
|
||||
);
|
||||
newMessageController.clear();
|
||||
currentInputText = "";
|
||||
|
|
|
|||
|
|
@ -323,6 +323,7 @@ class _MediaViewerViewState extends State<MediaViewerView> {
|
|||
responseToMessageId:
|
||||
allMediaFiles.first.messageOtherId,
|
||||
),
|
||||
PushKind.reaction,
|
||||
);
|
||||
setState(() {
|
||||
selectedShortReaction = index;
|
||||
|
|
@ -393,6 +394,7 @@ class _MediaViewerViewState extends State<MediaViewerView> {
|
|||
),
|
||||
timestamp: DateTime.now(),
|
||||
),
|
||||
pushKind: PushKind.storedMediaFile,
|
||||
);
|
||||
final res = await saveImageToGallery(imageBytes!);
|
||||
if (res == null) {
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import 'package:twonly/src/components/alert_dialog.dart';
|
|||
import 'package:twonly/src/database/daos/contacts_dao.dart';
|
||||
import 'package:twonly/src/database/tables/messages_table.dart';
|
||||
import 'package:twonly/src/database/twonly_database.dart';
|
||||
import 'package:twonly/src/services/notification_service.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
import 'package:twonly/globals.dart';
|
||||
import 'package:twonly/src/components/headline.dart';
|
||||
|
|
@ -67,6 +68,7 @@ class _SearchUsernameView extends State<SearchUsernameView> {
|
|||
timestamp: DateTime.now(),
|
||||
content: MessageContent(),
|
||||
),
|
||||
pushKind: PushKind.contactRequest,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -238,6 +240,7 @@ class _ContactsListViewState extends State<ContactsListView> {
|
|||
timestamp: DateTime.now(),
|
||||
content: MessageContent(),
|
||||
),
|
||||
pushKind: PushKind.acceptRequest,
|
||||
);
|
||||
notifyContactsAboutProfileChange();
|
||||
},
|
||||
|
|
|
|||
64
lib/src/views/settings/notification_view.dart
Normal file
64
lib/src/views/settings/notification_view.dart
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:twonly/globals.dart';
|
||||
import 'package:twonly/src/components/alert_dialog.dart';
|
||||
import 'package:twonly/src/services/fcm_service.dart';
|
||||
import 'package:twonly/src/services/notification_service.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
import 'package:twonly/src/utils/storage.dart';
|
||||
|
||||
class NotificationView extends StatelessWidget {
|
||||
const NotificationView({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(context.lang.settingsNotification),
|
||||
),
|
||||
body: ListView(
|
||||
children: [
|
||||
ListTile(
|
||||
title: Text(context.lang.settingsNotifyTroubleshooting),
|
||||
subtitle: Text(context.lang.settingsNotifyTroubleshootingDesc),
|
||||
onTap: () async {
|
||||
await initFCMAfterAuthenticated();
|
||||
final storage = getSecureStorage();
|
||||
String? storedToken = await storage.read(key: "google_fcm");
|
||||
//await setupNotificationWithUsers(force: true);
|
||||
if (!context.mounted) return;
|
||||
|
||||
if (storedToken == null) {
|
||||
final platform = Platform.isAndroid ? "Google's" : "Apple's";
|
||||
showAlertDialog(context, "Problem detected",
|
||||
"twonly is not able to register your app to $platform push server infrastrukture. For Android that can happen when you do not have the Google Play Services installed. If you theses installed and want to help us to fix the issue please send us your debug log in Settings > Help > Debug log.");
|
||||
} else {
|
||||
final run = await showAlertDialog(
|
||||
context,
|
||||
context.lang.settingsNotifyTroubleshootingNoProblem,
|
||||
context.lang.settingsNotifyTroubleshootingNoProblemDesc);
|
||||
|
||||
if (run) {
|
||||
final user = await getUser();
|
||||
if (user != null) {
|
||||
final pushData = await getPushData(
|
||||
user.userId,
|
||||
PushKind.testNotification,
|
||||
);
|
||||
await apiProvider.sendTextMessage(
|
||||
user.userId,
|
||||
Uint8List(0),
|
||||
pushData,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -7,6 +7,7 @@ import 'package:twonly/src/utils/misc.dart';
|
|||
import 'package:twonly/src/utils/storage.dart';
|
||||
import 'package:twonly/src/views/settings/account_view.dart';
|
||||
import 'package:twonly/src/views/settings/appearance_view.dart';
|
||||
import 'package:twonly/src/views/settings/notification_view.dart';
|
||||
import 'package:twonly/src/views/settings/profile_view.dart';
|
||||
import 'package:twonly/src/views/settings/help_view.dart';
|
||||
import 'package:twonly/src/views/settings/privacy_view.dart';
|
||||
|
|
@ -129,11 +130,16 @@ class _SettingsMainViewState extends State<SettingsMainView> {
|
|||
}));
|
||||
},
|
||||
),
|
||||
// BetterListTile(
|
||||
// icon: FontAwesomeIcons.bell,
|
||||
// text: context.lang.settingsNotification,
|
||||
// onTap: () async {},
|
||||
// ),
|
||||
BetterListTile(
|
||||
icon: FontAwesomeIcons.bell,
|
||||
text: context.lang.settingsNotification,
|
||||
onTap: () async {
|
||||
Navigator.push(context,
|
||||
MaterialPageRoute(builder: (context) {
|
||||
return NotificationView();
|
||||
}));
|
||||
},
|
||||
),
|
||||
const Divider(),
|
||||
BetterListTile(
|
||||
icon: FontAwesomeIcons.circleQuestion,
|
||||
|
|
|
|||
Loading…
Reference in a new issue