You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
session-ios/SessionMessagingKit/Sending & Receiving/Notifications/PushNotificationAPI.swift

246 lines
10 KiB
Swift

// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import GRDB
import PromiseKit
import SessionSnodeKit
import SessionUtilitiesKit
@objc(LKPushNotificationAPI)
public final class PushNotificationAPI : NSObject {
Fixed a number of reported bugs, some cleanup, added animated profile support Added support for animated profile images (no ability to crop/resize) Updated the message trimming to only remove messages if the open group has 2000 messages or more Updated the message trimming setting to default to be on Updated the ContextMenu to fade out the snapshot as well (looked buggy if the device had even minor lag) Updated the ProfileManager to delete and re-download invalid avatar images (and updated the conversation screen to reload when avatars complete downloading) Updated the message request notification logic so it will show notifications when receiving a new message request as long as the user has read all the old ones (previously the user had to accept/reject all the old ones) Fixed a bug where the "trim open group messages" toggle was accessing UI off the main thread Fixed a bug where the "Chats" settings screen had a close button instead of a back button Fixed a bug where the 'viewsToMove' for the reply UI was inconsistent in some places Fixed an issue where the ProfileManager was doing all of it's validation (and writing to disk) within the database write closure which would block database writes unnecessarily Fixed a bug where a message request wouldn't be identified as such just because it wasn't visible in the conversations list Fixed a bug where opening a message request notification would result in the message request being in the wrong state (also wouldn't insert the 'MessageRequestsViewController' into the hierarchy) Fixed a bug where the avatar image wouldn't appear beside incoming closed group message in some situations cases Removed an error log that was essentially just spam Remove the logic to delete old profile images when calling save on a Profile (wouldn't get called if the row was modified directly and duplicates GarbageCollection logic) Remove the logic to send a notification when calling save on a Profile (wouldn't get called if the row was modified directly) Tweaked the message trimming description to be more accurate Cleaned up some duplicate logic used to determine if a notification should be shown Cleaned up some onion request logic (was passing the version info in some cases when not needed) Moved the push notification notify API call into the PushNotificationAPI class for consistency
3 years ago
struct RegistrationRequestBody: Codable {
let token: String
let pubKey: String?
}
Fixed a number of reported bugs, some cleanup, added animated profile support Added support for animated profile images (no ability to crop/resize) Updated the message trimming to only remove messages if the open group has 2000 messages or more Updated the message trimming setting to default to be on Updated the ContextMenu to fade out the snapshot as well (looked buggy if the device had even minor lag) Updated the ProfileManager to delete and re-download invalid avatar images (and updated the conversation screen to reload when avatars complete downloading) Updated the message request notification logic so it will show notifications when receiving a new message request as long as the user has read all the old ones (previously the user had to accept/reject all the old ones) Fixed a bug where the "trim open group messages" toggle was accessing UI off the main thread Fixed a bug where the "Chats" settings screen had a close button instead of a back button Fixed a bug where the 'viewsToMove' for the reply UI was inconsistent in some places Fixed an issue where the ProfileManager was doing all of it's validation (and writing to disk) within the database write closure which would block database writes unnecessarily Fixed a bug where a message request wouldn't be identified as such just because it wasn't visible in the conversations list Fixed a bug where opening a message request notification would result in the message request being in the wrong state (also wouldn't insert the 'MessageRequestsViewController' into the hierarchy) Fixed a bug where the avatar image wouldn't appear beside incoming closed group message in some situations cases Removed an error log that was essentially just spam Remove the logic to delete old profile images when calling save on a Profile (wouldn't get called if the row was modified directly and duplicates GarbageCollection logic) Remove the logic to send a notification when calling save on a Profile (wouldn't get called if the row was modified directly) Tweaked the message trimming description to be more accurate Cleaned up some duplicate logic used to determine if a notification should be shown Cleaned up some onion request logic (was passing the version info in some cases when not needed) Moved the push notification notify API call into the PushNotificationAPI class for consistency
3 years ago
struct NotifyRequestBody: Codable {
enum CodingKeys: String, CodingKey {
case data
case sendTo = "send_to"
}
let data: String
let sendTo: String
}
struct ClosedGroupRequestBody: Codable {
let closedGroupPublicKey: String
let pubKey: String
}
// MARK: - Settings
public static let server = "https://live.apns.getsession.org"
4 years ago
public static let serverPublicKey = "642a6585919742e5a2d4dc51244964fbcd8bcab2b75612407de58b810740d049"
private static let maxRetryCount: UInt = 4
private static let tokenExpirationInterval: TimeInterval = 12 * 60 * 60
@objc public enum ClosedGroupOperation : Int {
case subscribe, unsubscribe
public var endpoint: String {
switch self {
case .subscribe: return "subscribe_closed_group"
case .unsubscribe: return "unsubscribe_closed_group"
}
}
}
// MARK: - Initialization
private override init() { }
// MARK: - Registration
4 years ago
public static func unregister(_ token: Data) -> Promise<Void> {
Fixed a number of reported bugs, some cleanup, added animated profile support Added support for animated profile images (no ability to crop/resize) Updated the message trimming to only remove messages if the open group has 2000 messages or more Updated the message trimming setting to default to be on Updated the ContextMenu to fade out the snapshot as well (looked buggy if the device had even minor lag) Updated the ProfileManager to delete and re-download invalid avatar images (and updated the conversation screen to reload when avatars complete downloading) Updated the message request notification logic so it will show notifications when receiving a new message request as long as the user has read all the old ones (previously the user had to accept/reject all the old ones) Fixed a bug where the "trim open group messages" toggle was accessing UI off the main thread Fixed a bug where the "Chats" settings screen had a close button instead of a back button Fixed a bug where the 'viewsToMove' for the reply UI was inconsistent in some places Fixed an issue where the ProfileManager was doing all of it's validation (and writing to disk) within the database write closure which would block database writes unnecessarily Fixed a bug where a message request wouldn't be identified as such just because it wasn't visible in the conversations list Fixed a bug where opening a message request notification would result in the message request being in the wrong state (also wouldn't insert the 'MessageRequestsViewController' into the hierarchy) Fixed a bug where the avatar image wouldn't appear beside incoming closed group message in some situations cases Removed an error log that was essentially just spam Remove the logic to delete old profile images when calling save on a Profile (wouldn't get called if the row was modified directly and duplicates GarbageCollection logic) Remove the logic to send a notification when calling save on a Profile (wouldn't get called if the row was modified directly) Tweaked the message trimming description to be more accurate Cleaned up some duplicate logic used to determine if a notification should be shown Cleaned up some onion request logic (was passing the version info in some cases when not needed) Moved the push notification notify API call into the PushNotificationAPI class for consistency
3 years ago
let requestBody: RegistrationRequestBody = RegistrationRequestBody(token: token.toHexString(), pubKey: nil)
guard let body: Data = try? JSONEncoder().encode(requestBody) else {
return Promise(error: HTTP.Error.invalidJSON)
}
let url = URL(string: "\(server)/unregister")!
var request: URLRequest = URLRequest(url: url)
request.httpMethod = "POST"
request.allHTTPHeaderFields = [ Header.contentType.rawValue: "application/json" ]
request.httpBody = body
let promise: Promise<Void> = attempt(maxRetryCount: maxRetryCount, recoveringOn: DispatchQueue.global()) {
OnionRequestAPI.sendOnionRequest(request, to: server, using: .v2, with: serverPublicKey)
.map2 { _, response in
guard let response: UpdateRegistrationResponse = try? response?.decoded(as: UpdateRegistrationResponse.self) else {
return SNLog("Couldn't unregister from push notifications.")
}
guard response.body.code != 0 else {
return SNLog("Couldn't unregister from push notifications due to error: \(response.body.message ?? "nil").")
}
}
}
promise.catch2 { error in
4 years ago
SNLog("Couldn't unregister from push notifications.")
}
// Unsubscribe from all closed groups (including ones the user is no longer a member of, just in case)
Storage.shared.read { db in
let userPublicKey: String = getUserHexEncodedPublicKey(db)
try ClosedGroup
.select(.threadId)
.asRequest(of: String.self)
.fetchAll(db)
.forEach { closedGroupPublicKey in
performOperation(.unsubscribe, for: closedGroupPublicKey, publicKey: userPublicKey)
}
}
return promise
}
4 years ago
@objc(unregisterToken:)
public static func objc_unregister(token: Data) -> AnyPromise {
return AnyPromise.from(unregister(token))
}
4 years ago
public static func register(with token: Data, publicKey: String, isForcedUpdate: Bool) -> Promise<Void> {
let hexEncodedToken: String = token.toHexString()
Fixed a number of reported bugs, some cleanup, added animated profile support Added support for animated profile images (no ability to crop/resize) Updated the message trimming to only remove messages if the open group has 2000 messages or more Updated the message trimming setting to default to be on Updated the ContextMenu to fade out the snapshot as well (looked buggy if the device had even minor lag) Updated the ProfileManager to delete and re-download invalid avatar images (and updated the conversation screen to reload when avatars complete downloading) Updated the message request notification logic so it will show notifications when receiving a new message request as long as the user has read all the old ones (previously the user had to accept/reject all the old ones) Fixed a bug where the "trim open group messages" toggle was accessing UI off the main thread Fixed a bug where the "Chats" settings screen had a close button instead of a back button Fixed a bug where the 'viewsToMove' for the reply UI was inconsistent in some places Fixed an issue where the ProfileManager was doing all of it's validation (and writing to disk) within the database write closure which would block database writes unnecessarily Fixed a bug where a message request wouldn't be identified as such just because it wasn't visible in the conversations list Fixed a bug where opening a message request notification would result in the message request being in the wrong state (also wouldn't insert the 'MessageRequestsViewController' into the hierarchy) Fixed a bug where the avatar image wouldn't appear beside incoming closed group message in some situations cases Removed an error log that was essentially just spam Remove the logic to delete old profile images when calling save on a Profile (wouldn't get called if the row was modified directly and duplicates GarbageCollection logic) Remove the logic to send a notification when calling save on a Profile (wouldn't get called if the row was modified directly) Tweaked the message trimming description to be more accurate Cleaned up some duplicate logic used to determine if a notification should be shown Cleaned up some onion request logic (was passing the version info in some cases when not needed) Moved the push notification notify API call into the PushNotificationAPI class for consistency
3 years ago
let requestBody: RegistrationRequestBody = RegistrationRequestBody(token: hexEncodedToken, pubKey: publicKey)
guard let body: Data = try? JSONEncoder().encode(requestBody) else {
return Promise(error: HTTP.Error.invalidJSON)
}
let userDefaults = UserDefaults.standard
let oldToken = userDefaults[.deviceToken]
let lastUploadTime = userDefaults[.lastDeviceTokenUpload]
let now = Date().timeIntervalSince1970
guard isForcedUpdate || hexEncodedToken != oldToken || now - lastUploadTime > tokenExpirationInterval else {
4 years ago
SNLog("Device token hasn't changed or expired; no need to re-upload.")
return Promise<Void> { $0.fulfill(()) }
}
let url = URL(string: "\(server)/register")!
var request: URLRequest = URLRequest(url: url)
request.httpMethod = "POST"
request.allHTTPHeaderFields = [ Header.contentType.rawValue: "application/json" ]
request.httpBody = body
var promises: [Promise<Void>] = []
promises.append(
attempt(maxRetryCount: maxRetryCount, recoveringOn: DispatchQueue.global()) {
OnionRequestAPI.sendOnionRequest(request, to: server, using: .v2, with: serverPublicKey)
.map2 { _, data -> Void in
guard let response: UpdateRegistrationResponse = try? data?.decoded(as: UpdateRegistrationResponse.self) else {
return SNLog("Couldn't register device token.")
}
guard response.body.code != 0 else {
return SNLog("Couldn't register device token due to error: \(response.body.message ?? "nil").")
}
userDefaults[.deviceToken] = hexEncodedToken
userDefaults[.lastDeviceTokenUpload] = now
userDefaults[.isUsingFullAPNs] = true
}
}
)
promises.first?.catch2 { error in
4 years ago
SNLog("Couldn't register device token.")
}
// Subscribe to all closed groups
promises.append(
contentsOf: Storage.shared
.read { db -> [String] in
try ClosedGroup
.select(.threadId)
.joining(
required: ClosedGroup.members
.filter(GroupMember.Columns.profileId == getUserHexEncodedPublicKey(db))
)
.asRequest(of: String.self)
.fetchAll(db)
}
.defaulting(to: [])
.map { closedGroupPublicKey -> Promise<Void> in
performOperation(.subscribe, for: closedGroupPublicKey, publicKey: publicKey)
}
)
return when(fulfilled: promises)
}
@objc(registerWithToken:hexEncodedPublicKey:isForcedUpdate:)
public static func objc_register(with token: Data, publicKey: String, isForcedUpdate: Bool) -> AnyPromise {
return AnyPromise.from(register(with: token, publicKey: publicKey, isForcedUpdate: isForcedUpdate))
}
@discardableResult
public static func performOperation(_ operation: ClosedGroupOperation, for closedGroupPublicKey: String, publicKey: String) -> Promise<Void> {
let isUsingFullAPNs = UserDefaults.standard[.isUsingFullAPNs]
let requestBody: ClosedGroupRequestBody = ClosedGroupRequestBody(
closedGroupPublicKey: closedGroupPublicKey,
pubKey: publicKey
)
guard isUsingFullAPNs else { return Promise<Void> { $0.fulfill(()) } }
guard let body: Data = try? JSONEncoder().encode(requestBody) else {
return Promise(error: HTTP.Error.invalidJSON)
}
let url = URL(string: "\(server)/\(operation.endpoint)")!
var request: URLRequest = URLRequest(url: url)
request.httpMethod = "POST"
request.allHTTPHeaderFields = [ Header.contentType.rawValue: "application/json" ]
request.httpBody = body
let promise: Promise<Void> = attempt(maxRetryCount: maxRetryCount, recoveringOn: DispatchQueue.global()) {
OnionRequestAPI.sendOnionRequest(request, to: server, using: .v2, with: serverPublicKey)
.map2 { _, response in
guard let response: UpdateRegistrationResponse = try? response?.decoded(as: UpdateRegistrationResponse.self) else {
return SNLog("Couldn't subscribe/unsubscribe for closed group: \(closedGroupPublicKey).")
}
guard response.body.code != 0 else {
return SNLog("Couldn't subscribe/unsubscribe for closed group: \(closedGroupPublicKey) due to error: \(response.body.message ?? "nil").")
}
}
}
promise.catch2 { error in
SNLog("Couldn't subscribe/unsubscribe for closed group: \(closedGroupPublicKey).")
}
return promise
}
@objc(performOperation:forClosedGroupWithPublicKey:userPublicKey:)
public static func objc_performOperation(_ operation: ClosedGroupOperation, for closedGroupPublicKey: String, publicKey: String) -> AnyPromise {
return AnyPromise.from(performOperation(operation, for: closedGroupPublicKey, publicKey: publicKey))
}
Fixed a number of reported bugs, some cleanup, added animated profile support Added support for animated profile images (no ability to crop/resize) Updated the message trimming to only remove messages if the open group has 2000 messages or more Updated the message trimming setting to default to be on Updated the ContextMenu to fade out the snapshot as well (looked buggy if the device had even minor lag) Updated the ProfileManager to delete and re-download invalid avatar images (and updated the conversation screen to reload when avatars complete downloading) Updated the message request notification logic so it will show notifications when receiving a new message request as long as the user has read all the old ones (previously the user had to accept/reject all the old ones) Fixed a bug where the "trim open group messages" toggle was accessing UI off the main thread Fixed a bug where the "Chats" settings screen had a close button instead of a back button Fixed a bug where the 'viewsToMove' for the reply UI was inconsistent in some places Fixed an issue where the ProfileManager was doing all of it's validation (and writing to disk) within the database write closure which would block database writes unnecessarily Fixed a bug where a message request wouldn't be identified as such just because it wasn't visible in the conversations list Fixed a bug where opening a message request notification would result in the message request being in the wrong state (also wouldn't insert the 'MessageRequestsViewController' into the hierarchy) Fixed a bug where the avatar image wouldn't appear beside incoming closed group message in some situations cases Removed an error log that was essentially just spam Remove the logic to delete old profile images when calling save on a Profile (wouldn't get called if the row was modified directly and duplicates GarbageCollection logic) Remove the logic to send a notification when calling save on a Profile (wouldn't get called if the row was modified directly) Tweaked the message trimming description to be more accurate Cleaned up some duplicate logic used to determine if a notification should be shown Cleaned up some onion request logic (was passing the version info in some cases when not needed) Moved the push notification notify API call into the PushNotificationAPI class for consistency
3 years ago
// MARK: - Notify
public static func notify(
recipient: String,
with message: String,
maxRetryCount: UInt? = nil,
queue: DispatchQueue = DispatchQueue.global()
) -> Promise<Void> {
let requestBody: NotifyRequestBody = NotifyRequestBody(data: message, sendTo: recipient)
guard let body: Data = try? JSONEncoder().encode(requestBody) else {
return Promise(error: HTTP.Error.invalidJSON)
}
let url = URL(string: "\(server)/notify")!
var request: URLRequest = URLRequest(url: url)
request.httpMethod = "POST"
request.allHTTPHeaderFields = [ Header.contentType.rawValue: "application/json" ]
request.httpBody = body
let retryCount: UInt = (maxRetryCount ?? PushNotificationAPI.maxRetryCount)
let promise: Promise<Void> = attempt(maxRetryCount: retryCount, recoveringOn: queue) {
OnionRequestAPI.sendOnionRequest(request, to: server, using: .v2, with: serverPublicKey)
.map { _ in }
}
return promise
}
}