diff --git a/generate_proto.sh b/generate_proto.sh index dd2ef76..d3101f6 100755 --- a/generate_proto.sh +++ b/generate_proto.sh @@ -4,6 +4,8 @@ protoc --proto_path="./lib/src/model/protobuf/backup/" --dart_out="./lib/src/model/protobuf/backup/" "backup.proto" +protoc --proto_path="./lib/src/model/protobuf/push_notification/" --dart_out="./lib/src/model/protobuf/push_notification/" "push_notification.proto" +protoc --proto_path="./lib/src/model/protobuf/push_notification/" --swift_out="./ios/NotificationService/" "push_notification.proto" SRC_DIR="../twonly-server/twonly/src/" diff --git a/ios/NotificationService/NotificationService.swift b/ios/NotificationService/NotificationService.swift index e701472..62c20f2 100644 --- a/ios/NotificationService/NotificationService.swift +++ b/ios/NotificationService/NotificationService.swift @@ -5,419 +5,417 @@ // Created by Tobi on 03.04.25. // -import UserNotifications -import CryptoKit -import Foundation +// import UserNotifications +// import CryptoKit +// import Foundation -class NotificationService: UNNotificationServiceExtension { - - var contentHandler: ((UNNotificationContent) -> Void)? - var bestAttemptContent: UNMutableNotificationContent? - - override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) { - self.contentHandler = contentHandler - bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent) - - if let bestAttemptContent = bestAttemptContent { - - guard let _ = bestAttemptContent.userInfo as? [String: Any], - let push_data = bestAttemptContent.userInfo["push_data"] as? String else { - return contentHandler(bestAttemptContent); - } - - let data = getPushNotificationData(pushDataJson: push_data) - - if data != nil { - if data!.title == "blocked" { - NSLog("Block message because user is blocked!") - // https://developer.apple.com/documentation/bundleresources/entitlements/com.apple.developer.usernotifications.filtering - return contentHandler(UNNotificationContent()) - } - bestAttemptContent.title = data!.title; - bestAttemptContent.body = data!.body; - bestAttemptContent.threadIdentifier = String(format: "%d", data!.notificationId) - } else { - bestAttemptContent.title = "\(bestAttemptContent.title)" - } - - contentHandler(bestAttemptContent) - } - } - - override func serviceExtensionTimeWillExpire() { - // Called just before the extension will be terminated by the system. - // Use this as an opportunity to deliver your "best attempt" at modified content, otherwise the original push payload will be used. - if let contentHandler = contentHandler, let bestAttemptContent = bestAttemptContent { - contentHandler(bestAttemptContent) - } - } - -} +// class NotificationService: UNNotificationServiceExtension { +// var contentHandler: ((UNNotificationContent) -> Void)? +// var bestAttemptContent: UNMutableNotificationContent? -enum PushKind: String, Codable { - case text - case twonly - case video - case image - case contactRequest - case acceptRequest - case storedMediaFile - case reaction - case testNotification - case reopenedMedia - case reactionToVideo - case reactionToText - case reactionToImage - case response -} +// override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) { +// self.contentHandler = contentHandler +// bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent) -import CryptoKit -import Foundation -import Security +// if let bestAttemptContent = bestAttemptContent { -func getPushNotificationData(pushDataJson: String) -> (title: String, body: String, notificationId: Int)? { - // Decode the pushDataJson - guard let pushData = decodePushData(pushDataJson) else { - NSLog("Failed to decode push data") - return nil - } +// guard let _ = bestAttemptContent.userInfo as? [String: Any], +// let push_data = bestAttemptContent.userInfo["push_data"] as? String else { +// return contentHandler(bestAttemptContent); +// } - var pushKind: PushKind? - var displayName: String? - var fromUserId: Int? - var blocked: Bool? +// let data = getPushNotificationData(pushDataJson: push_data) - // Check the keyId - if pushData.keyId == 0 { - let key = "InsecureOnlyUsedForAddingContact".data(using: .utf8)!.map { Int($0) } - pushKind = tryDecryptMessage(key: key, pushData: pushData) - } else { - let pushKeys = getPushKey() - if pushKeys != nil { - for (userId, userKeys) in pushKeys! { - for key in userKeys.keys { - if key.id == pushData.keyId { - pushKind = tryDecryptMessage(key: key.key, pushData: pushData) - if pushKind != nil { - displayName = userKeys.displayName - fromUserId = userId - blocked = userKeys.blocked - break - } - } - } - // Found correct key and user - if displayName != nil { break } - } - } else { - NSLog("pushKeys are empty") - } - } - - if blocked == true { - return ("blocked", "blocked", 0) - } +// if data != nil { +// if data!.title == "blocked" { +// NSLog("Block message because user is blocked!") +// // https://developer.apple.com/documentation/bundleresources/entitlements/com.apple.developer.usernotifications.filtering +// return contentHandler(UNNotificationContent()) +// } +// bestAttemptContent.title = data!.title; +// bestAttemptContent.body = data!.body; +// bestAttemptContent.threadIdentifier = String(format: "%d", data!.notificationId) +// } else { +// bestAttemptContent.title = "\(bestAttemptContent.title)" +// } - // Handle the push notification based on the pushKind - if let pushKind = pushKind { - - if pushKind == .testNotification { - return ("Test Notification", "This is a test notification.", 0) - } else if displayName != nil && fromUserId != nil { - return (displayName!, getPushNotificationText(pushKind: pushKind), fromUserId!) - } else { - return ("", getPushNotificationTextWithoutUserId(pushKind: pushKind), 1) - } +// contentHandler(bestAttemptContent) +// } +// } - } else { - NSLog("Failed to decrypt message or pushKind is nil") - } - return nil -} +// override func serviceExtensionTimeWillExpire() { +// // Called just before the extension will be terminated by the system. +// // Use this as an opportunity to deliver your "best attempt" at modified content, otherwise the original push payload will be used. +// if let contentHandler = contentHandler, let bestAttemptContent = bestAttemptContent { +// contentHandler(bestAttemptContent) +// } +// } -func tryDecryptMessage(key: [Int], pushData: PushNotification) -> PushKind? { - // Convert the key from [Int] to Data - let keyData = Data(key.map { UInt8($0) }) // Convert Int to UInt8 - - guard let nonceData = Data(base64Encoded: pushData.nonce), - let cipherTextData = Data(base64Encoded: pushData.cipherText), - let macData = Data(base64Encoded: pushData.mac) else { - NSLog("Failed to decode base64 strings") - return nil - } +// } - do { - // Create a nonce for ChaChaPoly - let nonce = try ChaChaPoly.Nonce(data: nonceData) - - // Create a sealed box for ChaChaPoly - let sealedBox = try ChaChaPoly.SealedBox(nonce: nonce, ciphertext: cipherTextData, tag: macData) - - // Decrypt the data using the key - let decryptedData = try ChaChaPoly.open(sealedBox, using: SymmetricKey(data: keyData)) - - // Convert decrypted data to a string - if let decryptedMessage = String(data: decryptedData, encoding: .utf8) { - NSLog("Decrypted message: \(decryptedMessage)") - - // Here you can determine the PushKind based on the decrypted message - return determinePushKind(from: decryptedMessage) - } - } catch { - NSLog("Decryption failed: \(error)") - } - - return nil -} +// enum PushKind: String, Codable { +// case text +// case twonly +// case video +// case image +// case contactRequest +// case acceptRequest +// case storedMediaFile +// case reaction +// case testNotification +// case reopenedMedia +// case reactionToVideo +// case reactionToText +// case reactionToImage +// case response +// } -// Placeholder function to determine PushKind from the decrypted message -func determinePushKind(from message: String) -> PushKind? { - // Implement your logic to determine the PushKind based on the message content - // For example, you might check for specific keywords or formats in the message - // This is just a placeholder implementation - if message.contains("text") { - return .text - } else if message.contains("video") { - return .video - } else if message.contains("image") { - return .image - } else if message.contains("twonly") { - return .twonly - } else if message.contains("contactRequest") { - return .contactRequest - } else if message.contains("acceptRequest") { - return .acceptRequest - } else if message.contains("storedMediaFile") { - return .storedMediaFile - } else if message.contains("reaction") { - return .reaction - } else if message.contains("testNotification") { - return .testNotification - } else if message.contains("reopenedMedia") { - return .reopenedMedia - } else if message.contains("reactionToVideo") { - return .reactionToVideo - } else if message.contains("reactionToText") { - return .reactionToText - } else if message.contains("reactionToImage") { - return .reactionToImage - } else if message.contains("response") { - return .response - } else { - return nil // Unknown PushKind - } -} +// import CryptoKit +// import Foundation +// import Security +// func getPushNotificationData(pushDataJson: String) -> (title: String, body: String, notificationId: Int)? { +// // Decode the pushDataJson +// guard let pushData = decodePushData(pushDataJson) else { +// NSLog("Failed to decode push data") +// return nil +// } -func decodePushData(_ json: String) -> PushNotification? { - // First, decode the base64 string - guard let base64Data = Data(base64Encoded: json) else { - NSLog("Failed to decode base64 string") - return nil - } - - // Convert the base64 decoded data to a JSON string - guard let jsonString = String(data: base64Data, encoding: .utf8) else { - NSLog("Failed to convert base64 data to JSON string") - return nil - } - - // Convert the JSON string to Data - guard let jsonData = jsonString.data(using: .utf8) else { - NSLog("Failed to convert JSON string to Data") - return nil - } - - do { - // Use JSONDecoder to decode the JSON data into a PushNotification instance - let decoder = JSONDecoder() - let pushNotification = try decoder.decode(PushNotification.self, from: jsonData) - return pushNotification - } catch { - NSLog("Error decoding JSON: \(error)") - return nil - } -} +// var pushKind: PushKind? +// var displayName: String? +// var fromUserId: Int? +// var blocked: Bool? -struct PushNotification: Codable { - let keyId: Int - let nonce: String - let cipherText: String - let mac: String +// // Check the keyId +// if pushData.keyId == 0 { +// let key = "InsecureOnlyUsedForAddingContact".data(using: .utf8)!.map { Int($0) } +// pushKind = tryDecryptMessage(key: key, pushData: pushData) +// } else { +// let pushKeys = getPushKey() +// if pushKeys != nil { +// for (userId, userKeys) in pushKeys! { +// for key in userKeys.keys { +// if key.id == pushData.keyId { +// pushKind = tryDecryptMessage(key: key.key, pushData: pushData) +// if pushKind != nil { +// displayName = userKeys.displayName +// fromUserId = userId +// blocked = userKeys.blocked +// break +// } +// } +// } +// // Found correct key and user +// if displayName != nil { break } +// } +// } else { +// NSLog("pushKeys are empty") +// } +// } - // You can add custom coding keys if the JSON keys differ from the property names - enum CodingKeys: String, CodingKey { - case keyId - case nonce - case cipherText - case mac - } -} +// if blocked == true { +// return ("blocked", "blocked", 0) +// } -struct PushKeyMeta: Codable { - let id: Int - let key: [Int] - let createdAt: Date - - enum CodingKeys: String, CodingKey { - case id - case key - case createdAt - } -} +// // Handle the push notification based on the pushKind +// if let pushKind = pushKind { -struct PushUser: Codable { - let displayName: String - let keys: [PushKeyMeta] - let blocked: Bool? - - enum CodingKeys: String, CodingKey { - case displayName - case keys - case blocked - } -} +// if pushKind == .testNotification { +// return ("Test Notification", "This is a test notification.", 0) +// } else if displayName != nil && fromUserId != nil { +// return (displayName!, getPushNotificationText(pushKind: pushKind), fromUserId!) +// } else { +// return ("", getPushNotificationTextWithoutUserId(pushKind: pushKind), 1) +// } -func getPushKey() -> [Int: PushUser]? { - // Retrieve the data from secure storage (Keychain) - guard let data = readFromKeychain(key: "receivingPushKeys") else { - NSLog("No data found for key: receivingPushKeys") - return nil - } - - do { - // Decode the JSON data into a dictionary - let jsonData = data.data(using: .utf8)! - let jsonMap = try JSONSerialization.jsonObject(with: jsonData, options: []) as? [String: Any] - - var pushKeys: [Int: PushUser] = [:] - - // Iterate through the JSON map and decode each PushUser - for (key, value) in jsonMap ?? [:] { - if let userData = try? JSONSerialization.data(withJSONObject: value, options: []), - let pushUser = try? JSONDecoder().decode(PushUser.self, from: userData) { - pushKeys[Int(key)!] = pushUser - } - } - - return pushKeys - } catch { - NSLog("Error decoding JSON: \(error)") - return nil - } -} +// } else { +// NSLog("Failed to decrypt message or pushKind is nil") +// } +// return nil +// } -// Helper function to read from Keychain -func readFromKeychain(key: String) -> String? { - let query: [String: Any] = [ - kSecClass as String: kSecClassGenericPassword, - kSecAttrAccount as String: key, - kSecReturnData as String: kCFBooleanTrue!, - kSecMatchLimit as String: kSecMatchLimitOne, - kSecAttrAccessGroup as String: "CN332ZUGRP.eu.twonly.shared" // Use your access group - ] - - var dataTypeRef: AnyObject? = nil - let status: OSStatus = SecItemCopyMatching(query as CFDictionary, &dataTypeRef) - - if status == errSecSuccess { - if let data = dataTypeRef as? Data { - return String(data: data, encoding: .utf8) - } - } - - return nil -} +// func tryDecryptMessage(key: [Int], pushData: PushNotification) -> PushKind? { +// // Convert the key from [Int] to Data +// let keyData = Data(key.map { UInt8($0) }) // Convert Int to UInt8 -func getPushNotificationText(pushKind: PushKind) -> String { - let systemLanguage = Locale.current.languageCode ?? "en" // Get the current system language +// guard let nonceData = Data(base64Encoded: pushData.nonce), +// let cipherTextData = Data(base64Encoded: pushData.cipherText), +// let macData = Data(base64Encoded: pushData.mac) else { +// NSLog("Failed to decode base64 strings") +// return nil +// } - var pushNotificationText: [PushKind: String] = [:] +// do { +// // Create a nonce for ChaChaPoly +// let nonce = try ChaChaPoly.Nonce(data: nonceData) - // Define the messages based on the system language - if systemLanguage.contains("de") { // German - pushNotificationText = [ - .text: "hat dir eine Nachricht gesendet.", - .twonly: "hat dir ein twonly gesendet.", - .video: "hat dir ein Video gesendet.", - .image: "hat dir ein Bild gesendet.", - .contactRequest: "möchte sich mit dir vernetzen.", - .acceptRequest: "ist jetzt mit dir vernetzt.", - .storedMediaFile: "hat dein Bild gespeichert.", - .reaction: "hat auf dein Bild reagiert.", - .testNotification: "Das ist eine Testbenachrichtigung.", - .reopenedMedia: "hat dein Bild erneut geöffnet.", - .reactionToVideo: "hat auf dein Video reagiert.", - .reactionToText: "hat auf deinen Text reagiert.", - .reactionToImage: "hat auf dein Bild reagiert.", - .response: "hat dir geantwortet." - ] - } else { // Default to English - pushNotificationText = [ - .text: "has sent you a message.", - .twonly: "has sent you a twonly.", - .video: "has sent you a video.", - .image: "has sent you an image.", - .contactRequest: "wants to connect with you.", - .acceptRequest: "is now connected with you.", - .storedMediaFile: "has stored your image.", - .reaction: "has reacted to your image.", - .testNotification: "This is a test notification.", - .reopenedMedia: "has reopened your image.", - .reactionToVideo: "has reacted to your video.", - .reactionToText: "has reacted to your text.", - .reactionToImage: "has reacted to your image.", - .response: "has responded." - ] - } +// // Create a sealed box for ChaChaPoly +// let sealedBox = try ChaChaPoly.SealedBox(nonce: nonce, ciphertext: cipherTextData, tag: macData) - // Return the corresponding message or an empty string if not found - return pushNotificationText[pushKind] ?? "" -} +// // Decrypt the data using the key +// let decryptedData = try ChaChaPoly.open(sealedBox, using: SymmetricKey(data: keyData)) -func getPushNotificationTextWithoutUserId(pushKind: PushKind) -> String { - let systemLanguage = Locale.current.languageCode ?? "en" // Get the current system language +// // Convert decrypted data to a string +// if let decryptedMessage = String(data: decryptedData, encoding: .utf8) { +// NSLog("Decrypted message: \(decryptedMessage)") - var pushNotificationText: [PushKind: String] = [:] +// // Here you can determine the PushKind based on the decrypted message +// return determinePushKind(from: decryptedMessage) +// } +// } catch { +// NSLog("Decryption failed: \(error)") +// } - // Define the messages based on the system language - if systemLanguage.contains("de") { // German - pushNotificationText = [ - .text: "Du hast eine Nachricht erhalten.", - .twonly: "Du hast ein twonly erhalten.", - .video: "Du hast ein Video erhalten.", - .image: "Du hast ein Bild erhalten.", - .contactRequest: "Du hast eine Kontaktanfrage erhalten.", - .acceptRequest: "Deine Kontaktanfrage wurde angenommen.", - .storedMediaFile: "Dein Bild wurde gespeichert.", - .reaction: "Du hast eine Reaktion auf dein Bild erhalten.", - .testNotification: "Das ist eine Testbenachrichtigung.", - .reopenedMedia: "hat dein Bild erneut geöffnet.", - .reactionToVideo: "Du hast eine Reaktion auf dein Video erhalten.", - .reactionToText: "Du hast eine Reaktion auf deinen Text erhalten.", - .reactionToImage: "Du hast eine Reaktion auf dein Bild erhalten.", - .response: "Du hast eine Antwort erhalten." - ] - } else { // Default to English - pushNotificationText = [ - .text: "You got a message.", - .twonly: "You got a twonly.", - .video: "You got a video.", - .image: "You got an image.", - .contactRequest: "You got a contact request.", - .acceptRequest: "Your contact request has been accepted.", - .storedMediaFile: "Your image has been saved.", - .reaction: "You got a reaction to your image.", - .testNotification: "This is a test notification.", - .reopenedMedia: "has reopened your image.", - .reactionToVideo: "You got a reaction to your video.", - .reactionToText: "You got a reaction to your text.", - .reactionToImage: "You got a reaction to your image.", - .response: "You got a response." - ] - } +// return nil +// } - // Return the corresponding message or an empty string if not found - return pushNotificationText[pushKind] ?? "" -} +// // Placeholder function to determine PushKind from the decrypted message +// func determinePushKind(from message: String) -> PushKind? { +// // Implement your logic to determine the PushKind based on the message content +// // For example, you might check for specific keywords or formats in the message +// // This is just a placeholder implementation +// if message.contains("text") { +// return .text +// } else if message.contains("video") { +// return .video +// } else if message.contains("image") { +// return .image +// } else if message.contains("twonly") { +// return .twonly +// } else if message.contains("contactRequest") { +// return .contactRequest +// } else if message.contains("acceptRequest") { +// return .acceptRequest +// } else if message.contains("storedMediaFile") { +// return .storedMediaFile +// } else if message.contains("reaction") { +// return .reaction +// } else if message.contains("testNotification") { +// return .testNotification +// } else if message.contains("reopenedMedia") { +// return .reopenedMedia +// } else if message.contains("reactionToVideo") { +// return .reactionToVideo +// } else if message.contains("reactionToText") { +// return .reactionToText +// } else if message.contains("reactionToImage") { +// return .reactionToImage +// } else if message.contains("response") { +// return .response +// } else { +// return nil // Unknown PushKind +// } +// } + +// func decodePushData(_ json: String) -> PushNotification? { +// // First, decode the base64 string +// guard let base64Data = Data(base64Encoded: json) else { +// NSLog("Failed to decode base64 string") +// return nil +// } + +// // Convert the base64 decoded data to a JSON string +// guard let jsonString = String(data: base64Data, encoding: .utf8) else { +// NSLog("Failed to convert base64 data to JSON string") +// return nil +// } + +// // Convert the JSON string to Data +// guard let jsonData = jsonString.data(using: .utf8) else { +// NSLog("Failed to convert JSON string to Data") +// return nil +// } + +// do { +// // Use JSONDecoder to decode the JSON data into a PushNotification instance +// let decoder = JSONDecoder() +// let pushNotification = try decoder.decode(PushNotification.self, from: jsonData) +// return pushNotification +// } catch { +// NSLog("Error decoding JSON: \(error)") +// return nil +// } +// } + +// struct PushNotification: Codable { +// let keyId: Int +// let nonce: String +// let cipherText: String +// let mac: String + +// // You can add custom coding keys if the JSON keys differ from the property names +// enum CodingKeys: String, CodingKey { +// case keyId +// case nonce +// case cipherText +// case mac +// } +// } + +// struct PushKeyMeta: Codable { +// let id: Int +// let key: [Int] +// let createdAt: Date + +// enum CodingKeys: String, CodingKey { +// case id +// case key +// case createdAt +// } +// } + +// struct PushUser: Codable { +// let displayName: String +// let keys: [PushKeyMeta] +// let blocked: Bool? + +// enum CodingKeys: String, CodingKey { +// case displayName +// case keys +// case blocked +// } +// } + +// func getPushKey() -> [Int: PushUser]? { +// // Retrieve the data from secure storage (Keychain) +// guard let data = readFromKeychain(key: "receivingPushKeys") else { +// NSLog("No data found for key: receivingPushKeys") +// return nil +// } + +// do { +// // Decode the JSON data into a dictionary +// let jsonData = data.data(using: .utf8)! +// let jsonMap = try JSONSerialization.jsonObject(with: jsonData, options: []) as? [String: Any] + +// var pushKeys: [Int: PushUser] = [:] + +// // Iterate through the JSON map and decode each PushUser +// for (key, value) in jsonMap ?? [:] { +// if let userData = try? JSONSerialization.data(withJSONObject: value, options: []), +// let pushUser = try? JSONDecoder().decode(PushUser.self, from: userData) { +// pushKeys[Int(key)!] = pushUser +// } +// } + +// return pushKeys +// } catch { +// NSLog("Error decoding JSON: \(error)") +// return nil +// } +// } + +// // Helper function to read from Keychain +// func readFromKeychain(key: String) -> String? { +// let query: [String: Any] = [ +// kSecClass as String: kSecClassGenericPassword, +// kSecAttrAccount as String: key, +// kSecReturnData as String: kCFBooleanTrue!, +// kSecMatchLimit as String: kSecMatchLimitOne, +// kSecAttrAccessGroup as String: "CN332ZUGRP.eu.twonly.shared" // Use your access group +// ] + +// var dataTypeRef: AnyObject? = nil +// let status: OSStatus = SecItemCopyMatching(query as CFDictionary, &dataTypeRef) + +// if status == errSecSuccess { +// if let data = dataTypeRef as? Data { +// return String(data: data, encoding: .utf8) +// } +// } + +// return nil +// } + +// func getPushNotificationText(pushKind: PushKind) -> String { +// let systemLanguage = Locale.current.languageCode ?? "en" // Get the current system language + +// var pushNotificationText: [PushKind: String] = [:] + +// // Define the messages based on the system language +// if systemLanguage.contains("de") { // German +// pushNotificationText = [ +// .text: "hat dir eine Nachricht gesendet.", +// .twonly: "hat dir ein twonly gesendet.", +// .video: "hat dir ein Video gesendet.", +// .image: "hat dir ein Bild gesendet.", +// .contactRequest: "möchte sich mit dir vernetzen.", +// .acceptRequest: "ist jetzt mit dir vernetzt.", +// .storedMediaFile: "hat dein Bild gespeichert.", +// .reaction: "hat auf dein Bild reagiert.", +// .testNotification: "Das ist eine Testbenachrichtigung.", +// .reopenedMedia: "hat dein Bild erneut geöffnet.", +// .reactionToVideo: "hat auf dein Video reagiert.", +// .reactionToText: "hat auf deinen Text reagiert.", +// .reactionToImage: "hat auf dein Bild reagiert.", +// .response: "hat dir geantwortet." +// ] +// } else { // Default to English +// pushNotificationText = [ +// .text: "has sent you a message.", +// .twonly: "has sent you a twonly.", +// .video: "has sent you a video.", +// .image: "has sent you an image.", +// .contactRequest: "wants to connect with you.", +// .acceptRequest: "is now connected with you.", +// .storedMediaFile: "has stored your image.", +// .reaction: "has reacted to your image.", +// .testNotification: "This is a test notification.", +// .reopenedMedia: "has reopened your image.", +// .reactionToVideo: "has reacted to your video.", +// .reactionToText: "has reacted to your text.", +// .reactionToImage: "has reacted to your image.", +// .response: "has responded." +// ] +// } + +// // Return the corresponding message or an empty string if not found +// return pushNotificationText[pushKind] ?? "" +// } + +// func getPushNotificationTextWithoutUserId(pushKind: PushKind) -> String { +// let systemLanguage = Locale.current.languageCode ?? "en" // Get the current system language + +// var pushNotificationText: [PushKind: String] = [:] + +// // Define the messages based on the system language +// if systemLanguage.contains("de") { // German +// pushNotificationText = [ +// .text: "Du hast eine Nachricht erhalten.", +// .twonly: "Du hast ein twonly erhalten.", +// .video: "Du hast ein Video erhalten.", +// .image: "Du hast ein Bild erhalten.", +// .contactRequest: "Du hast eine Kontaktanfrage erhalten.", +// .acceptRequest: "Deine Kontaktanfrage wurde angenommen.", +// .storedMediaFile: "Dein Bild wurde gespeichert.", +// .reaction: "Du hast eine Reaktion auf dein Bild erhalten.", +// .testNotification: "Das ist eine Testbenachrichtigung.", +// .reopenedMedia: "hat dein Bild erneut geöffnet.", +// .reactionToVideo: "Du hast eine Reaktion auf dein Video erhalten.", +// .reactionToText: "Du hast eine Reaktion auf deinen Text erhalten.", +// .reactionToImage: "Du hast eine Reaktion auf dein Bild erhalten.", +// .response: "Du hast eine Antwort erhalten." +// ] +// } else { // Default to English +// pushNotificationText = [ +// .text: "You got a message.", +// .twonly: "You got a twonly.", +// .video: "You got a video.", +// .image: "You got an image.", +// .contactRequest: "You got a contact request.", +// .acceptRequest: "Your contact request has been accepted.", +// .storedMediaFile: "Your image has been saved.", +// .reaction: "You got a reaction to your image.", +// .testNotification: "This is a test notification.", +// .reopenedMedia: "has reopened your image.", +// .reactionToVideo: "You got a reaction to your video.", +// .reactionToText: "You got a reaction to your text.", +// .reactionToImage: "You got a reaction to your image.", +// .response: "You got a response." +// ] +// } + +// // Return the corresponding message or an empty string if not found +// return pushNotificationText[pushKind] ?? "" +// } diff --git a/ios/NotificationService/push_notification.pb.swift b/ios/NotificationService/push_notification.pb.swift new file mode 100644 index 0000000..be89a9d --- /dev/null +++ b/ios/NotificationService/push_notification.pb.swift @@ -0,0 +1,467 @@ +// DO NOT EDIT. +// swift-format-ignore-file +// swiftlint:disable all +// +// Generated by the Swift generator plugin for the protocol buffer compiler. +// Source: push_notification.proto +// +// For information on using the generated types, please see the documentation: +// https://github.com/apple/swift-protobuf/ + +import Foundation +import SwiftProtobuf + +// If the compiler emits an error on this type, it is because this file +// was generated by a version of the `protoc` Swift plug-in that is +// incompatible with the version of SwiftProtobuf to which you are linking. +// Please ensure that you are building against the same version of the API +// that was used to generate this file. +fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAPIVersionCheck { + struct _2: SwiftProtobuf.ProtobufAPIVersion_2 {} + typealias Version = _2 +} + +enum PushKind: SwiftProtobuf.Enum, Swift.CaseIterable { + typealias RawValue = Int + case reaction // = 0 + case response // = 1 + case text // = 2 + case video // = 3 + case twonly // = 4 + case image // = 5 + case contactRequest // = 6 + case acceptRequest // = 7 + case storedMediaFile // = 8 + case testNotification // = 9 + case reopenedMedia // = 10 + case reactionToVideo // = 11 + case reactionToText // = 12 + case reactionToImage // = 13 + case UNRECOGNIZED(Int) + + init() { + self = .reaction + } + + init?(rawValue: Int) { + switch rawValue { + case 0: self = .reaction + case 1: self = .response + case 2: self = .text + case 3: self = .video + case 4: self = .twonly + case 5: self = .image + case 6: self = .contactRequest + case 7: self = .acceptRequest + case 8: self = .storedMediaFile + case 9: self = .testNotification + case 10: self = .reopenedMedia + case 11: self = .reactionToVideo + case 12: self = .reactionToText + case 13: self = .reactionToImage + default: self = .UNRECOGNIZED(rawValue) + } + } + + var rawValue: Int { + switch self { + case .reaction: return 0 + case .response: return 1 + case .text: return 2 + case .video: return 3 + case .twonly: return 4 + case .image: return 5 + case .contactRequest: return 6 + case .acceptRequest: return 7 + case .storedMediaFile: return 8 + case .testNotification: return 9 + case .reopenedMedia: return 10 + case .reactionToVideo: return 11 + case .reactionToText: return 12 + case .reactionToImage: return 13 + case .UNRECOGNIZED(let i): return i + } + } + + // The compiler won't synthesize support with the UNRECOGNIZED case. + static let allCases: [PushKind] = [ + .reaction, + .response, + .text, + .video, + .twonly, + .image, + .contactRequest, + .acceptRequest, + .storedMediaFile, + .testNotification, + .reopenedMedia, + .reactionToVideo, + .reactionToText, + .reactionToImage, + ] + +} + +struct EncryptedPushNotification: @unchecked Sendable { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + var keyID: Int64 = 0 + + var nonce: Data = Data() + + var ciphertext: Data = Data() + + var mac: Data = Data() + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} +} + +struct PushNotification: Sendable { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + var kind: PushKind = .reaction + + var messageID: Int64 { + get {return _messageID ?? 0} + set {_messageID = newValue} + } + /// Returns true if `messageID` has been explicitly set. + var hasMessageID: Bool {return self._messageID != nil} + /// Clears the value of `messageID`. Subsequent reads from it will return its default value. + mutating func clearMessageID() {self._messageID = nil} + + var reactionContent: String { + get {return _reactionContent ?? String()} + set {_reactionContent = newValue} + } + /// Returns true if `reactionContent` has been explicitly set. + var hasReactionContent: Bool {return self._reactionContent != nil} + /// Clears the value of `reactionContent`. Subsequent reads from it will return its default value. + mutating func clearReactionContent() {self._reactionContent = nil} + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} + + fileprivate var _messageID: Int64? = nil + fileprivate var _reactionContent: String? = nil +} + +struct PushUsers: Sendable { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + var users: [PushUser] = [] + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} +} + +struct PushUser: Sendable { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + var userID: Int64 = 0 + + var displayName: String = String() + + var blocked: Bool = false + + var lastMessageID: Int64 { + get {return _lastMessageID ?? 0} + set {_lastMessageID = newValue} + } + /// Returns true if `lastMessageID` has been explicitly set. + var hasLastMessageID: Bool {return self._lastMessageID != nil} + /// Clears the value of `lastMessageID`. Subsequent reads from it will return its default value. + mutating func clearLastMessageID() {self._lastMessageID = nil} + + var pushKeys: [PushKey] = [] + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} + + fileprivate var _lastMessageID: Int64? = nil +} + +struct PushKey: @unchecked Sendable { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + var id: Int64 = 0 + + var key: Data = Data() + + var createdAtUnixTimestamp: Int64 = 0 + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} +} + +// MARK: - Code below here is support for the SwiftProtobuf runtime. + +extension PushKind: SwiftProtobuf._ProtoNameProviding { + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 0: .same(proto: "reaction"), + 1: .same(proto: "response"), + 2: .same(proto: "text"), + 3: .same(proto: "video"), + 4: .same(proto: "twonly"), + 5: .same(proto: "image"), + 6: .same(proto: "contactRequest"), + 7: .same(proto: "acceptRequest"), + 8: .same(proto: "storedMediaFile"), + 9: .same(proto: "testNotification"), + 10: .same(proto: "reopenedMedia"), + 11: .same(proto: "reactionToVideo"), + 12: .same(proto: "reactionToText"), + 13: .same(proto: "reactionToImage"), + ] +} + +extension EncryptedPushNotification: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = "EncryptedPushNotification" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "keyId"), + 2: .same(proto: "nonce"), + 3: .same(proto: "ciphertext"), + 4: .same(proto: "mac"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularInt64Field(value: &self.keyID) }() + case 2: try { try decoder.decodeSingularBytesField(value: &self.nonce) }() + case 3: try { try decoder.decodeSingularBytesField(value: &self.ciphertext) }() + case 4: try { try decoder.decodeSingularBytesField(value: &self.mac) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if self.keyID != 0 { + try visitor.visitSingularInt64Field(value: self.keyID, fieldNumber: 1) + } + if !self.nonce.isEmpty { + try visitor.visitSingularBytesField(value: self.nonce, fieldNumber: 2) + } + if !self.ciphertext.isEmpty { + try visitor.visitSingularBytesField(value: self.ciphertext, fieldNumber: 3) + } + if !self.mac.isEmpty { + try visitor.visitSingularBytesField(value: self.mac, fieldNumber: 4) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: EncryptedPushNotification, rhs: EncryptedPushNotification) -> Bool { + if lhs.keyID != rhs.keyID {return false} + if lhs.nonce != rhs.nonce {return false} + if lhs.ciphertext != rhs.ciphertext {return false} + if lhs.mac != rhs.mac {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension PushNotification: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = "PushNotification" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "kind"), + 2: .same(proto: "messageId"), + 3: .same(proto: "reactionContent"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularEnumField(value: &self.kind) }() + case 2: try { try decoder.decodeSingularInt64Field(value: &self._messageID) }() + case 3: try { try decoder.decodeSingularStringField(value: &self._reactionContent) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 + if self.kind != .reaction { + try visitor.visitSingularEnumField(value: self.kind, fieldNumber: 1) + } + try { if let v = self._messageID { + try visitor.visitSingularInt64Field(value: v, fieldNumber: 2) + } }() + try { if let v = self._reactionContent { + try visitor.visitSingularStringField(value: v, fieldNumber: 3) + } }() + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: PushNotification, rhs: PushNotification) -> Bool { + if lhs.kind != rhs.kind {return false} + if lhs._messageID != rhs._messageID {return false} + if lhs._reactionContent != rhs._reactionContent {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension PushUsers: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = "PushUsers" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "users"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeRepeatedMessageField(value: &self.users) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if !self.users.isEmpty { + try visitor.visitRepeatedMessageField(value: self.users, fieldNumber: 1) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: PushUsers, rhs: PushUsers) -> Bool { + if lhs.users != rhs.users {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension PushUser: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = "PushUser" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "userId"), + 2: .same(proto: "displayName"), + 3: .same(proto: "blocked"), + 4: .same(proto: "lastMessageId"), + 5: .same(proto: "pushKeys"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularInt64Field(value: &self.userID) }() + case 2: try { try decoder.decodeSingularStringField(value: &self.displayName) }() + case 3: try { try decoder.decodeSingularBoolField(value: &self.blocked) }() + case 4: try { try decoder.decodeSingularInt64Field(value: &self._lastMessageID) }() + case 5: try { try decoder.decodeRepeatedMessageField(value: &self.pushKeys) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 + if self.userID != 0 { + try visitor.visitSingularInt64Field(value: self.userID, fieldNumber: 1) + } + if !self.displayName.isEmpty { + try visitor.visitSingularStringField(value: self.displayName, fieldNumber: 2) + } + if self.blocked != false { + try visitor.visitSingularBoolField(value: self.blocked, fieldNumber: 3) + } + try { if let v = self._lastMessageID { + try visitor.visitSingularInt64Field(value: v, fieldNumber: 4) + } }() + if !self.pushKeys.isEmpty { + try visitor.visitRepeatedMessageField(value: self.pushKeys, fieldNumber: 5) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: PushUser, rhs: PushUser) -> Bool { + if lhs.userID != rhs.userID {return false} + if lhs.displayName != rhs.displayName {return false} + if lhs.blocked != rhs.blocked {return false} + if lhs._lastMessageID != rhs._lastMessageID {return false} + if lhs.pushKeys != rhs.pushKeys {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension PushKey: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = "PushKey" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "id"), + 2: .same(proto: "key"), + 3: .same(proto: "createdAtUnixTimestamp"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularInt64Field(value: &self.id) }() + case 2: try { try decoder.decodeSingularBytesField(value: &self.key) }() + case 3: try { try decoder.decodeSingularInt64Field(value: &self.createdAtUnixTimestamp) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if self.id != 0 { + try visitor.visitSingularInt64Field(value: self.id, fieldNumber: 1) + } + if !self.key.isEmpty { + try visitor.visitSingularBytesField(value: self.key, fieldNumber: 2) + } + if self.createdAtUnixTimestamp != 0 { + try visitor.visitSingularInt64Field(value: self.createdAtUnixTimestamp, fieldNumber: 3) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: PushKey, rhs: PushKey) -> Bool { + if lhs.id != rhs.id {return false} + if lhs.key != rhs.key {return false} + if lhs.createdAtUnixTimestamp != rhs.createdAtUnixTimestamp {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} diff --git a/ios/Podfile b/ios/Podfile index d188a75..88b581a 100644 --- a/ios/Podfile +++ b/ios/Podfile @@ -33,6 +33,7 @@ pod 'FirebaseMessaging', :modular_headers => true pod 'FirebaseCoreInternal', :modular_headers => true pod 'GoogleUtilities', :modular_headers => true pod 'FirebaseCore', :modular_headers => true +pod 'SwiftProtobuf' # pod 'sqlite3', :modular_headers => true diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 052f8a2..cca8a12 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -216,6 +216,7 @@ PODS: - sqlite3/math - sqlite3/perf-threadsafe - sqlite3/rtree + - SwiftProtobuf (1.30.0) - url_launcher_ios (0.0.1): - Flutter - video_compress (0.3.0): @@ -253,6 +254,7 @@ DEPENDENCIES: - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) - sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`) - sqlite3_flutter_libs (from `.symlinks/plugins/sqlite3_flutter_libs/darwin`) + - SwiftProtobuf - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) - video_compress (from `.symlinks/plugins/video_compress/ios`) - video_player_avfoundation (from `.symlinks/plugins/video_player_avfoundation/darwin`) @@ -276,6 +278,7 @@ SPEC REPOS: - SDWebImage - SDWebImageWebPCoder - sqlite3 + - SwiftProtobuf EXTERNAL SOURCES: background_downloader: @@ -372,10 +375,11 @@ SPEC CHECKSUMS: sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0 sqlite3: 3c950dc86011117c307eb0b28c4a7bb449dce9f1 sqlite3_flutter_libs: 74334e3ef2dbdb7d37e50859bb45da43935779c4 + SwiftProtobuf: 3697407f0d5b23bedeba9c2eaaf3ec6fdff69349 url_launcher_ios: 694010445543906933d732453a59da0a173ae33d video_compress: f2133a07762889d67f0711ac831faa26f956980e video_player_avfoundation: 2cef49524dd1f16c5300b9cd6efd9611ce03639b -PODFILE CHECKSUM: 3e94c12f4f6904137d1449e3b100fda499ccd32d +PODFILE CHECKSUM: a01f0821a361ca6708e29b1299e8becf492a8a71 COCOAPODS: 1.16.2 diff --git a/lib/app.dart b/lib/app.dart index d55289f..c53fe96 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -5,7 +5,6 @@ import 'package:twonly/src/localization/generated/app_localizations.dart'; import 'package:twonly/src/providers/connection.provider.dart'; import 'package:twonly/src/providers/settings.provider.dart'; import 'package:twonly/src/services/api/media_upload.dart'; -import 'package:twonly/src/services/notification.service.dart'; import 'package:twonly/src/utils/storage.dart'; import 'package:twonly/src/views/onboarding/onboarding.view.dart'; import 'package:twonly/src/views/home.view.dart'; @@ -36,7 +35,6 @@ class _AppState extends State with WidgetsBindingObserver { globalCallbackConnectionState = (update) { context.read().updateConnectionState(update); setUserPlan(); - setupNotificationWithUsers(); }; initAsync(); diff --git a/lib/main.dart b/lib/main.dart index 90f287d..4522a8f 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -11,7 +11,7 @@ import 'package:flutter/material.dart'; import 'package:twonly/src/providers/connection.provider.dart'; import 'package:twonly/src/providers/settings.provider.dart'; import 'package:twonly/src/services/fcm.service.dart'; -import 'package:twonly/src/services/notification.service.dart'; +import 'package:twonly/src/services/notifications/setup.notifications.dart'; import 'package:twonly/src/services/twonly_safe/create_backup.twonly_safe.dart'; import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/storage.dart'; diff --git a/lib/src/constants/secure_storage_keys.dart b/lib/src/constants/secure_storage_keys.dart index 14035c0..73b59a4 100644 --- a/lib/src/constants/secure_storage_keys.dart +++ b/lib/src/constants/secure_storage_keys.dart @@ -5,4 +5,7 @@ class SecureStorageKeys { static const String googleFcm = "google_fcm"; static const String userData = "userData"; static const String twonlySafeLastBackupHash = "twonly_safe_last_backup_hash"; + + static const String receivingPushKeys = "receiving_pus_keys"; + static const String sendingPushKeys = "sending_pus_keys"; } diff --git a/lib/src/database/daos/contacts_dao.dart b/lib/src/database/daos/contacts_dao.dart index 2580661..19f95e5 100644 --- a/lib/src/database/daos/contacts_dao.dart +++ b/lib/src/database/daos/contacts_dao.dart @@ -1,7 +1,7 @@ import 'package:drift/drift.dart'; import 'package:twonly/src/database/tables/contacts_table.dart'; import 'package:twonly/src/database/twonly_database.dart'; -import 'package:twonly/src/services/notification.service.dart'; +import 'package:twonly/src/services/notifications/pushkeys.notifications.dart'; part 'contacts_dao.g.dart'; @@ -113,7 +113,11 @@ class ContactsDao extends DatabaseAccessor Future newMessageExchange(int userId) { return updateContact( - userId, ContactsCompanion(lastMessageExchange: Value(DateTime.now()))); + userId, + ContactsCompanion( + lastMessageExchange: Value(DateTime.now()), + ), + ); } Stream> watchNotAcceptedContacts() { diff --git a/lib/src/model/protobuf/api/websocket/error.pbenum.dart b/lib/src/model/protobuf/api/websocket/error.pbenum.dart index a098d6f..b2084c2 100644 --- a/lib/src/model/protobuf/api/websocket/error.pbenum.dart +++ b/lib/src/model/protobuf/api/websocket/error.pbenum.dart @@ -46,6 +46,7 @@ class ErrorCode extends $pb.ProtobufEnum { static const ErrorCode InvalidSignedPreKey = ErrorCode._(1027, _omitEnumNames ? '' : 'InvalidSignedPreKey'); static const ErrorCode UserIdNotFound = ErrorCode._(1028, _omitEnumNames ? '' : 'UserIdNotFound'); static const ErrorCode UserIdAlreadyTaken = ErrorCode._(1029, _omitEnumNames ? '' : 'UserIdAlreadyTaken'); + static const ErrorCode AppVersionOutdated = ErrorCode._(1030, _omitEnumNames ? '' : 'AppVersionOutdated'); static const $core.List values = [ Unknown, @@ -80,6 +81,7 @@ class ErrorCode extends $pb.ProtobufEnum { InvalidSignedPreKey, UserIdNotFound, UserIdAlreadyTaken, + AppVersionOutdated, ]; static final $core.Map<$core.int, ErrorCode> _byValue = $pb.ProtobufEnum.initByValue(values); diff --git a/lib/src/model/protobuf/api/websocket/error.pbjson.dart b/lib/src/model/protobuf/api/websocket/error.pbjson.dart index 47f586e..cd4bbd2 100644 --- a/lib/src/model/protobuf/api/websocket/error.pbjson.dart +++ b/lib/src/model/protobuf/api/websocket/error.pbjson.dart @@ -49,6 +49,7 @@ const ErrorCode$json = { {'1': 'InvalidSignedPreKey', '2': 1027}, {'1': 'UserIdNotFound', '2': 1028}, {'1': 'UserIdAlreadyTaken', '2': 1029}, + {'1': 'AppVersionOutdated', '2': 1030}, ], }; @@ -68,5 +69,5 @@ final $typed_data.Uint8List errorCodeDescriptor = $convert.base64Decode( 'dlZBD+BxIVChBQbGFuTGltaXRSZWFjaGVkEP8HEhQKD05vdEVub3VnaENyZWRpdBCACBISCg1Q' 'bGFuRG93bmdyYWRlEIEIEhkKFFBsYW5VcGdyYWRlTm90WWVhcmx5EIIIEhgKE0ludmFsaWRTaW' 'duZWRQcmVLZXkQgwgSEwoOVXNlcklkTm90Rm91bmQQhAgSFwoSVXNlcklkQWxyZWFkeVRha2Vu' - 'EIUI'); + 'EIUIEhcKEkFwcFZlcnNpb25PdXRkYXRlZBCGCA=='); diff --git a/lib/src/model/protobuf/push_notification/push_notification.pb.dart b/lib/src/model/protobuf/push_notification/push_notification.pb.dart new file mode 100644 index 0000000..c0e8a0d --- /dev/null +++ b/lib/src/model/protobuf/push_notification/push_notification.pb.dart @@ -0,0 +1,415 @@ +// +// Generated code. Do not modify. +// source: push_notification.proto +// +// @dart = 2.12 + +// ignore_for_file: annotate_overrides, camel_case_types, comment_references +// ignore_for_file: constant_identifier_names, library_prefixes +// ignore_for_file: non_constant_identifier_names, prefer_final_fields +// ignore_for_file: unnecessary_import, unnecessary_this, unused_import + +import 'dart:core' as $core; + +import 'package:fixnum/fixnum.dart' as $fixnum; +import 'package:protobuf/protobuf.dart' as $pb; + +import 'push_notification.pbenum.dart'; + +export 'push_notification.pbenum.dart'; + +class EncryptedPushNotification extends $pb.GeneratedMessage { + factory EncryptedPushNotification({ + $fixnum.Int64? keyId, + $core.List<$core.int>? nonce, + $core.List<$core.int>? ciphertext, + $core.List<$core.int>? mac, + }) { + final $result = create(); + if (keyId != null) { + $result.keyId = keyId; + } + if (nonce != null) { + $result.nonce = nonce; + } + if (ciphertext != null) { + $result.ciphertext = ciphertext; + } + if (mac != null) { + $result.mac = mac; + } + return $result; + } + EncryptedPushNotification._() : super(); + factory EncryptedPushNotification.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory EncryptedPushNotification.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'EncryptedPushNotification', createEmptyInstance: create) + ..aInt64(1, _omitFieldNames ? '' : 'keyId', protoName: 'keyId') + ..a<$core.List<$core.int>>(2, _omitFieldNames ? '' : 'nonce', $pb.PbFieldType.OY) + ..a<$core.List<$core.int>>(3, _omitFieldNames ? '' : 'ciphertext', $pb.PbFieldType.OY) + ..a<$core.List<$core.int>>(4, _omitFieldNames ? '' : 'mac', $pb.PbFieldType.OY) + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + EncryptedPushNotification clone() => EncryptedPushNotification()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + EncryptedPushNotification copyWith(void Function(EncryptedPushNotification) updates) => super.copyWith((message) => updates(message as EncryptedPushNotification)) as EncryptedPushNotification; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static EncryptedPushNotification create() => EncryptedPushNotification._(); + EncryptedPushNotification createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static EncryptedPushNotification getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static EncryptedPushNotification? _defaultInstance; + + @$pb.TagNumber(1) + $fixnum.Int64 get keyId => $_getI64(0); + @$pb.TagNumber(1) + set keyId($fixnum.Int64 v) { $_setInt64(0, v); } + @$pb.TagNumber(1) + $core.bool hasKeyId() => $_has(0); + @$pb.TagNumber(1) + void clearKeyId() => clearField(1); + + @$pb.TagNumber(2) + $core.List<$core.int> get nonce => $_getN(1); + @$pb.TagNumber(2) + set nonce($core.List<$core.int> v) { $_setBytes(1, v); } + @$pb.TagNumber(2) + $core.bool hasNonce() => $_has(1); + @$pb.TagNumber(2) + void clearNonce() => clearField(2); + + @$pb.TagNumber(3) + $core.List<$core.int> get ciphertext => $_getN(2); + @$pb.TagNumber(3) + set ciphertext($core.List<$core.int> v) { $_setBytes(2, v); } + @$pb.TagNumber(3) + $core.bool hasCiphertext() => $_has(2); + @$pb.TagNumber(3) + void clearCiphertext() => clearField(3); + + @$pb.TagNumber(4) + $core.List<$core.int> get mac => $_getN(3); + @$pb.TagNumber(4) + set mac($core.List<$core.int> v) { $_setBytes(3, v); } + @$pb.TagNumber(4) + $core.bool hasMac() => $_has(3); + @$pb.TagNumber(4) + void clearMac() => clearField(4); +} + +class PushNotification extends $pb.GeneratedMessage { + factory PushNotification({ + PushKind? kind, + $fixnum.Int64? messageId, + $core.String? reactionContent, + }) { + final $result = create(); + if (kind != null) { + $result.kind = kind; + } + if (messageId != null) { + $result.messageId = messageId; + } + if (reactionContent != null) { + $result.reactionContent = reactionContent; + } + return $result; + } + PushNotification._() : super(); + factory PushNotification.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory PushNotification.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'PushNotification', createEmptyInstance: create) + ..e(1, _omitFieldNames ? '' : 'kind', $pb.PbFieldType.OE, defaultOrMaker: PushKind.reaction, valueOf: PushKind.valueOf, enumValues: PushKind.values) + ..aInt64(2, _omitFieldNames ? '' : 'messageId', protoName: 'messageId') + ..aOS(3, _omitFieldNames ? '' : 'reactionContent', protoName: 'reactionContent') + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + PushNotification clone() => PushNotification()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + PushNotification copyWith(void Function(PushNotification) updates) => super.copyWith((message) => updates(message as PushNotification)) as PushNotification; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static PushNotification create() => PushNotification._(); + PushNotification createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static PushNotification getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static PushNotification? _defaultInstance; + + @$pb.TagNumber(1) + PushKind get kind => $_getN(0); + @$pb.TagNumber(1) + set kind(PushKind v) { setField(1, v); } + @$pb.TagNumber(1) + $core.bool hasKind() => $_has(0); + @$pb.TagNumber(1) + void clearKind() => clearField(1); + + @$pb.TagNumber(2) + $fixnum.Int64 get messageId => $_getI64(1); + @$pb.TagNumber(2) + set messageId($fixnum.Int64 v) { $_setInt64(1, v); } + @$pb.TagNumber(2) + $core.bool hasMessageId() => $_has(1); + @$pb.TagNumber(2) + void clearMessageId() => clearField(2); + + @$pb.TagNumber(3) + $core.String get reactionContent => $_getSZ(2); + @$pb.TagNumber(3) + set reactionContent($core.String v) { $_setString(2, v); } + @$pb.TagNumber(3) + $core.bool hasReactionContent() => $_has(2); + @$pb.TagNumber(3) + void clearReactionContent() => clearField(3); +} + +class PushUsers extends $pb.GeneratedMessage { + factory PushUsers({ + $core.Iterable? users, + }) { + final $result = create(); + if (users != null) { + $result.users.addAll(users); + } + return $result; + } + PushUsers._() : super(); + factory PushUsers.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory PushUsers.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'PushUsers', createEmptyInstance: create) + ..pc(1, _omitFieldNames ? '' : 'users', $pb.PbFieldType.PM, subBuilder: PushUser.create) + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + PushUsers clone() => PushUsers()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + PushUsers copyWith(void Function(PushUsers) updates) => super.copyWith((message) => updates(message as PushUsers)) as PushUsers; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static PushUsers create() => PushUsers._(); + PushUsers createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static PushUsers getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static PushUsers? _defaultInstance; + + @$pb.TagNumber(1) + $core.List get users => $_getList(0); +} + +class PushUser extends $pb.GeneratedMessage { + factory PushUser({ + $fixnum.Int64? userId, + $core.String? displayName, + $core.bool? blocked, + $fixnum.Int64? lastMessageId, + $core.Iterable? pushKeys, + }) { + final $result = create(); + if (userId != null) { + $result.userId = userId; + } + if (displayName != null) { + $result.displayName = displayName; + } + if (blocked != null) { + $result.blocked = blocked; + } + if (lastMessageId != null) { + $result.lastMessageId = lastMessageId; + } + if (pushKeys != null) { + $result.pushKeys.addAll(pushKeys); + } + return $result; + } + PushUser._() : super(); + factory PushUser.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory PushUser.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'PushUser', createEmptyInstance: create) + ..aInt64(1, _omitFieldNames ? '' : 'userId', protoName: 'userId') + ..aOS(2, _omitFieldNames ? '' : 'displayName', protoName: 'displayName') + ..aOB(3, _omitFieldNames ? '' : 'blocked') + ..aInt64(4, _omitFieldNames ? '' : 'lastMessageId', protoName: 'lastMessageId') + ..pc(5, _omitFieldNames ? '' : 'pushKeys', $pb.PbFieldType.PM, protoName: 'pushKeys', subBuilder: PushKey.create) + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + PushUser clone() => PushUser()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + PushUser copyWith(void Function(PushUser) updates) => super.copyWith((message) => updates(message as PushUser)) as PushUser; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static PushUser create() => PushUser._(); + PushUser createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static PushUser getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static PushUser? _defaultInstance; + + @$pb.TagNumber(1) + $fixnum.Int64 get userId => $_getI64(0); + @$pb.TagNumber(1) + set userId($fixnum.Int64 v) { $_setInt64(0, v); } + @$pb.TagNumber(1) + $core.bool hasUserId() => $_has(0); + @$pb.TagNumber(1) + void clearUserId() => clearField(1); + + @$pb.TagNumber(2) + $core.String get displayName => $_getSZ(1); + @$pb.TagNumber(2) + set displayName($core.String v) { $_setString(1, v); } + @$pb.TagNumber(2) + $core.bool hasDisplayName() => $_has(1); + @$pb.TagNumber(2) + void clearDisplayName() => clearField(2); + + @$pb.TagNumber(3) + $core.bool get blocked => $_getBF(2); + @$pb.TagNumber(3) + set blocked($core.bool v) { $_setBool(2, v); } + @$pb.TagNumber(3) + $core.bool hasBlocked() => $_has(2); + @$pb.TagNumber(3) + void clearBlocked() => clearField(3); + + @$pb.TagNumber(4) + $fixnum.Int64 get lastMessageId => $_getI64(3); + @$pb.TagNumber(4) + set lastMessageId($fixnum.Int64 v) { $_setInt64(3, v); } + @$pb.TagNumber(4) + $core.bool hasLastMessageId() => $_has(3); + @$pb.TagNumber(4) + void clearLastMessageId() => clearField(4); + + @$pb.TagNumber(5) + $core.List get pushKeys => $_getList(4); +} + +class PushKey extends $pb.GeneratedMessage { + factory PushKey({ + $fixnum.Int64? id, + $core.List<$core.int>? key, + $fixnum.Int64? createdAtUnixTimestamp, + }) { + final $result = create(); + if (id != null) { + $result.id = id; + } + if (key != null) { + $result.key = key; + } + if (createdAtUnixTimestamp != null) { + $result.createdAtUnixTimestamp = createdAtUnixTimestamp; + } + return $result; + } + PushKey._() : super(); + factory PushKey.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory PushKey.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'PushKey', createEmptyInstance: create) + ..aInt64(1, _omitFieldNames ? '' : 'id') + ..a<$core.List<$core.int>>(2, _omitFieldNames ? '' : 'key', $pb.PbFieldType.OY) + ..aInt64(3, _omitFieldNames ? '' : 'createdAtUnixTimestamp', protoName: 'createdAtUnixTimestamp') + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + PushKey clone() => PushKey()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + PushKey copyWith(void Function(PushKey) updates) => super.copyWith((message) => updates(message as PushKey)) as PushKey; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static PushKey create() => PushKey._(); + PushKey createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static PushKey getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static PushKey? _defaultInstance; + + @$pb.TagNumber(1) + $fixnum.Int64 get id => $_getI64(0); + @$pb.TagNumber(1) + set id($fixnum.Int64 v) { $_setInt64(0, v); } + @$pb.TagNumber(1) + $core.bool hasId() => $_has(0); + @$pb.TagNumber(1) + void clearId() => clearField(1); + + @$pb.TagNumber(2) + $core.List<$core.int> get key => $_getN(1); + @$pb.TagNumber(2) + set key($core.List<$core.int> v) { $_setBytes(1, v); } + @$pb.TagNumber(2) + $core.bool hasKey() => $_has(1); + @$pb.TagNumber(2) + void clearKey() => clearField(2); + + @$pb.TagNumber(3) + $fixnum.Int64 get createdAtUnixTimestamp => $_getI64(2); + @$pb.TagNumber(3) + set createdAtUnixTimestamp($fixnum.Int64 v) { $_setInt64(2, v); } + @$pb.TagNumber(3) + $core.bool hasCreatedAtUnixTimestamp() => $_has(2); + @$pb.TagNumber(3) + void clearCreatedAtUnixTimestamp() => clearField(3); +} + + +const _omitFieldNames = $core.bool.fromEnvironment('protobuf.omit_field_names'); +const _omitMessageNames = $core.bool.fromEnvironment('protobuf.omit_message_names'); diff --git a/lib/src/model/protobuf/push_notification/push_notification.pbenum.dart b/lib/src/model/protobuf/push_notification/push_notification.pbenum.dart new file mode 100644 index 0000000..8bf3ded --- /dev/null +++ b/lib/src/model/protobuf/push_notification/push_notification.pbenum.dart @@ -0,0 +1,56 @@ +// +// Generated code. Do not modify. +// source: push_notification.proto +// +// @dart = 2.12 + +// ignore_for_file: annotate_overrides, camel_case_types, comment_references +// ignore_for_file: constant_identifier_names, library_prefixes +// ignore_for_file: non_constant_identifier_names, prefer_final_fields +// ignore_for_file: unnecessary_import, unnecessary_this, unused_import + +import 'dart:core' as $core; + +import 'package:protobuf/protobuf.dart' as $pb; + +class PushKind extends $pb.ProtobufEnum { + static const PushKind reaction = PushKind._(0, _omitEnumNames ? '' : 'reaction'); + static const PushKind response = PushKind._(1, _omitEnumNames ? '' : 'response'); + static const PushKind text = PushKind._(2, _omitEnumNames ? '' : 'text'); + static const PushKind video = PushKind._(3, _omitEnumNames ? '' : 'video'); + static const PushKind twonly = PushKind._(4, _omitEnumNames ? '' : 'twonly'); + static const PushKind image = PushKind._(5, _omitEnumNames ? '' : 'image'); + static const PushKind contactRequest = PushKind._(6, _omitEnumNames ? '' : 'contactRequest'); + static const PushKind acceptRequest = PushKind._(7, _omitEnumNames ? '' : 'acceptRequest'); + static const PushKind storedMediaFile = PushKind._(8, _omitEnumNames ? '' : 'storedMediaFile'); + static const PushKind testNotification = PushKind._(9, _omitEnumNames ? '' : 'testNotification'); + static const PushKind reopenedMedia = PushKind._(10, _omitEnumNames ? '' : 'reopenedMedia'); + static const PushKind reactionToVideo = PushKind._(11, _omitEnumNames ? '' : 'reactionToVideo'); + static const PushKind reactionToText = PushKind._(12, _omitEnumNames ? '' : 'reactionToText'); + static const PushKind reactionToImage = PushKind._(13, _omitEnumNames ? '' : 'reactionToImage'); + + static const $core.List values = [ + reaction, + response, + text, + video, + twonly, + image, + contactRequest, + acceptRequest, + storedMediaFile, + testNotification, + reopenedMedia, + reactionToVideo, + reactionToText, + reactionToImage, + ]; + + static final $core.Map<$core.int, PushKind> _byValue = $pb.ProtobufEnum.initByValue(values); + static PushKind? valueOf($core.int value) => _byValue[value]; + + const PushKind._($core.int v, $core.String n) : super(v, n); +} + + +const _omitEnumNames = $core.bool.fromEnvironment('protobuf.omit_enum_names'); diff --git a/lib/src/model/protobuf/push_notification/push_notification.pbjson.dart b/lib/src/model/protobuf/push_notification/push_notification.pbjson.dart new file mode 100644 index 0000000..d66d5aa --- /dev/null +++ b/lib/src/model/protobuf/push_notification/push_notification.pbjson.dart @@ -0,0 +1,130 @@ +// +// Generated code. Do not modify. +// source: push_notification.proto +// +// @dart = 2.12 + +// ignore_for_file: annotate_overrides, camel_case_types, comment_references +// ignore_for_file: constant_identifier_names, library_prefixes +// ignore_for_file: non_constant_identifier_names, prefer_final_fields +// ignore_for_file: unnecessary_import, unnecessary_this, unused_import + +import 'dart:convert' as $convert; +import 'dart:core' as $core; +import 'dart:typed_data' as $typed_data; + +@$core.Deprecated('Use pushKindDescriptor instead') +const PushKind$json = { + '1': 'PushKind', + '2': [ + {'1': 'reaction', '2': 0}, + {'1': 'response', '2': 1}, + {'1': 'text', '2': 2}, + {'1': 'video', '2': 3}, + {'1': 'twonly', '2': 4}, + {'1': 'image', '2': 5}, + {'1': 'contactRequest', '2': 6}, + {'1': 'acceptRequest', '2': 7}, + {'1': 'storedMediaFile', '2': 8}, + {'1': 'testNotification', '2': 9}, + {'1': 'reopenedMedia', '2': 10}, + {'1': 'reactionToVideo', '2': 11}, + {'1': 'reactionToText', '2': 12}, + {'1': 'reactionToImage', '2': 13}, + ], +}; + +/// Descriptor for `PushKind`. Decode as a `google.protobuf.EnumDescriptorProto`. +final $typed_data.Uint8List pushKindDescriptor = $convert.base64Decode( + 'CghQdXNoS2luZBIMCghyZWFjdGlvbhAAEgwKCHJlc3BvbnNlEAESCAoEdGV4dBACEgkKBXZpZG' + 'VvEAMSCgoGdHdvbmx5EAQSCQoFaW1hZ2UQBRISCg5jb250YWN0UmVxdWVzdBAGEhEKDWFjY2Vw' + 'dFJlcXVlc3QQBxITCg9zdG9yZWRNZWRpYUZpbGUQCBIUChB0ZXN0Tm90aWZpY2F0aW9uEAkSEQ' + 'oNcmVvcGVuZWRNZWRpYRAKEhMKD3JlYWN0aW9uVG9WaWRlbxALEhIKDnJlYWN0aW9uVG9UZXh0' + 'EAwSEwoPcmVhY3Rpb25Ub0ltYWdlEA0='); + +@$core.Deprecated('Use encryptedPushNotificationDescriptor instead') +const EncryptedPushNotification$json = { + '1': 'EncryptedPushNotification', + '2': [ + {'1': 'keyId', '3': 1, '4': 1, '5': 3, '10': 'keyId'}, + {'1': 'nonce', '3': 2, '4': 1, '5': 12, '10': 'nonce'}, + {'1': 'ciphertext', '3': 3, '4': 1, '5': 12, '10': 'ciphertext'}, + {'1': 'mac', '3': 4, '4': 1, '5': 12, '10': 'mac'}, + ], +}; + +/// Descriptor for `EncryptedPushNotification`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List encryptedPushNotificationDescriptor = $convert.base64Decode( + 'ChlFbmNyeXB0ZWRQdXNoTm90aWZpY2F0aW9uEhQKBWtleUlkGAEgASgDUgVrZXlJZBIUCgVub2' + '5jZRgCIAEoDFIFbm9uY2USHgoKY2lwaGVydGV4dBgDIAEoDFIKY2lwaGVydGV4dBIQCgNtYWMY' + 'BCABKAxSA21hYw=='); + +@$core.Deprecated('Use pushNotificationDescriptor instead') +const PushNotification$json = { + '1': 'PushNotification', + '2': [ + {'1': 'kind', '3': 1, '4': 1, '5': 14, '6': '.PushKind', '10': 'kind'}, + {'1': 'messageId', '3': 2, '4': 1, '5': 3, '9': 0, '10': 'messageId', '17': true}, + {'1': 'reactionContent', '3': 3, '4': 1, '5': 9, '9': 1, '10': 'reactionContent', '17': true}, + ], + '8': [ + {'1': '_messageId'}, + {'1': '_reactionContent'}, + ], +}; + +/// Descriptor for `PushNotification`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List pushNotificationDescriptor = $convert.base64Decode( + 'ChBQdXNoTm90aWZpY2F0aW9uEh0KBGtpbmQYASABKA4yCS5QdXNoS2luZFIEa2luZBIhCgltZX' + 'NzYWdlSWQYAiABKANIAFIJbWVzc2FnZUlkiAEBEi0KD3JlYWN0aW9uQ29udGVudBgDIAEoCUgB' + 'Ug9yZWFjdGlvbkNvbnRlbnSIAQFCDAoKX21lc3NhZ2VJZEISChBfcmVhY3Rpb25Db250ZW50'); + +@$core.Deprecated('Use pushUsersDescriptor instead') +const PushUsers$json = { + '1': 'PushUsers', + '2': [ + {'1': 'users', '3': 1, '4': 3, '5': 11, '6': '.PushUser', '10': 'users'}, + ], +}; + +/// Descriptor for `PushUsers`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List pushUsersDescriptor = $convert.base64Decode( + 'CglQdXNoVXNlcnMSHwoFdXNlcnMYASADKAsyCS5QdXNoVXNlclIFdXNlcnM='); + +@$core.Deprecated('Use pushUserDescriptor instead') +const PushUser$json = { + '1': 'PushUser', + '2': [ + {'1': 'userId', '3': 1, '4': 1, '5': 3, '10': 'userId'}, + {'1': 'displayName', '3': 2, '4': 1, '5': 9, '10': 'displayName'}, + {'1': 'blocked', '3': 3, '4': 1, '5': 8, '10': 'blocked'}, + {'1': 'lastMessageId', '3': 4, '4': 1, '5': 3, '9': 0, '10': 'lastMessageId', '17': true}, + {'1': 'pushKeys', '3': 5, '4': 3, '5': 11, '6': '.PushKey', '10': 'pushKeys'}, + ], + '8': [ + {'1': '_lastMessageId'}, + ], +}; + +/// Descriptor for `PushUser`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List pushUserDescriptor = $convert.base64Decode( + 'CghQdXNoVXNlchIWCgZ1c2VySWQYASABKANSBnVzZXJJZBIgCgtkaXNwbGF5TmFtZRgCIAEoCV' + 'ILZGlzcGxheU5hbWUSGAoHYmxvY2tlZBgDIAEoCFIHYmxvY2tlZBIpCg1sYXN0TWVzc2FnZUlk' + 'GAQgASgDSABSDWxhc3RNZXNzYWdlSWSIAQESJAoIcHVzaEtleXMYBSADKAsyCC5QdXNoS2V5Ug' + 'hwdXNoS2V5c0IQCg5fbGFzdE1lc3NhZ2VJZA=='); + +@$core.Deprecated('Use pushKeyDescriptor instead') +const PushKey$json = { + '1': 'PushKey', + '2': [ + {'1': 'id', '3': 1, '4': 1, '5': 3, '10': 'id'}, + {'1': 'key', '3': 2, '4': 1, '5': 12, '10': 'key'}, + {'1': 'createdAtUnixTimestamp', '3': 3, '4': 1, '5': 3, '10': 'createdAtUnixTimestamp'}, + ], +}; + +/// Descriptor for `PushKey`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List pushKeyDescriptor = $convert.base64Decode( + 'CgdQdXNoS2V5Eg4KAmlkGAEgASgDUgJpZBIQCgNrZXkYAiABKAxSA2tleRI2ChZjcmVhdGVkQX' + 'RVbml4VGltZXN0YW1wGAMgASgDUhZjcmVhdGVkQXRVbml4VGltZXN0YW1w'); + diff --git a/lib/src/model/protobuf/push_notification/push_notification.pbserver.dart b/lib/src/model/protobuf/push_notification/push_notification.pbserver.dart new file mode 100644 index 0000000..a77f69b --- /dev/null +++ b/lib/src/model/protobuf/push_notification/push_notification.pbserver.dart @@ -0,0 +1,14 @@ +// +// Generated code. Do not modify. +// source: push_notification.proto +// +// @dart = 2.12 + +// ignore_for_file: annotate_overrides, camel_case_types, comment_references +// ignore_for_file: constant_identifier_names +// ignore_for_file: deprecated_member_use_from_same_package, library_prefixes +// ignore_for_file: non_constant_identifier_names, prefer_final_fields +// ignore_for_file: unnecessary_import, unnecessary_this, unused_import + +export 'push_notification.pb.dart'; + diff --git a/lib/src/model/protobuf/push_notification/push_notification.proto b/lib/src/model/protobuf/push_notification/push_notification.proto new file mode 100644 index 0000000..3c19946 --- /dev/null +++ b/lib/src/model/protobuf/push_notification/push_notification.proto @@ -0,0 +1,50 @@ +syntax = "proto3"; + +message EncryptedPushNotification { + int64 keyId = 1; + bytes nonce = 2; + bytes ciphertext = 3; + bytes mac = 4; +} + +enum PushKind { + reaction = 0; + response = 1; + text = 2; + video = 3; + twonly = 4; + image = 5; + contactRequest = 6; + acceptRequest = 7; + storedMediaFile = 8; + testNotification = 9; + reopenedMedia = 10; + reactionToVideo = 11; + reactionToText = 12; + reactionToImage = 13; +}; + +message PushNotification { + PushKind kind = 1; + optional int64 messageId = 2; + optional string reactionContent = 3; +} + + +message PushUsers { + repeated PushUser users = 1; +} + +message PushUser { + int64 userId = 1; + string displayName = 2; + bool blocked = 3; + optional int64 lastMessageId = 4; + repeated PushKey pushKeys = 5; +} + +message PushKey { + int64 id = 1; + bytes key = 2; + int64 createdAtUnixTimestamp = 3; +} \ No newline at end of file diff --git a/lib/src/services/api.service.dart b/lib/src/services/api.service.dart index 847da60..834cc6e 100644 --- a/lib/src/services/api.service.dart +++ b/lib/src/services/api.service.dart @@ -23,6 +23,7 @@ import 'package:twonly/src/services/api/utils.dart'; import 'package:twonly/src/services/api/media_download.dart'; import 'package:twonly/src/services/api/media_upload.dart'; import 'package:twonly/src/services/api/server_messages.dart'; +import 'package:twonly/src/services/notifications/pushkeys.notifications.dart'; import 'package:twonly/src/services/signal/identity.signal.dart'; import 'package:twonly/src/services/signal/prekeys.signal.dart'; import 'package:twonly/src/services/signal/utils.signal.dart'; @@ -92,6 +93,7 @@ class ApiService { notifyContactsAboutProfileChange(); twonlyDB.markUpdated(); syncFlameCounters(); + setupNotificationWithUsers(); signalHandleNewServerConnection(); } } diff --git a/lib/src/services/api/media_upload.dart b/lib/src/services/api/media_upload.dart index edc5c64..0a12bde 100644 --- a/lib/src/services/api/media_upload.dart +++ b/lib/src/services/api/media_upload.dart @@ -20,8 +20,9 @@ import 'package:twonly/src/database/twonly_database.dart'; import 'package:twonly/src/model/json/message.dart'; import 'package:twonly/src/model/protobuf/api/http/http_requests.pb.dart'; import 'package:twonly/src/model/protobuf/api/websocket/error.pb.dart'; +import 'package:twonly/src/model/protobuf/push_notification/push_notification.pbserver.dart'; import 'package:twonly/src/services/api/media_download.dart'; -import 'package:twonly/src/services/notification.service.dart'; +import 'package:twonly/src/services/notifications/pushkeys.notifications.dart'; import 'package:twonly/src/services/signal/encryption.signal.dart'; import 'package:twonly/src/services/twonly_safe/create_backup.twonly_safe.dart'; import 'package:twonly/src/utils/log.dart'; @@ -539,17 +540,24 @@ Future handleMediaUpload(MediaUpload media) async { ); if (encryptedBytes == null) continue; + + var messageOnSuccess = TextMessage() + ..body = encryptedBytes + ..userId = Int64(message.contactId); + final pushKind = (media.metadata!.isRealTwonly) ? PushKind.twonly : (media.metadata!.isVideo) ? PushKind.video : PushKind.image; - var messageOnSuccess = TextMessage() - ..body = encryptedBytes - ..userId = Int64(message.contactId); - - var pushData = await getPushData(message.contactId, pushKind); + final pushData = await getPushData( + message.contactId, + PushNotification( + messageId: Int64(message.messageId), + kind: pushKind, + ), + ); if (pushData != null) { messageOnSuccess.pushData = pushData.toList(); } diff --git a/lib/src/services/api/messages.dart b/lib/src/services/api/messages.dart index 021c9c6..ba152e5 100644 --- a/lib/src/services/api/messages.dart +++ b/lib/src/services/api/messages.dart @@ -2,15 +2,17 @@ import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'package:drift/drift.dart'; +import 'package:fixnum/fixnum.dart'; import 'package:twonly/globals.dart'; import 'package:twonly/src/database/twonly_database.dart'; import 'package:twonly/src/database/tables/messages_table.dart'; import 'package:twonly/src/model/json/message.dart'; import 'package:twonly/src/model/json/userdata.dart'; import 'package:twonly/src/model/protobuf/api/websocket/error.pb.dart'; +import 'package:twonly/src/model/protobuf/push_notification/push_notification.pb.dart'; import 'package:twonly/src/services/api/utils.dart'; +import 'package:twonly/src/services/notifications/pushkeys.notifications.dart'; import 'package:twonly/src/services/signal/encryption.signal.dart'; -import 'package:twonly/src/services/notification.service.dart'; import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/storage.dart'; @@ -116,14 +118,15 @@ Future sendRetransmitMessage(int retransId) async { // encrypts and stores the message and then sends it in the background Future encryptAndSendMessageAsync(int? messageId, int userId, MessageJson msg, - {PushKind? pushKind, bool willNotGetACKByUser = false}) async { + {PushNotification? pushNotification, + bool willNotGetACKByUser = false}) async { if (gIsDemoUser) { return; } Uint8List? pushData; - if (pushKind != null) { - pushData = await getPushData(userId, pushKind); + if (pushNotification != null) { + pushData = await getPushData(userId, pushNotification); } int? retransId = await twonlyDB.messageRetransmissionDao.insertRetransmission( @@ -158,7 +161,7 @@ Future encryptAndSendMessageAsync(int? messageId, int userId, MessageJson msg, Future sendTextMessage( int target, TextMessageContent content, - PushKind? pushKind, + PushNotification? pushNotification, ) async { DateTime messageSendAt = DateTime.now(); @@ -178,6 +181,10 @@ Future sendTextMessage( if (messageId == null) return; + if (pushNotification != null && !pushNotification.hasReactionContent()) { + pushNotification.messageId = Int64(messageId); + } + MessageJson msg = MessageJson( kind: MessageKind.textMessage, messageId: messageId, @@ -185,14 +192,22 @@ Future sendTextMessage( timestamp: messageSendAt, ); - await encryptAndSendMessageAsync(messageId, target, msg, pushKind: pushKind); + await encryptAndSendMessageAsync( + messageId, + target, + msg, + pushNotification: pushNotification, + ); } Future notifyContactAboutOpeningMessage( int fromUserId, List messageOtherIds, ) async { + int biggestMessageId = messageOtherIds.first; + for (final messageOtherId in messageOtherIds) { + if (messageOtherId > biggestMessageId) biggestMessageId = messageOtherId; await encryptAndSendMessageAsync( null, fromUserId, @@ -204,6 +219,7 @@ Future notifyContactAboutOpeningMessage( ), ); } + await updateLastMessageId(fromUserId, biggestMessageId); } Future notifyContactsAboutProfileChange() async { @@ -216,8 +232,12 @@ Future notifyContactsAboutProfileChange() async { for (Contact contact in contacts) { if (contact.myAvatarCounter < user.avatarCounter) { - twonlyDB.contactsDao.updateContact(contact.userId, - ContactsCompanion(myAvatarCounter: Value(user.avatarCounter))); + twonlyDB.contactsDao.updateContact( + contact.userId, + ContactsCompanion( + myAvatarCounter: Value(user.avatarCounter), + ), + ); await encryptAndSendMessageAsync( null, contact.userId, diff --git a/lib/src/services/api/server_messages.dart b/lib/src/services/api/server_messages.dart index fb2c3f3..7ecec2e 100644 --- a/lib/src/services/api/server_messages.dart +++ b/lib/src/services/api/server_messages.dart @@ -9,19 +9,21 @@ import 'package:twonly/src/database/tables/messages_table.dart'; import 'package:twonly/src/model/json/message.dart'; import 'package:twonly/src/model/protobuf/api/websocket/client_to_server.pb.dart' as client; -import 'package:twonly/src/model/protobuf/api/websocket/client_to_server.pbserver.dart'; +import 'package:twonly/src/model/protobuf/api/websocket/client_to_server.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' as server; import 'package:twonly/src/services/api/messages.dart'; import 'package:twonly/src/services/api/utils.dart'; import 'package:twonly/src/services/api/media_download.dart'; -import 'package:twonly/src/services/notification.service.dart'; +import 'package:twonly/src/services/notifications/pushkeys.notifications.dart'; +import 'package:twonly/src/services/notifications/setup.notifications.dart'; import 'package:twonly/src/services/signal/encryption.signal.dart'; import 'package:twonly/src/services/signal/identity.signal.dart'; import 'package:twonly/src/services/signal/prekeys.signal.dart'; import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/misc.dart'; +import 'package:twonly/src/views/components/animate_icon.dart'; final lockHandleServerMessage = Mutex(); @@ -272,6 +274,14 @@ Future handleNewMessage(int fromUserId, Uint8List body) async { if (content is TextMessageContent) { responseToMessageId = content.responseToMessageId; responseToOtherMessageId = content.responseToOtherMessageId; + + if (responseToMessageId != null || + responseToOtherMessageId != null) { + // reactions are shown in the notification directly... + if (isEmoji(content.text)) { + openedAt = DateTime.now(); + } + } } if (content is ReopenedMediaFileContent) { responseToMessageId = content.messageId; diff --git a/lib/src/services/fcm.service.dart b/lib/src/services/fcm.service.dart index d625b9c..4b2a1b4 100644 --- a/lib/src/services/fcm.service.dart +++ b/lib/src/services/fcm.service.dart @@ -3,7 +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/services/notification.background.service.dart'; +import 'package:twonly/src/services/notifications/background.notifications.dart'; import 'package:twonly/src/utils/log.dart'; import 'dart:io' show Platform; import '../../firebase_options.dart'; diff --git a/lib/src/services/notification.service.dart b/lib/src/services/notification.service.dart deleted file mode 100644 index fc3e081..0000000 --- a/lib/src/services/notification.service.dart +++ /dev/null @@ -1,422 +0,0 @@ -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_secure_storage/flutter_secure_storage.dart'; -import 'package:flutter_svg/svg.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/model/json/message.dart' as my; -import 'package:twonly/src/services/api/messages.dart'; -import 'package:twonly/src/utils/log.dart'; - -class PushUser { - String displayName; - bool blocked; - List keys; - - PushUser({ - required this.displayName, - required this.blocked, - required this.keys, - }); - - // Factory method to create a User from JSON - factory PushUser.fromJson(Map json) { - return PushUser( - displayName: json['displayName'], - blocked: json['blocked'] ?? false, - keys: (json['keys'] as List) - .map((keyJson) => PushKeyMeta.fromJson(keyJson)) - .toList(), - ); - } - - // Method to convert User to JSON - Map toJson() { - return { - 'displayName': displayName, - 'blocked': blocked, - 'keys': keys.map((key) => key.toJson()).toList(), - }; - } -} - -class PushKeyMeta { - int id; - List key; - DateTime createdAt; - - PushKeyMeta({ - required this.id, - required this.key, - required this.createdAt, - }); - - // Factory method to create Keys from JSON - factory PushKeyMeta.fromJson(Map json) { - return PushKeyMeta( - id: json['id'], - key: List.from(json['key']), - createdAt: DateTime.fromMillisecondsSinceEpoch(json['createdAt']), - ); - } - - // Method to convert Keys to JSON - Map 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 twonlyDB.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.generate(32, (index) => random.nextInt(256)), - createdAt: DateTime.now(), - ); - await 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.generate(32, (index) => random.nextInt(256)), - createdAt: DateTime.now(), - ); - await sendNewPushKey(contact.userId, pushKey); - final pushUser = PushUser( - displayName: getContactDisplayName(contact), - blocked: contact.blocked, - keys: [pushKey], - ); - pushKeys[contact.userId] = pushUser; - wasChanged = true; - } - } - - if (wasChanged) { - await setPushKeys("receivingPushKeys", pushKeys); - } -} - -Future sendNewPushKey(int userId, PushKeyMeta pushKey) async { - await encryptAndSendMessageAsync( - null, - userId, - my.MessageJson( - kind: MessageKind.pushKey, - content: my.PushKeyContent(keyId: pushKey.id, key: pushKey.key), - timestamp: pushKey.createdAt, - ), - ); -} - -Future updatePushUser(Contact contact) async { - var receivingPushKeys = await getPushKeys("receivingPushKeys"); - - if (receivingPushKeys[contact.userId] == null) { - receivingPushKeys[contact.userId] = PushUser( - displayName: getContactDisplayName(contact), - keys: [], - blocked: contact.blocked, - ); - } else { - receivingPushKeys[contact.userId]!.displayName = - getContactDisplayName(contact); - receivingPushKeys[contact.userId]!.blocked = contact.blocked; - } - - await setPushKeys("receivingPushKeys", receivingPushKeys); -} - -Future handleNewPushKey(int fromUserId, my.PushKeyContent pushKey) async { - var pushKeys = await getPushKeys("sendingPushKeys"); - - if (pushKeys[fromUserId] == null) { - pushKeys[fromUserId] = PushUser(displayName: "-", keys: [], blocked: false); - } - - // 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, - reopenedMedia, - reactionToVideo, - reactionToText, - reactionToImage, - response, -} - -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 nonce; - final List cipherText; - final List mac; - - PushNotification({ - required this.keyId, - required this.nonce, - required this.cipherText, - required this.mac, - }); - - // Convert a PushNotification instance to a Map - Map toJson() { - return { - 'keyId': keyId, - 'nonce': base64Encode(nonce), - 'cipherText': base64Encode(cipherText), - 'mac': base64Encode(mac), - }; - } - - // Create a PushNotification instance from a Map - factory PushNotification.fromJson(Map 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 getPushData(int toUserId, PushKind kind) async { - final Map pushKeys = await getPushKeys("sendingPushKeys"); - - List 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; - Log.error("Using insecure key as the receiver does not send a push key!"); - await encryptAndSendMessageAsync( - null, - toUserId, - my.MessageJson( - kind: MessageKind.requestPushKey, - content: my.MessageContent(), - timestamp: DateTime.now(), - ), - ); - } - } else { - try { - key = pushKeys[toUserId]!.keys.last.key; - keyId = pushKeys[toUserId]!.keys.last.id; - } catch (e) { - Log.error("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 Utf8Encoder().convert(jsonEncode(res.toJson())); -} - -Future> getPushKeys(String storageKey) async { - var storage = FlutterSecureStorage(); - String? pushKeysJson = await storage.read( - key: storageKey, - iOptions: IOSOptions( - groupId: "CN332ZUGRP.eu.twonly.shared", - synchronizable: false, - accessibility: KeychainAccessibility.first_unlock, - ), - ); - Map pushKeys = {}; - if (pushKeysJson != null) { - Map jsonMap = jsonDecode(pushKeysJson); - jsonMap.forEach((key, value) { - pushKeys[int.parse(key)] = PushUser.fromJson(value); - }); - } - return pushKeys; -} - -Future setPushKeys(String storageKey, Map pushKeys) async { - var storage = FlutterSecureStorage(); - Map jsonToSend = {}; - pushKeys.forEach((key, value) { - jsonToSend[key.toString()] = value.toJson(); - }); - - await storage.delete( - key: storageKey, - iOptions: IOSOptions( - groupId: "CN332ZUGRP.eu.twonly.shared", - synchronizable: false, - accessibility: KeychainAccessibility.first_unlock, - ), - ); - - String jsonString = jsonEncode(jsonToSend); - await storage.write( - key: storageKey, - value: jsonString, - iOptions: IOSOptions( - groupId: "CN332ZUGRP.eu.twonly.shared", - synchronizable: false, - accessibility: KeychainAccessibility.first_unlock, - ), - ); -} - -final StreamController selectNotificationStream = - StreamController.broadcast(); - -@pragma('vm:entry-point') -void notificationTapBackground(NotificationResponse notificationResponse) { - // ignore: avoid_print - print('notification(${notificationResponse.id}) action tapped: ' - '${notificationResponse.actionId} with' - ' payload: ${notificationResponse.payload}'); - if (notificationResponse.input?.isNotEmpty ?? false) { - // ignore: avoid_print - print( - 'notification action tapped with input: ${notificationResponse.input}'); - } -} - -final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = - FlutterLocalNotificationsPlugin(); - -int id = 0; - -Future setupPushNotification() async { - const AndroidInitializationSettings initializationSettingsAndroid = - AndroidInitializationSettings("ic_launcher_foreground"); - - final List darwinNotificationCategories = - []; - - /// Note: permissions aren't requested here just to demonstrate that can be - /// done later - final DarwinInitializationSettings initializationSettingsDarwin = - DarwinInitializationSettings( - requestAlertPermission: true, - requestBadgePermission: true, - requestSoundPermission: true, - requestProvisionalPermission: false, - notificationCategories: darwinNotificationCategories, - ); - - final InitializationSettings initializationSettings = InitializationSettings( - android: initializationSettingsAndroid, - iOS: initializationSettingsDarwin, - ); - - await flutterLocalNotificationsPlugin.initialize( - initializationSettings, - onDidReceiveNotificationResponse: selectNotificationStream.add, - onDidReceiveBackgroundNotificationResponse: notificationTapBackground, - ); -} - -Future createPushAvatars() async { - if (!Platform.isAndroid) { - return; // avatars currently only shown in Android... - } - final contacts = await twonlyDB.contactsDao.getAllNotBlockedContacts(); - - for (final contact in contacts) { - if (contact.avatarSvg == null) return null; - - final PictureInfo pictureInfo = - await vg.loadPicture(SvgStringLoader(contact.avatarSvg!), null); - - 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 getApplicationCacheDirectory(); - final avatarsDirectory = Directory('${directory.path}/avatars'); - - // Create the avatars directory if it does not exist - if (!await avatarsDirectory.exists()) { - await avatarsDirectory.create(recursive: true); - } - final filePath = '${avatarsDirectory.path}/${contact.userId}.png'; - await File(filePath).writeAsBytes(pngBytes); - pictureInfo.picture.dispose(); - } -} diff --git a/lib/src/services/notification.background.service.dart b/lib/src/services/notifications/background.notifications.dart similarity index 65% rename from lib/src/services/notification.background.service.dart rename to lib/src/services/notifications/background.notifications.dart index 1cd1da0..b19c429 100644 --- a/lib/src/services/notification.background.service.dart +++ b/lib/src/services/notifications/background.notifications.dart @@ -5,7 +5,9 @@ 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/constants/secure_storage_keys.dart'; +import 'package:twonly/src/model/protobuf/push_notification/push_notification.pb.dart'; +import 'package:twonly/src/services/notifications/pushkeys.notifications.dart'; import 'package:twonly/src/utils/log.dart'; final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = @@ -24,7 +26,9 @@ Future customLocalPushNotification(String title, String msg) async { const DarwinNotificationDetails darwinNotificationDetails = DarwinNotificationDetails(); const NotificationDetails notificationDetails = NotificationDetails( - android: androidNotificationDetails, iOS: darwinNotificationDetails); + android: androidNotificationDetails, + iOS: darwinNotificationDetails, + ); await flutterLocalNotificationsPlugin.show( 999999 + Random.secure().nextInt(9999), @@ -34,67 +38,79 @@ Future customLocalPushNotification(String title, String msg) async { ); } -Future handlePushData(String pushDataJson) async { +Future handlePushData(String pushDataB64) async { try { - String jsonString = utf8.decode(base64.decode(pushDataJson)); - final pushData = PushNotification.fromJson(jsonDecode(jsonString)); + final pushData = + EncryptedPushNotification.fromBuffer(base64.decode(pushDataB64)); - PushKind? pushKind; - PushUser? pushUser; - int? fromUserId; + PushNotification? pushNotification; + PushUser? foundPushUser; if (pushData.keyId == 0) { List key = "InsecureOnlyUsedForAddingContact".codeUnits; - pushKind = await tryDecryptMessage(key, pushData); + pushNotification = await tryDecryptMessage(key, pushData); } else { - var pushKeys = await getPushKeys("receivingPushKeys"); - for (final userId in pushKeys.keys) { - for (final key in pushKeys[userId]!.keys) { + final pushUsers = await getPushKeys(SecureStorageKeys.receivingPushKeys); + for (final pushUser in pushUsers) { + for (final key in pushUser.pushKeys) { if (key.id == pushData.keyId) { - pushKind = await tryDecryptMessage(key.key, pushData); - if (pushKind != null) { - pushUser = pushKeys[userId]!; - fromUserId = userId; + pushNotification = await tryDecryptMessage(key.key, pushData); + if (pushNotification != null) { + foundPushUser = pushUser; break; } } } // found correct key and user - if (pushUser != null) break; + if (foundPushUser != null) break; } } - if (pushKind != null) { - if (pushKind == PushKind.testNotification) { + if (pushNotification != null) { + if (pushNotification.kind == PushKind.testNotification) { await customLocalPushNotification( - "Test notification", "This is a test notification."); - } else if (pushUser != null && fromUserId != null) { - await showLocalPushNotification(pushUser, fromUserId, pushKind); + "Test notification", + "This is a test notification.", + ); + } else if (foundPushUser != null) { + if (pushNotification.hasMessageId()) { + if (pushNotification.messageId <= foundPushUser.lastMessageId) { + Log.info( + "Got a push notification for a message which was already opened.", + ); + return; + } + } + + await showLocalPushNotification(foundPushUser, pushNotification); } else { - await showLocalPushNotificationWithoutUserId(pushKind); + await showLocalPushNotificationWithoutUserId(pushNotification); } } } catch (e) { + await customLocalPushNotification( + "Du hast eine neue Nachricht.", + "Öffne twonly um mehr zu erfahren.", + ); Log.error(e); } } -Future tryDecryptMessage( - List key, PushNotification noti) async { +Future tryDecryptMessage( + List key, EncryptedPushNotification push) async { try { final chacha20 = Chacha20.poly1305Aead(); SecretKeyData secretKeyData = SecretKeyData(key); SecretBox secretBox = SecretBox( - noti.cipherText, - nonce: noti.nonce, - mac: Mac(noti.mac), + push.ciphertext, + nonce: push.nonce, + mac: Mac(push.mac), ); final plaintext = await chacha20.decrypt(secretBox, secretKey: secretKeyData); - final plaintextString = utf8.decode(plaintext); - return PushKindExtension.fromString(plaintextString); + return PushNotification.fromBuffer(plaintext); } catch (e) { // this error is allowed to happen... return null; @@ -103,8 +119,7 @@ Future tryDecryptMessage( Future showLocalPushNotification( PushUser pushUser, - int fromUserId, - PushKind pushKind, + PushNotification pushNotification, ) async { String? title; String? body; @@ -116,56 +131,64 @@ Future showLocalPushNotification( } title = pushUser.displayName; - body = getPushNotificationText(pushKind); + body = getPushNotificationText(pushNotification); if (body == "") { Log.error("No push notification type defined!"); } FilePathAndroidBitmap? styleInformation; - String? avatarPath = await getAvatarIcon(fromUserId); + String? avatarPath = await getAvatarIcon(pushUser.userId.toInt()); 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); + 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); + android: androidNotificationDetails, + iOS: darwinNotificationDetails, + ); await flutterLocalNotificationsPlugin.show( - fromUserId, + pushUser.userId.toInt(), title, body, notificationDetails, - payload: pushKind.name, + payload: pushNotification.kind.name, ); } Future showLocalPushNotificationWithoutUserId( - PushKind pushKind, + PushNotification pushNotification, ) async { String? title; String? body; - body = getPushNotificationTextWithoutUserId(pushKind); + body = getPushNotificationTextWithoutUserId(pushNotification.kind); 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.'); + AndroidNotificationDetails( + '0', + 'Messages', + channelDescription: 'Messages from other users.', + importance: Importance.max, + priority: Priority.max, + ticker: 'You got a new message.', + ); const DarwinNotificationDetails darwinNotificationDetails = DarwinNotificationDetails(); @@ -177,7 +200,7 @@ Future showLocalPushNotificationWithoutUserId( title, body, notificationDetails, - payload: pushKind.name, + payload: pushNotification.kind.name, ); } @@ -240,7 +263,7 @@ String getPushNotificationTextWithoutUserId(PushKind pushKind) { return pushNotificationText[pushKind.name] ?? ""; } -String getPushNotificationText(PushKind pushKind) { +String getPushNotificationText(PushNotification pushNotification) { String systemLanguage = Platform.localeName; Map pushNotificationText; @@ -256,9 +279,12 @@ String getPushNotificationText(PushKind pushKind) { 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.reactionToVideo.name: + "hat mit {{reaction}} auf dein Video reagiert.", + PushKind.reactionToText.name: + "hat mit {{reaction}} auf deine Nachricht reagiert.", + PushKind.reactionToImage.name: + "hat mit {{reaction}} auf dein Bild reagiert.", PushKind.response.name: "hat dir geantwortet.", }; } else { @@ -272,11 +298,19 @@ String getPushNotificationText(PushKind pushKind) { 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.reactionToVideo.name: + "has reacted with {{reaction}} to your video.", + PushKind.reactionToText.name: + "has reacted with {{reaction}} to your message.", + PushKind.reactionToImage.name: + "has reacted with {{reaction}} to your image.", PushKind.response.name: "has responded.", }; } - return pushNotificationText[pushKind.name] ?? ""; + var contentText = pushNotificationText[pushNotification.kind.name] ?? ""; + if (pushNotification.hasReactionContent()) { + contentText = contentText.replaceAll( + "{{reaction}}", pushNotification.reactionContent); + } + return contentText; } diff --git a/lib/src/services/notifications/pushkeys.notifications.dart b/lib/src/services/notifications/pushkeys.notifications.dart new file mode 100644 index 0000000..4ae9a5f --- /dev/null +++ b/lib/src/services/notifications/pushkeys.notifications.dart @@ -0,0 +1,263 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:math'; +import 'package:collection/collection.dart'; +import 'package:cryptography_plus/cryptography_plus.dart'; +import 'package:fixnum/fixnum.dart'; +import 'package:flutter/services.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/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/model/json/message.dart' as my; +import 'package:twonly/src/model/protobuf/push_notification/push_notification.pb.dart'; +import 'package:twonly/src/services/api/messages.dart'; +import 'package:twonly/src/utils/log.dart'; + +/// This function must be called after the database is setup +Future setupNotificationWithUsers({bool force = false}) async { + var pushUsers = await getPushKeys(SecureStorageKeys.receivingPushKeys); + + var wasChanged = false; + + final random = Random.secure(); + + final contacts = await twonlyDB.contactsDao.getAllNotBlockedContacts(); + for (final contact in contacts) { + PushUser? pushUser = + pushUsers.firstWhereOrNull((x) => x.userId == contact.userId); + + if (pushUser != null) { + // make it harder to predict the change of the key + final timeBefore = + DateTime.now().subtract(Duration(days: 5 + random.nextInt(5))); + final lastKey = pushUser.pushKeys.last; + final createdAt = DateTime.fromMillisecondsSinceEpoch( + lastKey.createdAtUnixTimestamp.toInt()); + + if (force || createdAt.isBefore(timeBefore)) { + final pushKey = PushKey( + id: lastKey.id + random.nextInt(5), + key: List.generate(32, (index) => random.nextInt(256)), + createdAtUnixTimestamp: Int64(DateTime.now().millisecondsSinceEpoch), + ); + await sendNewPushKey(contact.userId, pushKey); + // only store a maximum of two keys + pushUser.pushKeys.clear(); + pushUser.pushKeys.add(lastKey); + pushUser.pushKeys.add(pushKey); + wasChanged = true; + } + } else { + wasChanged = true; + + /// Insert a new push user + final pushKey = PushKey( + id: Int64(1), + key: List.generate(32, (index) => random.nextInt(256)), + createdAtUnixTimestamp: Int64(DateTime.now().millisecondsSinceEpoch), + ); + await sendNewPushKey(contact.userId, pushKey); + pushUsers.add(PushUser( + displayName: getContactDisplayName(contact), + blocked: contact.blocked, + pushKeys: [pushKey], + lastMessageId: null, + )); + } + } + + if (wasChanged) { + await setPushKeys(SecureStorageKeys.receivingPushKeys, pushUsers); + } +} + +Future sendNewPushKey(int userId, PushKey pushKey) async { + await encryptAndSendMessageAsync( + null, + userId, + my.MessageJson( + kind: MessageKind.pushKey, + content: my.PushKeyContent( + keyId: pushKey.id.toInt(), + key: pushKey.key, + ), + timestamp: DateTime.fromMillisecondsSinceEpoch( + pushKey.createdAtUnixTimestamp.toInt(), + ), + ), + ); +} + +Future updatePushUser(Contact contact) async { + var pushKeys = await getPushKeys(SecureStorageKeys.receivingPushKeys); + + PushUser? pushUser = + pushKeys.firstWhereOrNull((x) => x.userId == contact.userId); + + if (pushUser == null) { + pushKeys.add(PushUser( + displayName: getContactDisplayName(contact), + pushKeys: [], + blocked: contact.blocked, + lastMessageId: Int64(0), + )); + } else { + pushUser.displayName = getContactDisplayName(contact); + pushUser.blocked = contact.blocked; + } + + await setPushKeys(SecureStorageKeys.receivingPushKeys, pushKeys); +} + +Future handleNewPushKey(int fromUserId, my.PushKeyContent pushKey) async { + var pushKeys = await getPushKeys(SecureStorageKeys.sendingPushKeys); + + PushUser? pushUser = pushKeys.firstWhereOrNull((x) => x.userId == fromUserId); + + if (pushUser == null) { + final contact = await twonlyDB.contactsDao + .getContactByUserId(fromUserId) + .getSingleOrNull(); + if (contact == null) return; + pushKeys.add(PushUser( + userId: Int64(fromUserId), + displayName: getContactDisplayName(contact), + pushKeys: [], + blocked: contact.blocked, + lastMessageId: Int64(0), + )); + pushUser = pushKeys.firstWhereOrNull((x) => x.userId == fromUserId); + } + + if (pushUser == null) { + Log.error("could not store new push key as no user was found"); + } + + // only store the newest key... + pushUser!.pushKeys.clear(); + pushUser.pushKeys.add( + PushKey( + id: Int64(pushKey.keyId), + key: pushKey.key, + createdAtUnixTimestamp: Int64(DateTime.now().millisecondsSinceEpoch), + ), + ); + + await setPushKeys(SecureStorageKeys.sendingPushKeys, pushKeys); +} + +Future updateLastMessageId(int fromUserId, int messageId) async { + List pushUsers = + await getPushKeys(SecureStorageKeys.receivingPushKeys); + + PushUser? pushUser = + pushUsers.firstWhereOrNull((x) => x.userId == fromUserId); + if (pushUser == null) { + setupNotificationWithUsers(); + return; + } + + if (pushUser.lastMessageId < Int64(messageId)) { + pushUser.lastMessageId = Int64(messageId); + await setPushKeys(SecureStorageKeys.receivingPushKeys, pushUsers); + } +} + +/// this will trigger a push notification +/// push notification only containing the message kind and username +Future getPushData(int toUserId, PushNotification content) async { + final List pushKeys = + await getPushKeys(SecureStorageKeys.sendingPushKeys); + + List key = "InsecureOnlyUsedForAddingContact".codeUnits; + int keyId = 0; + + PushUser? pushUser = pushKeys.firstWhereOrNull((x) => x.userId == toUserId); + + if (pushUser == null) { + // user does not have send any push keys + // only allow accept request and contact request to be send in an insecure way :/ + // In future find a better way, e.g. use the signal protocol in a native way.. + if (content.kind != PushKind.acceptRequest && + content.kind != PushKind.contactRequest && + content.kind != PushKind.testNotification) { + // this will be enforced after every app uses this system... :/ + // return null; + Log.error("Using insecure key as the receiver does not send a push key!"); + await encryptAndSendMessageAsync( + null, + toUserId, + my.MessageJson( + kind: MessageKind.requestPushKey, + content: my.MessageContent(), + timestamp: DateTime.now(), + ), + ); + } + } else { + try { + key = pushUser.pushKeys.last.key; + keyId = pushUser.pushKeys.last.id.toInt(); + } catch (e) { + Log.error("No push notification key found for user $toUserId"); + return null; + } + } + + final chacha20 = Chacha20.poly1305Aead(); + final nonce = chacha20.newNonce(); + final secretBox = await chacha20.encrypt( + content.writeToBuffer(), + secretKey: SecretKeyData(key), + nonce: nonce, + ); + final res = EncryptedPushNotification( + keyId: Int64(keyId), + nonce: nonce, + ciphertext: secretBox.cipherText, + mac: secretBox.mac.bytes, + ); + return res.writeToBuffer(); +} + +Future> getPushKeys(String storageKey) async { + var storage = FlutterSecureStorage(); + String? pushKeysProto = await storage.read( + key: storageKey, + iOptions: IOSOptions( + groupId: "CN332ZUGRP.eu.twonly.shared", + synchronizable: false, + accessibility: KeychainAccessibility.first_unlock, + ), + ); + if (pushKeysProto == null) return []; + Uint8List pushKeysRaw = base64Decode(pushKeysProto); + return PushUsers.fromBuffer(pushKeysRaw).users; +} + +Future setPushKeys(String storageKey, List pushKeys) async { + var storage = FlutterSecureStorage(); + + await storage.delete( + key: storageKey, + iOptions: IOSOptions( + groupId: "CN332ZUGRP.eu.twonly.shared", + synchronizable: false, + accessibility: KeychainAccessibility.first_unlock, + ), + ); + + String jsonString = base64Encode(PushUsers(users: pushKeys).writeToBuffer()); + await storage.write( + key: storageKey, + value: jsonString, + iOptions: IOSOptions( + groupId: "CN332ZUGRP.eu.twonly.shared", + synchronizable: false, + accessibility: KeychainAccessibility.first_unlock, + ), + ); +} diff --git a/lib/src/services/notifications/setup.notifications.dart b/lib/src/services/notifications/setup.notifications.dart new file mode 100644 index 0000000..9a0d73b --- /dev/null +++ b/lib/src/services/notifications/setup.notifications.dart @@ -0,0 +1,91 @@ +import 'dart:async'; +import 'dart:io'; +import 'dart:ui' as ui; +import 'package:flutter/services.dart'; +import 'package:flutter_local_notifications/flutter_local_notifications.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:twonly/globals.dart'; + +final StreamController selectNotificationStream = + StreamController.broadcast(); + +@pragma('vm:entry-point') +void notificationTapBackground(NotificationResponse notificationResponse) { + // ignore: avoid_print + print('notification(${notificationResponse.id}) action tapped: ' + '${notificationResponse.actionId} with' + ' payload: ${notificationResponse.payload}'); + if (notificationResponse.input?.isNotEmpty ?? false) { + // ignore: avoid_print + print( + 'notification action tapped with input: ${notificationResponse.input}'); + } +} + +final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = + FlutterLocalNotificationsPlugin(); + +int id = 0; + +Future setupPushNotification() async { + const AndroidInitializationSettings initializationSettingsAndroid = + AndroidInitializationSettings("ic_launcher_foreground"); + + final List darwinNotificationCategories = + []; + + /// Note: permissions aren't requested here just to demonstrate that can be + /// done later + final DarwinInitializationSettings initializationSettingsDarwin = + DarwinInitializationSettings( + requestAlertPermission: true, + requestBadgePermission: true, + requestSoundPermission: true, + requestProvisionalPermission: false, + notificationCategories: darwinNotificationCategories, + ); + + final InitializationSettings initializationSettings = InitializationSettings( + android: initializationSettingsAndroid, + iOS: initializationSettingsDarwin, + ); + + await flutterLocalNotificationsPlugin.initialize( + initializationSettings, + onDidReceiveNotificationResponse: selectNotificationStream.add, + onDidReceiveBackgroundNotificationResponse: notificationTapBackground, + ); +} + +Future createPushAvatars() async { + if (!Platform.isAndroid) { + return; // avatars currently only shown in Android... + } + final contacts = await twonlyDB.contactsDao.getAllNotBlockedContacts(); + + for (final contact in contacts) { + if (contact.avatarSvg == null) return null; + + final PictureInfo pictureInfo = + await vg.loadPicture(SvgStringLoader(contact.avatarSvg!), null); + + 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 getApplicationCacheDirectory(); + final avatarsDirectory = Directory('${directory.path}/avatars'); + + // Create the avatars directory if it does not exist + if (!await avatarsDirectory.exists()) { + await avatarsDirectory.create(recursive: true); + } + final filePath = '${avatarsDirectory.path}/${contact.userId}.png'; + await File(filePath).writeAsBytes(pngBytes); + pictureInfo.picture.dispose(); + } +} diff --git a/lib/src/views/chats/add_new_user.view.dart b/lib/src/views/chats/add_new_user.view.dart index 2c64185..77e2292 100644 --- a/lib/src/views/chats/add_new_user.view.dart +++ b/lib/src/views/chats/add_new_user.view.dart @@ -4,14 +4,15 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:provider/provider.dart'; +import 'package:twonly/src/model/protobuf/push_notification/push_notification.pbserver.dart'; import 'package:twonly/src/providers/connection.provider.dart'; import 'package:twonly/src/services/api/utils.dart'; +import 'package:twonly/src/services/notifications/pushkeys.notifications.dart'; import 'package:twonly/src/services/signal/session.signal.dart'; import 'package:twonly/src/views/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/views/components/headline.dart'; @@ -103,7 +104,7 @@ class _SearchUsernameView extends State { timestamp: DateTime.now(), content: MessageContent(), ), - pushKind: PushKind.contactRequest, + pushNotification: PushNotification(kind: PushKind.contactRequest), ); } } @@ -281,7 +282,7 @@ class _ContactsListViewState extends State { timestamp: DateTime.now(), content: MessageContent(), ), - pushKind: PushKind.acceptRequest, + pushNotification: PushNotification(kind: PushKind.acceptRequest), ); notifyContactsAboutProfileChange(); }, diff --git a/lib/src/views/chats/chat_messages.view.dart b/lib/src/views/chats/chat_messages.view.dart index 2b6603f..8153c53 100644 --- a/lib/src/views/chats/chat_messages.view.dart +++ b/lib/src/views/chats/chat_messages.view.dart @@ -5,6 +5,8 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:twonly/globals.dart'; +import 'package:twonly/src/model/protobuf/push_notification/push_notification.pb.dart'; +import 'package:twonly/src/services/notifications/background.notifications.dart'; import 'package:twonly/src/views/chats/chat_messages_components/chat_message_entry.dart'; import 'package:twonly/src/views/components/animate_icon.dart'; import 'package:twonly/src/views/components/initialsavatar.dart'; @@ -14,7 +16,7 @@ import 'package:twonly/src/database/twonly_database.dart'; import 'package:twonly/src/database/tables/messages_table.dart'; import 'package:twonly/src/model/json/message.dart'; import 'package:twonly/src/services/api/messages.dart'; -import 'package:twonly/src/services/notification.service.dart'; + import 'package:twonly/src/views/camera/camera_send_to_view.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/views/contact/contact.view.dart'; @@ -174,6 +176,7 @@ class _ChatMessagesViewState extends State { Future _sendMessage() async { if (newMessageController.text == "") return; + await sendTextMessage( user.userId, TextMessageContent( @@ -181,7 +184,16 @@ class _ChatMessagesViewState extends State { responseToMessageId: responseToMessage?.messageOtherId, responseToOtherMessageId: responseToMessage?.messageId, ), - (responseToMessage == null) ? PushKind.text : PushKind.response, + PushNotification( + kind: (responseToMessage == null) + ? PushKind.text + : (isEmoji(newMessageController.text)) + ? PushKind.reaction + : PushKind.response, + reactionContent: (isEmoji(newMessageController.text)) + ? newMessageController.text + : null, + ), ); newMessageController.clear(); currentInputText = ""; diff --git a/lib/src/views/chats/chat_messages_components/chat_media_entry.dart b/lib/src/views/chats/chat_messages_components/chat_media_entry.dart index ea3e0fd..8e46bc5 100644 --- a/lib/src/views/chats/chat_messages_components/chat_media_entry.dart +++ b/lib/src/views/chats/chat_messages_components/chat_media_entry.dart @@ -1,13 +1,14 @@ import 'package:drift/drift.dart' show Value; import 'package:flutter/material.dart'; import 'package:twonly/globals.dart'; +import 'package:twonly/src/model/protobuf/push_notification/push_notification.pbserver.dart'; import 'package:twonly/src/views/chats/chat_messages_components/in_chat_media_viewer.dart'; import 'package:twonly/src/database/twonly_database.dart'; import 'package:twonly/src/database/tables/messages_table.dart'; import 'package:twonly/src/model/json/message.dart'; import 'package:twonly/src/services/api/messages.dart'; import 'package:twonly/src/services/api/media_download.dart' as received; -import 'package:twonly/src/services/notification.service.dart'; + import 'package:twonly/src/views/chats/media_viewer.view.dart'; import 'package:twonly/src/model/memory_item.model.dart'; import 'package:twonly/src/views/tutorial/tutorials.dart'; @@ -80,7 +81,9 @@ class _ChatMediaEntryState extends State { ), timestamp: DateTime.now(), ), - pushKind: PushKind.reopenedMedia, + pushNotification: PushNotification( + kind: PushKind.reopenedMedia, + ), ); await twonlyDB.messagesDao.updateMessageByMessageId( widget.message.messageId, diff --git a/lib/src/views/chats/media_viewer.view.dart b/lib/src/views/chats/media_viewer.view.dart index 9835a36..a30cf63 100644 --- a/lib/src/views/chats/media_viewer.view.dart +++ b/lib/src/views/chats/media_viewer.view.dart @@ -8,7 +8,9 @@ import 'package:lottie/lottie.dart'; import 'package:no_screenshot/no_screenshot.dart'; import 'package:twonly/globals.dart'; import 'package:twonly/src/database/daos/contacts_dao.dart'; +import 'package:twonly/src/model/protobuf/push_notification/push_notification.pb.dart'; import 'package:twonly/src/services/api/utils.dart'; +import 'package:twonly/src/services/notifications/background.notifications.dart'; import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/views/camera/share_image_editor_view.dart'; import 'package:twonly/src/views/components/animate_icon.dart'; @@ -18,7 +20,6 @@ import 'package:twonly/src/database/tables/messages_table.dart'; import 'package:twonly/src/model/json/message.dart'; import 'package:twonly/src/services/api/messages.dart'; import 'package:twonly/src/services/api/media_download.dart'; -import 'package:twonly/src/services/notification.service.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/storage.dart'; import 'package:twonly/src/views/camera/camera_send_to_view.dart'; @@ -314,7 +315,7 @@ class _MediaViewerViewState extends State { ), timestamp: DateTime.now(), ), - pushKind: PushKind.storedMediaFile, + pushNotification: PushNotification(kind: PushKind.acceptRequest), ); setState(() { imageSaved = true; @@ -640,7 +641,9 @@ class _MediaViewerViewState extends State { responseToMessageId: allMediaFiles.first.messageOtherId, ), - PushKind.reaction, + PushNotification( + kind: PushKind.response, + ), ); textMessageController.clear(); } @@ -817,14 +820,18 @@ class _EmojiReactionWidgetState extends State { child: GestureDetector( onTap: () { sendTextMessage( - widget.userId, - TextMessageContent( - text: widget.emoji, - responseToMessageId: widget.responseToMessageId, - ), - widget.isVideo + widget.userId, + TextMessageContent( + text: widget.emoji, + responseToMessageId: widget.responseToMessageId, + ), + PushNotification( + kind: widget.isVideo ? PushKind.reactionToVideo - : PushKind.reactionToImage); + : PushKind.reactionToImage, + reactionContent: widget.emoji, + ), + ); setState(() { selectedShortReaction = 0; // Assuming index is 0 for this example }); diff --git a/lib/src/views/components/message_send_state_icon.dart b/lib/src/views/components/message_send_state_icon.dart index d78ae97..9dc4db7 100644 --- a/lib/src/views/components/message_send_state_icon.dart +++ b/lib/src/views/components/message_send_state_icon.dart @@ -6,6 +6,7 @@ import 'package:twonly/src/database/twonly_database.dart'; import 'package:twonly/src/database/tables/messages_table.dart'; import 'package:twonly/src/model/json/message.dart'; import 'package:twonly/src/utils/misc.dart'; +import 'package:twonly/src/views/components/animate_icon.dart'; enum MessageSendState { received, @@ -89,11 +90,12 @@ class _MessageSendStateIconState extends State { MessageSendState state = messageSendStateFromMessage(message); late Color color; + MessageContent? content; if (message.contentJson == null) { color = getMessageColorFromType(TextMessageContent(text: ""), context); } else { - MessageContent? content = MessageContent.fromJson( + content = MessageContent.fromJson( message.kind, jsonDecode(message.contentJson!), ); @@ -106,6 +108,11 @@ class _MessageSendStateIconState extends State { switch (state) { case MessageSendState.receivedOpened: icon = Icon(Icons.crop_square, size: 14, color: color); + if (content is TextMessageContent) { + if (isEmoji(content.text)) { + icon = Text(content.text, style: TextStyle(fontSize: 12)); + } + } text = context.lang.messageSendState_Received; break; case MessageSendState.sendOpened: diff --git a/lib/src/views/home.view.dart b/lib/src/views/home.view.dart index 9399db6..f7af20d 100644 --- a/lib/src/views/home.view.dart +++ b/lib/src/views/home.view.dart @@ -4,10 +4,10 @@ import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:pie_menu/pie_menu.dart'; import 'package:screenshot/screenshot.dart'; +import 'package:twonly/src/services/notifications/setup.notifications.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/views/camera/camera_preview_components/camera_preview.dart'; import 'package:twonly/src/views/components/user_context_menu.dart'; -import 'package:twonly/src/services/notification.service.dart'; import 'package:twonly/src/views/memories/memories.view.dart'; import 'camera/camera_preview_controller_view.dart'; import 'chats/chat_list.view.dart'; diff --git a/lib/src/views/settings/notification.view.dart b/lib/src/views/settings/notification.view.dart index e2f54d1..6e64537 100644 --- a/lib/src/views/settings/notification.view.dart +++ b/lib/src/views/settings/notification.view.dart @@ -1,13 +1,15 @@ import 'dart:io'; +import 'package:fixnum/fixnum.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.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/model/protobuf/push_notification/push_notification.pbserver.dart'; +import 'package:twonly/src/services/notifications/pushkeys.notifications.dart'; import 'package:twonly/src/views/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'; @@ -50,7 +52,10 @@ class NotificationView extends StatelessWidget { if (user != null) { final pushData = await getPushData( user.userId, - PushKind.testNotification, + PushNotification( + messageId: Int64(0), + kind: PushKind.testNotification, + ), ); await apiService.sendTextMessage( user.userId,