#213 for android finished

This commit is contained in:
otsmr 2025-06-22 01:25:04 +02:00
parent 52280c55de
commit 3b777f7130
32 changed files with 2100 additions and 914 deletions

View file

@ -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/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/" SRC_DIR="../twonly-server/twonly/src/"

View file

@ -5,419 +5,417 @@
// Created by Tobi on 03.04.25. // Created by Tobi on 03.04.25.
// //
import UserNotifications // import UserNotifications
import CryptoKit // import CryptoKit
import Foundation // import Foundation
class NotificationService: UNNotificationServiceExtension { // class NotificationService: UNNotificationServiceExtension {
var contentHandler: ((UNNotificationContent) -> Void)? // var contentHandler: ((UNNotificationContent) -> Void)?
var bestAttemptContent: UNMutableNotificationContent? // var bestAttemptContent: UNMutableNotificationContent?
override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) { // override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {
self.contentHandler = contentHandler // self.contentHandler = contentHandler
bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent) // bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent)
if let bestAttemptContent = bestAttemptContent { // if let bestAttemptContent = bestAttemptContent {
guard let _ = bestAttemptContent.userInfo as? [String: Any], // guard let _ = bestAttemptContent.userInfo as? [String: Any],
let push_data = bestAttemptContent.userInfo["push_data"] as? String else { // let push_data = bestAttemptContent.userInfo["push_data"] as? String else {
return contentHandler(bestAttemptContent); // return contentHandler(bestAttemptContent);
} // }
let data = getPushNotificationData(pushDataJson: push_data) // let data = getPushNotificationData(pushDataJson: push_data)
if data != nil { // if data != nil {
if data!.title == "blocked" { // if data!.title == "blocked" {
NSLog("Block message because user is blocked!") // NSLog("Block message because user is blocked!")
// https://developer.apple.com/documentation/bundleresources/entitlements/com.apple.developer.usernotifications.filtering // // https://developer.apple.com/documentation/bundleresources/entitlements/com.apple.developer.usernotifications.filtering
return contentHandler(UNNotificationContent()) // return contentHandler(UNNotificationContent())
} // }
bestAttemptContent.title = data!.title; // bestAttemptContent.title = data!.title;
bestAttemptContent.body = data!.body; // bestAttemptContent.body = data!.body;
bestAttemptContent.threadIdentifier = String(format: "%d", data!.notificationId) // bestAttemptContent.threadIdentifier = String(format: "%d", data!.notificationId)
} else { // } else {
bestAttemptContent.title = "\(bestAttemptContent.title)" // bestAttemptContent.title = "\(bestAttemptContent.title)"
} // }
contentHandler(bestAttemptContent) // contentHandler(bestAttemptContent)
} // }
} // }
override func serviceExtensionTimeWillExpire() { // override func serviceExtensionTimeWillExpire() {
// Called just before the extension will be terminated by the system. // // 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. // // 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 { // if let contentHandler = contentHandler, let bestAttemptContent = bestAttemptContent {
contentHandler(bestAttemptContent) // contentHandler(bestAttemptContent)
} // }
} // }
} // }
// 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
// }
enum PushKind: String, Codable { // import CryptoKit
case text // import Foundation
case twonly // import Security
case video
case image
case contactRequest
case acceptRequest
case storedMediaFile
case reaction
case testNotification
case reopenedMedia
case reactionToVideo
case reactionToText
case reactionToImage
case response
}
import CryptoKit // func getPushNotificationData(pushDataJson: String) -> (title: String, body: String, notificationId: Int)? {
import Foundation // // Decode the pushDataJson
import Security // guard let pushData = decodePushData(pushDataJson) else {
// NSLog("Failed to decode push data")
// return nil
// }
func getPushNotificationData(pushDataJson: String) -> (title: String, body: String, notificationId: Int)? { // var pushKind: PushKind?
// Decode the pushDataJson // var displayName: String?
guard let pushData = decodePushData(pushDataJson) else { // var fromUserId: Int?
NSLog("Failed to decode push data") // var blocked: Bool?
return nil
}
var pushKind: PushKind? // // Check the keyId
var displayName: String? // if pushData.keyId == 0 {
var fromUserId: Int? // let key = "InsecureOnlyUsedForAddingContact".data(using: .utf8)!.map { Int($0) }
var blocked: Bool? // 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")
// }
// }
// Check the keyId // if blocked == true {
if pushData.keyId == 0 { // return ("blocked", "blocked", 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 { // // Handle the push notification based on the pushKind
return ("blocked", "blocked", 0) // if let pushKind = pushKind {
}
// Handle the push notification based on the pushKind // if pushKind == .testNotification {
if let pushKind = pushKind { // 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)
// }
if pushKind == .testNotification { // } else {
return ("Test Notification", "This is a test notification.", 0) // NSLog("Failed to decrypt message or pushKind is nil")
} else if displayName != nil && fromUserId != nil { // }
return (displayName!, getPushNotificationText(pushKind: pushKind), fromUserId!) // return nil
} else { // }
return ("", getPushNotificationTextWithoutUserId(pushKind: pushKind), 1)
}
} else { // func tryDecryptMessage(key: [Int], pushData: PushNotification) -> PushKind? {
NSLog("Failed to decrypt message or pushKind is nil") // // Convert the key from [Int] to Data
} // let keyData = Data(key.map { UInt8($0) }) // Convert Int to UInt8
return nil
}
func tryDecryptMessage(key: [Int], pushData: PushNotification) -> PushKind? { // guard let nonceData = Data(base64Encoded: pushData.nonce),
// Convert the key from [Int] to Data // let cipherTextData = Data(base64Encoded: pushData.cipherText),
let keyData = Data(key.map { UInt8($0) }) // Convert Int to UInt8 // let macData = Data(base64Encoded: pushData.mac) else {
// NSLog("Failed to decode base64 strings")
// return nil
// }
guard let nonceData = Data(base64Encoded: pushData.nonce), // do {
let cipherTextData = Data(base64Encoded: pushData.cipherText), // // Create a nonce for ChaChaPoly
let macData = Data(base64Encoded: pushData.mac) else { // let nonce = try ChaChaPoly.Nonce(data: nonceData)
NSLog("Failed to decode base64 strings")
return nil
}
do { // // Create a sealed box for ChaChaPoly
// Create a nonce for ChaChaPoly // let sealedBox = try ChaChaPoly.SealedBox(nonce: nonce, ciphertext: cipherTextData, tag: macData)
let nonce = try ChaChaPoly.Nonce(data: nonceData)
// Create a sealed box for ChaChaPoly // // Decrypt the data using the key
let sealedBox = try ChaChaPoly.SealedBox(nonce: nonce, ciphertext: cipherTextData, tag: macData) // let decryptedData = try ChaChaPoly.open(sealedBox, using: SymmetricKey(data: keyData))
// Decrypt the data using the key // // Convert decrypted data to a string
let decryptedData = try ChaChaPoly.open(sealedBox, using: SymmetricKey(data: keyData)) // if let decryptedMessage = String(data: decryptedData, encoding: .utf8) {
// NSLog("Decrypted message: \(decryptedMessage)")
// Convert decrypted data to a string // // Here you can determine the PushKind based on the decrypted message
if let decryptedMessage = String(data: decryptedData, encoding: .utf8) { // return determinePushKind(from: decryptedMessage)
NSLog("Decrypted message: \(decryptedMessage)") // }
// } catch {
// NSLog("Decryption failed: \(error)")
// }
// Here you can determine the PushKind based on the decrypted message // return nil
return determinePushKind(from: decryptedMessage) // }
}
} catch {
NSLog("Decryption failed: \(error)")
}
return nil // // 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
// }
// }
// Placeholder function to determine PushKind from the decrypted message // func decodePushData(_ json: String) -> PushNotification? {
func determinePushKind(from message: String) -> PushKind? { // // First, decode the base64 string
// Implement your logic to determine the PushKind based on the message content // guard let base64Data = Data(base64Encoded: json) else {
// For example, you might check for specific keywords or formats in the message // NSLog("Failed to decode base64 string")
// This is just a placeholder implementation // return nil
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
}
}
// // 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
// }
func decodePushData(_ json: String) -> PushNotification? { // // Convert the JSON string to Data
// First, decode the base64 string // guard let jsonData = jsonString.data(using: .utf8) else {
guard let base64Data = Data(base64Encoded: json) else { // NSLog("Failed to convert JSON string to Data")
NSLog("Failed to decode base64 string") // return nil
return nil // }
}
// Convert the base64 decoded data to a JSON string // do {
guard let jsonString = String(data: base64Data, encoding: .utf8) else { // // Use JSONDecoder to decode the JSON data into a PushNotification instance
NSLog("Failed to convert base64 data to JSON string") // let decoder = JSONDecoder()
return nil // let pushNotification = try decoder.decode(PushNotification.self, from: jsonData)
} // return pushNotification
// } catch {
// NSLog("Error decoding JSON: \(error)")
// return nil
// }
// }
// Convert the JSON string to Data // struct PushNotification: Codable {
guard let jsonData = jsonString.data(using: .utf8) else { // let keyId: Int
NSLog("Failed to convert JSON string to Data") // let nonce: String
return nil // let cipherText: String
} // let mac: String
do { // // You can add custom coding keys if the JSON keys differ from the property names
// Use JSONDecoder to decode the JSON data into a PushNotification instance // enum CodingKeys: String, CodingKey {
let decoder = JSONDecoder() // case keyId
let pushNotification = try decoder.decode(PushNotification.self, from: jsonData) // case nonce
return pushNotification // case cipherText
} catch { // case mac
NSLog("Error decoding JSON: \(error)") // }
return nil // }
}
}
struct PushNotification: Codable { // struct PushKeyMeta: Codable {
let keyId: Int // let id: Int
let nonce: String // let key: [Int]
let cipherText: String // let createdAt: Date
let mac: String
// You can add custom coding keys if the JSON keys differ from the property names // enum CodingKeys: String, CodingKey {
enum CodingKeys: String, CodingKey { // case id
case keyId // case key
case nonce // case createdAt
case cipherText // }
case mac // }
}
}
struct PushKeyMeta: Codable { // struct PushUser: Codable {
let id: Int // let displayName: String
let key: [Int] // let keys: [PushKeyMeta]
let createdAt: Date // let blocked: Bool?
enum CodingKeys: String, CodingKey { // enum CodingKeys: String, CodingKey {
case id // case displayName
case key // case keys
case createdAt // case blocked
} // }
} // }
struct PushUser: Codable { // func getPushKey() -> [Int: PushUser]? {
let displayName: String // // Retrieve the data from secure storage (Keychain)
let keys: [PushKeyMeta] // guard let data = readFromKeychain(key: "receivingPushKeys") else {
let blocked: Bool? // NSLog("No data found for key: receivingPushKeys")
// return nil
// }
enum CodingKeys: String, CodingKey { // do {
case displayName // // Decode the JSON data into a dictionary
case keys // let jsonData = data.data(using: .utf8)!
case blocked // let jsonMap = try JSONSerialization.jsonObject(with: jsonData, options: []) as? [String: Any]
}
}
func getPushKey() -> [Int: PushUser]? { // var pushKeys: [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 { // // Iterate through the JSON map and decode each PushUser
// Decode the JSON data into a dictionary // for (key, value) in jsonMap ?? [:] {
let jsonData = data.data(using: .utf8)! // if let userData = try? JSONSerialization.data(withJSONObject: value, options: []),
let jsonMap = try JSONSerialization.jsonObject(with: jsonData, options: []) as? [String: Any] // let pushUser = try? JSONDecoder().decode(PushUser.self, from: userData) {
// pushKeys[Int(key)!] = pushUser
// }
// }
var pushKeys: [Int: PushUser] = [:] // return pushKeys
// } catch {
// NSLog("Error decoding JSON: \(error)")
// return nil
// }
// }
// Iterate through the JSON map and decode each PushUser // // Helper function to read from Keychain
for (key, value) in jsonMap ?? [:] { // func readFromKeychain(key: String) -> String? {
if let userData = try? JSONSerialization.data(withJSONObject: value, options: []), // let query: [String: Any] = [
let pushUser = try? JSONDecoder().decode(PushUser.self, from: userData) { // kSecClass as String: kSecClassGenericPassword,
pushKeys[Int(key)!] = pushUser // kSecAttrAccount as String: key,
} // kSecReturnData as String: kCFBooleanTrue!,
} // kSecMatchLimit as String: kSecMatchLimitOne,
// kSecAttrAccessGroup as String: "CN332ZUGRP.eu.twonly.shared" // Use your access group
// ]
return pushKeys // var dataTypeRef: AnyObject? = nil
} catch { // let status: OSStatus = SecItemCopyMatching(query as CFDictionary, &dataTypeRef)
NSLog("Error decoding JSON: \(error)")
return nil
}
}
// Helper function to read from Keychain // if status == errSecSuccess {
func readFromKeychain(key: String) -> String? { // if let data = dataTypeRef as? Data {
let query: [String: Any] = [ // return String(data: data, encoding: .utf8)
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 // return nil
let status: OSStatus = SecItemCopyMatching(query as CFDictionary, &dataTypeRef) // }
if status == errSecSuccess { // func getPushNotificationText(pushKind: PushKind) -> String {
if let data = dataTypeRef as? Data { // let systemLanguage = Locale.current.languageCode ?? "en" // Get the current system language
return String(data: data, encoding: .utf8)
}
}
return nil // var pushNotificationText: [PushKind: String] = [:]
}
func getPushNotificationText(pushKind: PushKind) -> String { // // Define the messages based on the system language
let systemLanguage = Locale.current.languageCode ?? "en" // Get the current 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."
// ]
// }
var pushNotificationText: [PushKind: String] = [:] // // Return the corresponding message or an empty string if not found
// return pushNotificationText[pushKind] ?? ""
// }
// Define the messages based on the system language // func getPushNotificationTextWithoutUserId(pushKind: PushKind) -> String {
if systemLanguage.contains("de") { // German // let systemLanguage = Locale.current.languageCode ?? "en" // Get the current system language
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 // var pushNotificationText: [PushKind: String] = [:]
return pushNotificationText[pushKind] ?? ""
}
func getPushNotificationTextWithoutUserId(pushKind: PushKind) -> String { // // Define the messages based on the system language
let systemLanguage = Locale.current.languageCode ?? "en" // Get the current 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."
// ]
// }
var pushNotificationText: [PushKind: String] = [:] // // Return the corresponding message or an empty string if not found
// return pushNotificationText[pushKind] ?? ""
// 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] ?? ""
}

View file

@ -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<D: SwiftProtobuf.Decoder>(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<V: SwiftProtobuf.Visitor>(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<D: SwiftProtobuf.Decoder>(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<V: SwiftProtobuf.Visitor>(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<D: SwiftProtobuf.Decoder>(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<V: SwiftProtobuf.Visitor>(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<D: SwiftProtobuf.Decoder>(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<V: SwiftProtobuf.Visitor>(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<D: SwiftProtobuf.Decoder>(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<V: SwiftProtobuf.Visitor>(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
}
}

View file

@ -33,6 +33,7 @@ pod 'FirebaseMessaging', :modular_headers => true
pod 'FirebaseCoreInternal', :modular_headers => true pod 'FirebaseCoreInternal', :modular_headers => true
pod 'GoogleUtilities', :modular_headers => true pod 'GoogleUtilities', :modular_headers => true
pod 'FirebaseCore', :modular_headers => true pod 'FirebaseCore', :modular_headers => true
pod 'SwiftProtobuf'
# pod 'sqlite3', :modular_headers => true # pod 'sqlite3', :modular_headers => true

View file

@ -216,6 +216,7 @@ PODS:
- sqlite3/math - sqlite3/math
- sqlite3/perf-threadsafe - sqlite3/perf-threadsafe
- sqlite3/rtree - sqlite3/rtree
- SwiftProtobuf (1.30.0)
- url_launcher_ios (0.0.1): - url_launcher_ios (0.0.1):
- Flutter - Flutter
- video_compress (0.3.0): - video_compress (0.3.0):
@ -253,6 +254,7 @@ DEPENDENCIES:
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
- sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`) - sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`)
- sqlite3_flutter_libs (from `.symlinks/plugins/sqlite3_flutter_libs/darwin`) - sqlite3_flutter_libs (from `.symlinks/plugins/sqlite3_flutter_libs/darwin`)
- SwiftProtobuf
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
- video_compress (from `.symlinks/plugins/video_compress/ios`) - video_compress (from `.symlinks/plugins/video_compress/ios`)
- video_player_avfoundation (from `.symlinks/plugins/video_player_avfoundation/darwin`) - video_player_avfoundation (from `.symlinks/plugins/video_player_avfoundation/darwin`)
@ -276,6 +278,7 @@ SPEC REPOS:
- SDWebImage - SDWebImage
- SDWebImageWebPCoder - SDWebImageWebPCoder
- sqlite3 - sqlite3
- SwiftProtobuf
EXTERNAL SOURCES: EXTERNAL SOURCES:
background_downloader: background_downloader:
@ -372,10 +375,11 @@ SPEC CHECKSUMS:
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0 sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
sqlite3: 3c950dc86011117c307eb0b28c4a7bb449dce9f1 sqlite3: 3c950dc86011117c307eb0b28c4a7bb449dce9f1
sqlite3_flutter_libs: 74334e3ef2dbdb7d37e50859bb45da43935779c4 sqlite3_flutter_libs: 74334e3ef2dbdb7d37e50859bb45da43935779c4
SwiftProtobuf: 3697407f0d5b23bedeba9c2eaaf3ec6fdff69349
url_launcher_ios: 694010445543906933d732453a59da0a173ae33d url_launcher_ios: 694010445543906933d732453a59da0a173ae33d
video_compress: f2133a07762889d67f0711ac831faa26f956980e video_compress: f2133a07762889d67f0711ac831faa26f956980e
video_player_avfoundation: 2cef49524dd1f16c5300b9cd6efd9611ce03639b video_player_avfoundation: 2cef49524dd1f16c5300b9cd6efd9611ce03639b
PODFILE CHECKSUM: 3e94c12f4f6904137d1449e3b100fda499ccd32d PODFILE CHECKSUM: a01f0821a361ca6708e29b1299e8becf492a8a71
COCOAPODS: 1.16.2 COCOAPODS: 1.16.2

View file

@ -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/connection.provider.dart';
import 'package:twonly/src/providers/settings.provider.dart'; import 'package:twonly/src/providers/settings.provider.dart';
import 'package:twonly/src/services/api/media_upload.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/utils/storage.dart';
import 'package:twonly/src/views/onboarding/onboarding.view.dart'; import 'package:twonly/src/views/onboarding/onboarding.view.dart';
import 'package:twonly/src/views/home.view.dart'; import 'package:twonly/src/views/home.view.dart';
@ -36,7 +35,6 @@ class _AppState extends State<App> with WidgetsBindingObserver {
globalCallbackConnectionState = (update) { globalCallbackConnectionState = (update) {
context.read<CustomChangeProvider>().updateConnectionState(update); context.read<CustomChangeProvider>().updateConnectionState(update);
setUserPlan(); setUserPlan();
setupNotificationWithUsers();
}; };
initAsync(); initAsync();

View file

@ -11,7 +11,7 @@ import 'package:flutter/material.dart';
import 'package:twonly/src/providers/connection.provider.dart'; import 'package:twonly/src/providers/connection.provider.dart';
import 'package:twonly/src/providers/settings.provider.dart'; import 'package:twonly/src/providers/settings.provider.dart';
import 'package:twonly/src/services/fcm.service.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/services/twonly_safe/create_backup.twonly_safe.dart';
import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/storage.dart'; import 'package:twonly/src/utils/storage.dart';

View file

@ -5,4 +5,7 @@ class SecureStorageKeys {
static const String googleFcm = "google_fcm"; static const String googleFcm = "google_fcm";
static const String userData = "userData"; static const String userData = "userData";
static const String twonlySafeLastBackupHash = "twonly_safe_last_backup_hash"; static const String twonlySafeLastBackupHash = "twonly_safe_last_backup_hash";
static const String receivingPushKeys = "receiving_pus_keys";
static const String sendingPushKeys = "sending_pus_keys";
} }

View file

@ -1,7 +1,7 @@
import 'package:drift/drift.dart'; import 'package:drift/drift.dart';
import 'package:twonly/src/database/tables/contacts_table.dart'; import 'package:twonly/src/database/tables/contacts_table.dart';
import 'package:twonly/src/database/twonly_database.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'; part 'contacts_dao.g.dart';
@ -113,7 +113,11 @@ class ContactsDao extends DatabaseAccessor<TwonlyDatabase>
Future newMessageExchange(int userId) { Future newMessageExchange(int userId) {
return updateContact( return updateContact(
userId, ContactsCompanion(lastMessageExchange: Value(DateTime.now()))); userId,
ContactsCompanion(
lastMessageExchange: Value(DateTime.now()),
),
);
} }
Stream<List<Contact>> watchNotAcceptedContacts() { Stream<List<Contact>> watchNotAcceptedContacts() {

View file

@ -46,6 +46,7 @@ class ErrorCode extends $pb.ProtobufEnum {
static const ErrorCode InvalidSignedPreKey = ErrorCode._(1027, _omitEnumNames ? '' : 'InvalidSignedPreKey'); static const ErrorCode InvalidSignedPreKey = ErrorCode._(1027, _omitEnumNames ? '' : 'InvalidSignedPreKey');
static const ErrorCode UserIdNotFound = ErrorCode._(1028, _omitEnumNames ? '' : 'UserIdNotFound'); static const ErrorCode UserIdNotFound = ErrorCode._(1028, _omitEnumNames ? '' : 'UserIdNotFound');
static const ErrorCode UserIdAlreadyTaken = ErrorCode._(1029, _omitEnumNames ? '' : 'UserIdAlreadyTaken'); static const ErrorCode UserIdAlreadyTaken = ErrorCode._(1029, _omitEnumNames ? '' : 'UserIdAlreadyTaken');
static const ErrorCode AppVersionOutdated = ErrorCode._(1030, _omitEnumNames ? '' : 'AppVersionOutdated');
static const $core.List<ErrorCode> values = <ErrorCode> [ static const $core.List<ErrorCode> values = <ErrorCode> [
Unknown, Unknown,
@ -80,6 +81,7 @@ class ErrorCode extends $pb.ProtobufEnum {
InvalidSignedPreKey, InvalidSignedPreKey,
UserIdNotFound, UserIdNotFound,
UserIdAlreadyTaken, UserIdAlreadyTaken,
AppVersionOutdated,
]; ];
static final $core.Map<$core.int, ErrorCode> _byValue = $pb.ProtobufEnum.initByValue(values); static final $core.Map<$core.int, ErrorCode> _byValue = $pb.ProtobufEnum.initByValue(values);

View file

@ -49,6 +49,7 @@ const ErrorCode$json = {
{'1': 'InvalidSignedPreKey', '2': 1027}, {'1': 'InvalidSignedPreKey', '2': 1027},
{'1': 'UserIdNotFound', '2': 1028}, {'1': 'UserIdNotFound', '2': 1028},
{'1': 'UserIdAlreadyTaken', '2': 1029}, {'1': 'UserIdAlreadyTaken', '2': 1029},
{'1': 'AppVersionOutdated', '2': 1030},
], ],
}; };
@ -68,5 +69,5 @@ final $typed_data.Uint8List errorCodeDescriptor = $convert.base64Decode(
'dlZBD+BxIVChBQbGFuTGltaXRSZWFjaGVkEP8HEhQKD05vdEVub3VnaENyZWRpdBCACBISCg1Q' 'dlZBD+BxIVChBQbGFuTGltaXRSZWFjaGVkEP8HEhQKD05vdEVub3VnaENyZWRpdBCACBISCg1Q'
'bGFuRG93bmdyYWRlEIEIEhkKFFBsYW5VcGdyYWRlTm90WWVhcmx5EIIIEhgKE0ludmFsaWRTaW' 'bGFuRG93bmdyYWRlEIEIEhkKFFBsYW5VcGdyYWRlTm90WWVhcmx5EIIIEhgKE0ludmFsaWRTaW'
'duZWRQcmVLZXkQgwgSEwoOVXNlcklkTm90Rm91bmQQhAgSFwoSVXNlcklkQWxyZWFkeVRha2Vu' 'duZWRQcmVLZXkQgwgSEwoOVXNlcklkTm90Rm91bmQQhAgSFwoSVXNlcklkQWxyZWFkeVRha2Vu'
'EIUI'); 'EIUIEhcKEkFwcFZlcnNpb25PdXRkYXRlZBCGCA==');

View file

@ -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<EncryptedPushNotification> createRepeated() => $pb.PbList<EncryptedPushNotification>();
@$core.pragma('dart2js:noInline')
static EncryptedPushNotification getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<EncryptedPushNotification>(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<PushKind>(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<PushNotification> createRepeated() => $pb.PbList<PushNotification>();
@$core.pragma('dart2js:noInline')
static PushNotification getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<PushNotification>(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<PushUser>? 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<PushUser>(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<PushUsers> createRepeated() => $pb.PbList<PushUsers>();
@$core.pragma('dart2js:noInline')
static PushUsers getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<PushUsers>(create);
static PushUsers? _defaultInstance;
@$pb.TagNumber(1)
$core.List<PushUser> 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<PushKey>? 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<PushKey>(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<PushUser> createRepeated() => $pb.PbList<PushUser>();
@$core.pragma('dart2js:noInline')
static PushUser getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<PushUser>(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<PushKey> 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<PushKey> createRepeated() => $pb.PbList<PushKey>();
@$core.pragma('dart2js:noInline')
static PushKey getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<PushKey>(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');

View file

@ -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<PushKind> values = <PushKind> [
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');

View file

@ -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');

View file

@ -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';

View file

@ -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;
}

View file

@ -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_download.dart';
import 'package:twonly/src/services/api/media_upload.dart'; import 'package:twonly/src/services/api/media_upload.dart';
import 'package:twonly/src/services/api/server_messages.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/identity.signal.dart';
import 'package:twonly/src/services/signal/prekeys.signal.dart'; import 'package:twonly/src/services/signal/prekeys.signal.dart';
import 'package:twonly/src/services/signal/utils.signal.dart'; import 'package:twonly/src/services/signal/utils.signal.dart';
@ -92,6 +93,7 @@ class ApiService {
notifyContactsAboutProfileChange(); notifyContactsAboutProfileChange();
twonlyDB.markUpdated(); twonlyDB.markUpdated();
syncFlameCounters(); syncFlameCounters();
setupNotificationWithUsers();
signalHandleNewServerConnection(); signalHandleNewServerConnection();
} }
} }

View file

@ -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/json/message.dart';
import 'package:twonly/src/model/protobuf/api/http/http_requests.pb.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/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/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/signal/encryption.signal.dart';
import 'package:twonly/src/services/twonly_safe/create_backup.twonly_safe.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/log.dart';
@ -539,17 +540,24 @@ Future handleMediaUpload(MediaUpload media) async {
); );
if (encryptedBytes == null) continue; if (encryptedBytes == null) continue;
var messageOnSuccess = TextMessage()
..body = encryptedBytes
..userId = Int64(message.contactId);
final pushKind = (media.metadata!.isRealTwonly) final pushKind = (media.metadata!.isRealTwonly)
? PushKind.twonly ? PushKind.twonly
: (media.metadata!.isVideo) : (media.metadata!.isVideo)
? PushKind.video ? PushKind.video
: PushKind.image; : PushKind.image;
var messageOnSuccess = TextMessage() final pushData = await getPushData(
..body = encryptedBytes message.contactId,
..userId = Int64(message.contactId); PushNotification(
messageId: Int64(message.messageId),
var pushData = await getPushData(message.contactId, pushKind); kind: pushKind,
),
);
if (pushData != null) { if (pushData != null) {
messageOnSuccess.pushData = pushData.toList(); messageOnSuccess.pushData = pushData.toList();
} }

View file

@ -2,15 +2,17 @@ import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'package:drift/drift.dart'; import 'package:drift/drift.dart';
import 'package:fixnum/fixnum.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/globals.dart';
import 'package:twonly/src/database/twonly_database.dart'; import 'package:twonly/src/database/twonly_database.dart';
import 'package:twonly/src/database/tables/messages_table.dart'; import 'package:twonly/src/database/tables/messages_table.dart';
import 'package:twonly/src/model/json/message.dart'; import 'package:twonly/src/model/json/message.dart';
import 'package:twonly/src/model/json/userdata.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/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/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/signal/encryption.signal.dart';
import 'package:twonly/src/services/notification.service.dart';
import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/storage.dart'; import 'package:twonly/src/utils/storage.dart';
@ -116,14 +118,15 @@ Future sendRetransmitMessage(int retransId) async {
// encrypts and stores the message and then sends it in the background // encrypts and stores the message and then sends it in the background
Future encryptAndSendMessageAsync(int? messageId, int userId, MessageJson msg, Future encryptAndSendMessageAsync(int? messageId, int userId, MessageJson msg,
{PushKind? pushKind, bool willNotGetACKByUser = false}) async { {PushNotification? pushNotification,
bool willNotGetACKByUser = false}) async {
if (gIsDemoUser) { if (gIsDemoUser) {
return; return;
} }
Uint8List? pushData; Uint8List? pushData;
if (pushKind != null) { if (pushNotification != null) {
pushData = await getPushData(userId, pushKind); pushData = await getPushData(userId, pushNotification);
} }
int? retransId = await twonlyDB.messageRetransmissionDao.insertRetransmission( int? retransId = await twonlyDB.messageRetransmissionDao.insertRetransmission(
@ -158,7 +161,7 @@ Future encryptAndSendMessageAsync(int? messageId, int userId, MessageJson msg,
Future sendTextMessage( Future sendTextMessage(
int target, int target,
TextMessageContent content, TextMessageContent content,
PushKind? pushKind, PushNotification? pushNotification,
) async { ) async {
DateTime messageSendAt = DateTime.now(); DateTime messageSendAt = DateTime.now();
@ -178,6 +181,10 @@ Future sendTextMessage(
if (messageId == null) return; if (messageId == null) return;
if (pushNotification != null && !pushNotification.hasReactionContent()) {
pushNotification.messageId = Int64(messageId);
}
MessageJson msg = MessageJson( MessageJson msg = MessageJson(
kind: MessageKind.textMessage, kind: MessageKind.textMessage,
messageId: messageId, messageId: messageId,
@ -185,14 +192,22 @@ Future sendTextMessage(
timestamp: messageSendAt, timestamp: messageSendAt,
); );
await encryptAndSendMessageAsync(messageId, target, msg, pushKind: pushKind); await encryptAndSendMessageAsync(
messageId,
target,
msg,
pushNotification: pushNotification,
);
} }
Future notifyContactAboutOpeningMessage( Future notifyContactAboutOpeningMessage(
int fromUserId, int fromUserId,
List<int> messageOtherIds, List<int> messageOtherIds,
) async { ) async {
int biggestMessageId = messageOtherIds.first;
for (final messageOtherId in messageOtherIds) { for (final messageOtherId in messageOtherIds) {
if (messageOtherId > biggestMessageId) biggestMessageId = messageOtherId;
await encryptAndSendMessageAsync( await encryptAndSendMessageAsync(
null, null,
fromUserId, fromUserId,
@ -204,6 +219,7 @@ Future notifyContactAboutOpeningMessage(
), ),
); );
} }
await updateLastMessageId(fromUserId, biggestMessageId);
} }
Future notifyContactsAboutProfileChange() async { Future notifyContactsAboutProfileChange() async {
@ -216,8 +232,12 @@ Future notifyContactsAboutProfileChange() async {
for (Contact contact in contacts) { for (Contact contact in contacts) {
if (contact.myAvatarCounter < user.avatarCounter) { if (contact.myAvatarCounter < user.avatarCounter) {
twonlyDB.contactsDao.updateContact(contact.userId, twonlyDB.contactsDao.updateContact(
ContactsCompanion(myAvatarCounter: Value(user.avatarCounter))); contact.userId,
ContactsCompanion(
myAvatarCounter: Value(user.avatarCounter),
),
);
await encryptAndSendMessageAsync( await encryptAndSendMessageAsync(
null, null,
contact.userId, contact.userId,

View file

@ -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/json/message.dart';
import 'package:twonly/src/model/protobuf/api/websocket/client_to_server.pb.dart' import 'package:twonly/src/model/protobuf/api/websocket/client_to_server.pb.dart'
as client; 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/error.pb.dart';
import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pb.dart' import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pb.dart'
as server; as server;
import 'package:twonly/src/services/api/messages.dart'; import 'package:twonly/src/services/api/messages.dart';
import 'package:twonly/src/services/api/utils.dart'; import 'package:twonly/src/services/api/utils.dart';
import 'package:twonly/src/services/api/media_download.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/encryption.signal.dart';
import 'package:twonly/src/services/signal/identity.signal.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/prekeys.signal.dart';
import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/views/components/animate_icon.dart';
final lockHandleServerMessage = Mutex(); final lockHandleServerMessage = Mutex();
@ -272,6 +274,14 @@ Future<client.Response> handleNewMessage(int fromUserId, Uint8List body) async {
if (content is TextMessageContent) { if (content is TextMessageContent) {
responseToMessageId = content.responseToMessageId; responseToMessageId = content.responseToMessageId;
responseToOtherMessageId = content.responseToOtherMessageId; 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) { if (content is ReopenedMediaFileContent) {
responseToMessageId = content.messageId; responseToMessageId = content.messageId;

View file

@ -3,7 +3,7 @@ import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/globals.dart';
import 'package:twonly/src/constants/secure_storage_keys.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 'package:twonly/src/utils/log.dart';
import 'dart:io' show Platform; import 'dart:io' show Platform;
import '../../firebase_options.dart'; import '../../firebase_options.dart';

View file

@ -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<PushKeyMeta> keys;
PushUser({
required this.displayName,
required this.blocked,
required this.keys,
});
// Factory method to create a User from JSON
factory PushUser.fromJson(Map<String, dynamic> 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<String, dynamic> toJson() {
return {
'displayName': displayName,
'blocked': blocked,
'keys': keys.map((key) => key.toJson()).toList(),
};
}
}
class PushKeyMeta {
int id;
List<int> key;
DateTime createdAt;
PushKeyMeta({
required this.id,
required this.key,
required this.createdAt,
});
// Factory method to create Keys from JSON
factory PushKeyMeta.fromJson(Map<String, dynamic> json) {
return PushKeyMeta(
id: json['id'],
key: List<int>.from(json['key']),
createdAt: DateTime.fromMillisecondsSinceEpoch(json['createdAt']),
);
}
// Method to convert Keys to JSON
Map<String, dynamic> toJson() {
return {
'id': id,
'key': key,
'createdAt': createdAt.millisecondsSinceEpoch, // Store as timestamp
};
}
}
/// This function must be called after the database is setup
Future setupNotificationWithUsers({bool force = false}) async {
var pushKeys = await getPushKeys("receivingPushKeys");
var wasChanged = false;
final random = Random.secure();
final contacts = await 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<int>.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<int>.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<int> nonce;
final List<int> cipherText;
final List<int> mac;
PushNotification({
required this.keyId,
required this.nonce,
required this.cipherText,
required this.mac,
});
// Convert a PushNotification instance to a Map
Map<String, dynamic> toJson() {
return {
'keyId': keyId,
'nonce': base64Encode(nonce),
'cipherText': base64Encode(cipherText),
'mac': base64Encode(mac),
};
}
// Create a PushNotification instance from a Map
factory PushNotification.fromJson(Map<String, dynamic> json) {
return PushNotification(
keyId: json['keyId'],
nonce: base64Decode(json['nonce']),
cipherText: base64Decode(json['cipherText']),
mac: base64Decode(json['mac']),
);
}
}
/// this will trigger a push notification
/// push notification only containing the message kind and username
Future<Uint8List?> getPushData(int toUserId, PushKind kind) async {
final Map<int, PushUser> pushKeys = await getPushKeys("sendingPushKeys");
List<int> key = "InsecureOnlyUsedForAddingContact".codeUnits;
int keyId = 0;
if (pushKeys[toUserId] == null) {
// user does not have send any push keys
// only allow accept request and contactrequest to be send in an insecure way :/
// In future find a better way, e.g. use the signal protocol in a native way..
if (kind != PushKind.acceptRequest &&
kind != PushKind.contactRequest &&
kind != PushKind.testNotification) {
// this will be enforced after every app uses this system... :/
// return null;
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<Map<int, PushUser>> 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<int, PushUser> pushKeys = <int, PushUser>{};
if (pushKeysJson != null) {
Map<String, dynamic> jsonMap = jsonDecode(pushKeysJson);
jsonMap.forEach((key, value) {
pushKeys[int.parse(key)] = PushUser.fromJson(value);
});
}
return pushKeys;
}
Future setPushKeys(String storageKey, Map<int, PushUser> pushKeys) async {
var storage = FlutterSecureStorage();
Map<String, dynamic> 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<NotificationResponse> selectNotificationStream =
StreamController<NotificationResponse>.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<void> setupPushNotification() async {
const AndroidInitializationSettings initializationSettingsAndroid =
AndroidInitializationSettings("ic_launcher_foreground");
final List<DarwinNotificationCategory> darwinNotificationCategories =
<DarwinNotificationCategory>[];
/// 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();
}
}

View file

@ -5,7 +5,9 @@ import 'dart:math';
import 'package:cryptography_plus/cryptography_plus.dart'; import 'package:cryptography_plus/cryptography_plus.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:path_provider/path_provider.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'; import 'package:twonly/src/utils/log.dart';
final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin =
@ -24,7 +26,9 @@ Future customLocalPushNotification(String title, String msg) async {
const DarwinNotificationDetails darwinNotificationDetails = const DarwinNotificationDetails darwinNotificationDetails =
DarwinNotificationDetails(); DarwinNotificationDetails();
const NotificationDetails notificationDetails = NotificationDetails( const NotificationDetails notificationDetails = NotificationDetails(
android: androidNotificationDetails, iOS: darwinNotificationDetails); android: androidNotificationDetails,
iOS: darwinNotificationDetails,
);
await flutterLocalNotificationsPlugin.show( await flutterLocalNotificationsPlugin.show(
999999 + Random.secure().nextInt(9999), 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 { try {
String jsonString = utf8.decode(base64.decode(pushDataJson)); final pushData =
final pushData = PushNotification.fromJson(jsonDecode(jsonString)); EncryptedPushNotification.fromBuffer(base64.decode(pushDataB64));
PushKind? pushKind; PushNotification? pushNotification;
PushUser? pushUser; PushUser? foundPushUser;
int? fromUserId;
if (pushData.keyId == 0) { if (pushData.keyId == 0) {
List<int> key = "InsecureOnlyUsedForAddingContact".codeUnits; List<int> key = "InsecureOnlyUsedForAddingContact".codeUnits;
pushKind = await tryDecryptMessage(key, pushData); pushNotification = await tryDecryptMessage(key, pushData);
} else { } else {
var pushKeys = await getPushKeys("receivingPushKeys"); final pushUsers = await getPushKeys(SecureStorageKeys.receivingPushKeys);
for (final userId in pushKeys.keys) { for (final pushUser in pushUsers) {
for (final key in pushKeys[userId]!.keys) { for (final key in pushUser.pushKeys) {
if (key.id == pushData.keyId) { if (key.id == pushData.keyId) {
pushKind = await tryDecryptMessage(key.key, pushData); pushNotification = await tryDecryptMessage(key.key, pushData);
if (pushKind != null) { if (pushNotification != null) {
pushUser = pushKeys[userId]!; foundPushUser = pushUser;
fromUserId = userId;
break; break;
} }
} }
} }
// found correct key and user // found correct key and user
if (pushUser != null) break; if (foundPushUser != null) break;
} }
} }
if (pushKind != null) { if (pushNotification != null) {
if (pushKind == PushKind.testNotification) { if (pushNotification.kind == PushKind.testNotification) {
await customLocalPushNotification( await customLocalPushNotification(
"Test notification", "This is a test notification."); "Test notification",
} else if (pushUser != null && fromUserId != null) { "This is a test notification.",
await showLocalPushNotification(pushUser, fromUserId, pushKind); );
} 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 { } else {
await showLocalPushNotificationWithoutUserId(pushKind); await showLocalPushNotificationWithoutUserId(pushNotification);
} }
} }
} catch (e) { } catch (e) {
await customLocalPushNotification(
"Du hast eine neue Nachricht.",
"Öffne twonly um mehr zu erfahren.",
);
Log.error(e); Log.error(e);
} }
} }
Future<PushKind?> tryDecryptMessage( Future<PushNotification?> tryDecryptMessage(
List<int> key, PushNotification noti) async { List<int> key, EncryptedPushNotification push) async {
try { try {
final chacha20 = Chacha20.poly1305Aead(); final chacha20 = Chacha20.poly1305Aead();
SecretKeyData secretKeyData = SecretKeyData(key); SecretKeyData secretKeyData = SecretKeyData(key);
SecretBox secretBox = SecretBox( SecretBox secretBox = SecretBox(
noti.cipherText, push.ciphertext,
nonce: noti.nonce, nonce: push.nonce,
mac: Mac(noti.mac), mac: Mac(push.mac),
); );
final plaintext = final plaintext =
await chacha20.decrypt(secretBox, secretKey: secretKeyData); await chacha20.decrypt(secretBox, secretKey: secretKeyData);
final plaintextString = utf8.decode(plaintext); return PushNotification.fromBuffer(plaintext);
return PushKindExtension.fromString(plaintextString);
} catch (e) { } catch (e) {
// this error is allowed to happen... // this error is allowed to happen...
return null; return null;
@ -103,8 +119,7 @@ Future<PushKind?> tryDecryptMessage(
Future showLocalPushNotification( Future showLocalPushNotification(
PushUser pushUser, PushUser pushUser,
int fromUserId, PushNotification pushNotification,
PushKind pushKind,
) async { ) async {
String? title; String? title;
String? body; String? body;
@ -116,56 +131,64 @@ Future showLocalPushNotification(
} }
title = pushUser.displayName; title = pushUser.displayName;
body = getPushNotificationText(pushKind); body = getPushNotificationText(pushNotification);
if (body == "") { if (body == "") {
Log.error("No push notification type defined!"); Log.error("No push notification type defined!");
} }
FilePathAndroidBitmap? styleInformation; FilePathAndroidBitmap? styleInformation;
String? avatarPath = await getAvatarIcon(fromUserId); String? avatarPath = await getAvatarIcon(pushUser.userId.toInt());
if (avatarPath != null) { if (avatarPath != null) {
styleInformation = FilePathAndroidBitmap(avatarPath); styleInformation = FilePathAndroidBitmap(avatarPath);
} }
AndroidNotificationDetails androidNotificationDetails = AndroidNotificationDetails androidNotificationDetails =
AndroidNotificationDetails('0', 'Messages', AndroidNotificationDetails(
'0',
'Messages',
channelDescription: 'Messages from other users.', channelDescription: 'Messages from other users.',
importance: Importance.max, importance: Importance.max,
priority: Priority.max, priority: Priority.max,
ticker: 'You got a new message.', ticker: 'You got a new message.',
largeIcon: styleInformation); largeIcon: styleInformation,
);
const DarwinNotificationDetails darwinNotificationDetails = const DarwinNotificationDetails darwinNotificationDetails =
DarwinNotificationDetails(); DarwinNotificationDetails();
NotificationDetails notificationDetails = NotificationDetails( NotificationDetails notificationDetails = NotificationDetails(
android: androidNotificationDetails, iOS: darwinNotificationDetails); android: androidNotificationDetails,
iOS: darwinNotificationDetails,
);
await flutterLocalNotificationsPlugin.show( await flutterLocalNotificationsPlugin.show(
fromUserId, pushUser.userId.toInt(),
title, title,
body, body,
notificationDetails, notificationDetails,
payload: pushKind.name, payload: pushNotification.kind.name,
); );
} }
Future showLocalPushNotificationWithoutUserId( Future showLocalPushNotificationWithoutUserId(
PushKind pushKind, PushNotification pushNotification,
) async { ) async {
String? title; String? title;
String? body; String? body;
body = getPushNotificationTextWithoutUserId(pushKind); body = getPushNotificationTextWithoutUserId(pushNotification.kind);
if (body == "") { if (body == "") {
Log.error("No push notification type defined!"); Log.error("No push notification type defined!");
} }
AndroidNotificationDetails androidNotificationDetails = AndroidNotificationDetails androidNotificationDetails =
AndroidNotificationDetails('0', 'Messages', AndroidNotificationDetails(
'0',
'Messages',
channelDescription: 'Messages from other users.', channelDescription: 'Messages from other users.',
importance: Importance.max, importance: Importance.max,
priority: Priority.max, priority: Priority.max,
ticker: 'You got a new message.'); ticker: 'You got a new message.',
);
const DarwinNotificationDetails darwinNotificationDetails = const DarwinNotificationDetails darwinNotificationDetails =
DarwinNotificationDetails(); DarwinNotificationDetails();
@ -177,7 +200,7 @@ Future showLocalPushNotificationWithoutUserId(
title, title,
body, body,
notificationDetails, notificationDetails,
payload: pushKind.name, payload: pushNotification.kind.name,
); );
} }
@ -240,7 +263,7 @@ String getPushNotificationTextWithoutUserId(PushKind pushKind) {
return pushNotificationText[pushKind.name] ?? ""; return pushNotificationText[pushKind.name] ?? "";
} }
String getPushNotificationText(PushKind pushKind) { String getPushNotificationText(PushNotification pushNotification) {
String systemLanguage = Platform.localeName; String systemLanguage = Platform.localeName;
Map<String, String> pushNotificationText; Map<String, String> pushNotificationText;
@ -256,9 +279,12 @@ String getPushNotificationText(PushKind pushKind) {
PushKind.storedMediaFile.name: "hat dein Bild gespeichert.", PushKind.storedMediaFile.name: "hat dein Bild gespeichert.",
PushKind.reaction.name: "hat auf dein Bild reagiert.", PushKind.reaction.name: "hat auf dein Bild reagiert.",
PushKind.reopenedMedia.name: "hat dein Bild erneut geöffnet.", PushKind.reopenedMedia.name: "hat dein Bild erneut geöffnet.",
PushKind.reactionToVideo.name: "hat auf dein Video reagiert.", PushKind.reactionToVideo.name:
PushKind.reactionToText.name: "hat auf deinen Text reagiert.", "hat mit {{reaction}} auf dein Video reagiert.",
PushKind.reactionToImage.name: "hat auf dein Bild 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.", PushKind.response.name: "hat dir geantwortet.",
}; };
} else { } else {
@ -272,11 +298,19 @@ String getPushNotificationText(PushKind pushKind) {
PushKind.storedMediaFile.name: "has stored your image.", PushKind.storedMediaFile.name: "has stored your image.",
PushKind.reaction.name: "has reacted to your image.", PushKind.reaction.name: "has reacted to your image.",
PushKind.reopenedMedia.name: "has reopened your image.", PushKind.reopenedMedia.name: "has reopened your image.",
PushKind.reactionToVideo.name: "has reacted to your video.", PushKind.reactionToVideo.name:
PushKind.reactionToText.name: "has reacted to your text.", "has reacted with {{reaction}} to your video.",
PushKind.reactionToImage.name: "has reacted to your image.", 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.", 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;
} }

View file

@ -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<int>.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<int>.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<PushUser> 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<Uint8List?> getPushData(int toUserId, PushNotification content) async {
final List<PushUser> pushKeys =
await getPushKeys(SecureStorageKeys.sendingPushKeys);
List<int> 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<List<PushUser>> 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<PushUser> 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,
),
);
}

View file

@ -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<NotificationResponse> selectNotificationStream =
StreamController<NotificationResponse>.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<void> setupPushNotification() async {
const AndroidInitializationSettings initializationSettingsAndroid =
AndroidInitializationSettings("ic_launcher_foreground");
final List<DarwinNotificationCategory> darwinNotificationCategories =
<DarwinNotificationCategory>[];
/// 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();
}
}

View file

@ -4,14 +4,15 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:provider/provider.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/providers/connection.provider.dart';
import 'package:twonly/src/services/api/utils.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/services/signal/session.signal.dart';
import 'package:twonly/src/views/components/alert_dialog.dart'; import 'package:twonly/src/views/components/alert_dialog.dart';
import 'package:twonly/src/database/daos/contacts_dao.dart'; import 'package:twonly/src/database/daos/contacts_dao.dart';
import 'package:twonly/src/database/tables/messages_table.dart'; import 'package:twonly/src/database/tables/messages_table.dart';
import 'package:twonly/src/database/twonly_database.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/src/utils/misc.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/globals.dart';
import 'package:twonly/src/views/components/headline.dart'; import 'package:twonly/src/views/components/headline.dart';
@ -103,7 +104,7 @@ class _SearchUsernameView extends State<AddNewUserView> {
timestamp: DateTime.now(), timestamp: DateTime.now(),
content: MessageContent(), content: MessageContent(),
), ),
pushKind: PushKind.contactRequest, pushNotification: PushNotification(kind: PushKind.contactRequest),
); );
} }
} }
@ -281,7 +282,7 @@ class _ContactsListViewState extends State<ContactsListView> {
timestamp: DateTime.now(), timestamp: DateTime.now(),
content: MessageContent(), content: MessageContent(),
), ),
pushKind: PushKind.acceptRequest, pushNotification: PushNotification(kind: PushKind.acceptRequest),
); );
notifyContactsAboutProfileChange(); notifyContactsAboutProfileChange();
}, },

View file

@ -5,6 +5,8 @@ import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:twonly/globals.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/chats/chat_messages_components/chat_message_entry.dart';
import 'package:twonly/src/views/components/animate_icon.dart'; import 'package:twonly/src/views/components/animate_icon.dart';
import 'package:twonly/src/views/components/initialsavatar.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/database/tables/messages_table.dart';
import 'package:twonly/src/model/json/message.dart'; import 'package:twonly/src/model/json/message.dart';
import 'package:twonly/src/services/api/messages.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/views/camera/camera_send_to_view.dart';
import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/views/contact/contact.view.dart'; import 'package:twonly/src/views/contact/contact.view.dart';
@ -174,6 +176,7 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
Future _sendMessage() async { Future _sendMessage() async {
if (newMessageController.text == "") return; if (newMessageController.text == "") return;
await sendTextMessage( await sendTextMessage(
user.userId, user.userId,
TextMessageContent( TextMessageContent(
@ -181,7 +184,16 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
responseToMessageId: responseToMessage?.messageOtherId, responseToMessageId: responseToMessage?.messageOtherId,
responseToOtherMessageId: responseToMessage?.messageId, 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(); newMessageController.clear();
currentInputText = ""; currentInputText = "";

View file

@ -1,13 +1,14 @@
import 'package:drift/drift.dart' show Value; import 'package:drift/drift.dart' show Value;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:twonly/globals.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/views/chats/chat_messages_components/in_chat_media_viewer.dart';
import 'package:twonly/src/database/twonly_database.dart'; import 'package:twonly/src/database/twonly_database.dart';
import 'package:twonly/src/database/tables/messages_table.dart'; import 'package:twonly/src/database/tables/messages_table.dart';
import 'package:twonly/src/model/json/message.dart'; import 'package:twonly/src/model/json/message.dart';
import 'package:twonly/src/services/api/messages.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/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/views/chats/media_viewer.view.dart';
import 'package:twonly/src/model/memory_item.model.dart'; import 'package:twonly/src/model/memory_item.model.dart';
import 'package:twonly/src/views/tutorial/tutorials.dart'; import 'package:twonly/src/views/tutorial/tutorials.dart';
@ -80,7 +81,9 @@ class _ChatMediaEntryState extends State<ChatMediaEntry> {
), ),
timestamp: DateTime.now(), timestamp: DateTime.now(),
), ),
pushKind: PushKind.reopenedMedia, pushNotification: PushNotification(
kind: PushKind.reopenedMedia,
),
); );
await twonlyDB.messagesDao.updateMessageByMessageId( await twonlyDB.messagesDao.updateMessageByMessageId(
widget.message.messageId, widget.message.messageId,

View file

@ -8,7 +8,9 @@ import 'package:lottie/lottie.dart';
import 'package:no_screenshot/no_screenshot.dart'; import 'package:no_screenshot/no_screenshot.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/globals.dart';
import 'package:twonly/src/database/daos/contacts_dao.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/api/utils.dart';
import 'package:twonly/src/services/notifications/background.notifications.dart';
import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/views/camera/share_image_editor_view.dart'; import 'package:twonly/src/views/camera/share_image_editor_view.dart';
import 'package:twonly/src/views/components/animate_icon.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/model/json/message.dart';
import 'package:twonly/src/services/api/messages.dart'; import 'package:twonly/src/services/api/messages.dart';
import 'package:twonly/src/services/api/media_download.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/misc.dart';
import 'package:twonly/src/utils/storage.dart'; import 'package:twonly/src/utils/storage.dart';
import 'package:twonly/src/views/camera/camera_send_to_view.dart'; import 'package:twonly/src/views/camera/camera_send_to_view.dart';
@ -314,7 +315,7 @@ class _MediaViewerViewState extends State<MediaViewerView> {
), ),
timestamp: DateTime.now(), timestamp: DateTime.now(),
), ),
pushKind: PushKind.storedMediaFile, pushNotification: PushNotification(kind: PushKind.acceptRequest),
); );
setState(() { setState(() {
imageSaved = true; imageSaved = true;
@ -640,7 +641,9 @@ class _MediaViewerViewState extends State<MediaViewerView> {
responseToMessageId: responseToMessageId:
allMediaFiles.first.messageOtherId, allMediaFiles.first.messageOtherId,
), ),
PushKind.reaction, PushNotification(
kind: PushKind.response,
),
); );
textMessageController.clear(); textMessageController.clear();
} }
@ -822,9 +825,13 @@ class _EmojiReactionWidgetState extends State<EmojiReactionWidget> {
text: widget.emoji, text: widget.emoji,
responseToMessageId: widget.responseToMessageId, responseToMessageId: widget.responseToMessageId,
), ),
widget.isVideo PushNotification(
kind: widget.isVideo
? PushKind.reactionToVideo ? PushKind.reactionToVideo
: PushKind.reactionToImage); : PushKind.reactionToImage,
reactionContent: widget.emoji,
),
);
setState(() { setState(() {
selectedShortReaction = 0; // Assuming index is 0 for this example selectedShortReaction = 0; // Assuming index is 0 for this example
}); });

View file

@ -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/database/tables/messages_table.dart';
import 'package:twonly/src/model/json/message.dart'; import 'package:twonly/src/model/json/message.dart';
import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/views/components/animate_icon.dart';
enum MessageSendState { enum MessageSendState {
received, received,
@ -89,11 +90,12 @@ class _MessageSendStateIconState extends State<MessageSendStateIcon> {
MessageSendState state = messageSendStateFromMessage(message); MessageSendState state = messageSendStateFromMessage(message);
late Color color; late Color color;
MessageContent? content;
if (message.contentJson == null) { if (message.contentJson == null) {
color = getMessageColorFromType(TextMessageContent(text: ""), context); color = getMessageColorFromType(TextMessageContent(text: ""), context);
} else { } else {
MessageContent? content = MessageContent.fromJson( content = MessageContent.fromJson(
message.kind, message.kind,
jsonDecode(message.contentJson!), jsonDecode(message.contentJson!),
); );
@ -106,6 +108,11 @@ class _MessageSendStateIconState extends State<MessageSendStateIcon> {
switch (state) { switch (state) {
case MessageSendState.receivedOpened: case MessageSendState.receivedOpened:
icon = Icon(Icons.crop_square, size: 14, color: color); 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; text = context.lang.messageSendState_Received;
break; break;
case MessageSendState.sendOpened: case MessageSendState.sendOpened:

View file

@ -4,10 +4,10 @@ import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:pie_menu/pie_menu.dart'; import 'package:pie_menu/pie_menu.dart';
import 'package:screenshot/screenshot.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/utils/misc.dart';
import 'package:twonly/src/views/camera/camera_preview_components/camera_preview.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/views/components/user_context_menu.dart';
import 'package:twonly/src/services/notification.service.dart';
import 'package:twonly/src/views/memories/memories.view.dart'; import 'package:twonly/src/views/memories/memories.view.dart';
import 'camera/camera_preview_controller_view.dart'; import 'camera/camera_preview_controller_view.dart';
import 'chats/chat_list.view.dart'; import 'chats/chat_list.view.dart';

View file

@ -1,13 +1,15 @@
import 'dart:io'; import 'dart:io';
import 'package:fixnum/fixnum.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/globals.dart';
import 'package:twonly/src/constants/secure_storage_keys.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/views/components/alert_dialog.dart';
import 'package:twonly/src/services/fcm.service.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/misc.dart';
import 'package:twonly/src/utils/storage.dart'; import 'package:twonly/src/utils/storage.dart';
@ -50,7 +52,10 @@ class NotificationView extends StatelessWidget {
if (user != null) { if (user != null) {
final pushData = await getPushData( final pushData = await getPushData(
user.userId, user.userId,
PushKind.testNotification, PushNotification(
messageId: Int64(0),
kind: PushKind.testNotification,
),
); );
await apiService.sendTextMessage( await apiService.sendTextMessage(
user.userId, user.userId,