Update SessionMessagingKit for open groups
This commit is contained in:
parent
a3382f41d4
commit
362e2e9c03
1
Podfile
1
Podfile
|
@ -99,6 +99,7 @@ target 'SignalMessaging' do
|
|||
end
|
||||
|
||||
target 'SessionMessagingKit' do
|
||||
pod 'AFNetworking', inhibit_warnings: true
|
||||
pod 'CryptoSwift', :inhibit_warnings => true
|
||||
pod 'Curve25519Kit', :inhibit_warnings => true
|
||||
pod 'PromiseKit', :inhibit_warnings => true
|
||||
|
|
|
@ -197,6 +197,7 @@ PODS:
|
|||
- ZXingObjC/All (3.6.5)
|
||||
|
||||
DEPENDENCIES:
|
||||
- AFNetworking
|
||||
- AFNetworking (~> 3.2.1)
|
||||
- CocoaLumberjack
|
||||
- CryptoSwift
|
||||
|
@ -332,6 +333,6 @@ SPEC CHECKSUMS:
|
|||
YYImage: 6db68da66f20d9f169ceb94dfb9947c3867b9665
|
||||
ZXingObjC: fdbb269f25dd2032da343e06f10224d62f537bdb
|
||||
|
||||
PODFILE CHECKSUM: 278b25019daa575575de0bf9baf371f7cdcd4fc4
|
||||
PODFILE CHECKSUM: 8fc5917e97576b902a46b328af80664381ede889
|
||||
|
||||
COCOAPODS: 1.10.0.rc.1
|
||||
|
|
2
Pods
2
Pods
|
@ -1 +1 @@
|
|||
Subproject commit 38df69dfa06ce734db625bdf39534d654528a916
|
||||
Subproject commit 2bd2badebb8fd11a6f7312b2c490cf9a70a09874
|
|
@ -4,6 +4,7 @@ public struct Configuration {
|
|||
public let storage: SessionMessagingKitStorageProtocol
|
||||
public let sessionRestorationImplementation: SessionRestorationProtocol
|
||||
public let certificateValidator: SMKCertificateValidator
|
||||
public let openGroupAPIDelegate: OpenGroupAPIDelegate
|
||||
public let pnServerURL: String
|
||||
public let pnServerPublicKey: String
|
||||
|
||||
|
|
|
@ -0,0 +1,75 @@
|
|||
import AFNetworking
|
||||
import PromiseKit
|
||||
import SessionSnodeKit
|
||||
import SessionUtilitiesKit
|
||||
|
||||
@objc(SNFileServerAPI)
|
||||
public final class FileServerAPI : DotNetAPI {
|
||||
|
||||
// MARK: Settings
|
||||
private static let attachmentType = "net.app.core.oembed"
|
||||
private static let deviceLinkType = "network.loki.messenger.devicemapping"
|
||||
|
||||
internal static let publicKey = "62509D59BDEEC404DD0D489C1E15BA8F94FD3D619B01C1BF48A9922BFCB7311C"
|
||||
|
||||
public static let maxFileSize = 10_000_000 // 10 MB
|
||||
/// The file server has a file size limit of `maxFileSize`, which the Service Nodes try to enforce as well. However, the limit applied by the Service Nodes
|
||||
/// is on the **HTTP request** and not the actual file size. Because the file server expects the file data to be base 64 encoded, the size of the HTTP
|
||||
/// request for a given file will be at least `ceil(n / 3) * 4` bytes, where n is the file size in bytes. This is the minimum size because there might also
|
||||
/// be other parameters in the request. On average the multiplier appears to be about 1.5, so when checking whether the file will exceed the file size limit when
|
||||
/// uploading a file we just divide the size of the file by this number. The alternative would be to actually check the size of the HTTP request but that's only
|
||||
/// possible after proof of work has been calculated and the onion request encryption has happened, which takes several seconds.
|
||||
public static let fileSizeORMultiplier: Double = 3
|
||||
|
||||
@objc public static let server = "https://file.getsession.org"
|
||||
@objc public static let fileStorageBucketURL = "https://file-static.lokinet.org"
|
||||
|
||||
// MARK: Profile Pictures
|
||||
@objc(uploadProfilePicture:)
|
||||
public static func objc_uploadProfilePicture(_ profilePicture: Data) -> AnyPromise {
|
||||
return AnyPromise.from(uploadProfilePicture(profilePicture))
|
||||
}
|
||||
|
||||
public static func uploadProfilePicture(_ profilePicture: Data) -> Promise<String> {
|
||||
guard Double(profilePicture.count) < Double(maxFileSize) / fileSizeORMultiplier else { return Promise(error: Error.maxFileSizeExceeded) }
|
||||
let url = "\(server)/files"
|
||||
let parameters: JSON = [ "type" : attachmentType, "Content-Type" : "application/binary" ]
|
||||
var error: NSError?
|
||||
let request = AFHTTPRequestSerializer().multipartFormRequest(withMethod: "POST", urlString: url, parameters: parameters, constructingBodyWith: { formData in
|
||||
formData.appendPart(withFileData: profilePicture, name: "content", fileName: UUID().uuidString, mimeType: "application/binary")
|
||||
}, error: &error)
|
||||
// Uploads to the Loki File Server shouldn't include any personally identifiable information so use a dummy auth token
|
||||
request.addValue("Bearer loki", forHTTPHeaderField: "Authorization")
|
||||
if let error = error {
|
||||
SNLog("Couldn't upload profile picture due to error: \(error).")
|
||||
return Promise(error: error)
|
||||
}
|
||||
return OnionRequestAPI.sendOnionRequest(request, to: server, using: publicKey).map(on: DispatchQueue.global(qos: .userInitiated)) { json in
|
||||
guard let data = json["data"] as? JSON, let downloadURL = data["url"] as? String else {
|
||||
SNLog("Couldn't parse profile picture from: \(json).")
|
||||
throw Error.parsingFailed
|
||||
}
|
||||
Configuration.shared.storage.setLastProfilePictureUploadDate(Date())
|
||||
return downloadURL
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Open Group Server Public Key
|
||||
public static func getPublicKey(for openGroupServer: String) -> Promise<String> {
|
||||
let url = URL(string: "\(server)/loki/v1/getOpenGroupKey/\(URL(string: openGroupServer)!.host!)")!
|
||||
let request = TSRequest(url: url)
|
||||
let token = "loki" // Tokenless request; use a dummy token
|
||||
request.allHTTPHeaderFields = [ "Content-Type" : "application/json", "Authorization" : "Bearer \(token)" ]
|
||||
return OnionRequestAPI.sendOnionRequest(request, to: server, using: publicKey).map(on: DispatchQueue.global(qos: .userInitiated)) { json in
|
||||
guard let bodyAsString = json["data"] as? String, let bodyAsData = bodyAsString.data(using: .utf8),
|
||||
let body = try JSONSerialization.jsonObject(with: bodyAsData, options: [ .fragmentsAllowed ]) as? JSON else { throw HTTP.Error.invalidJSON }
|
||||
guard let base64EncodedPublicKey = body["data"] as? String else {
|
||||
SNLog("Couldn't parse open group public key from: \(body).")
|
||||
throw Error.parsingFailed
|
||||
}
|
||||
let prefixedPublicKey = Data(base64Encoded: base64EncodedPublicKey)!
|
||||
let hexEncodedPrefixedPublicKey = prefixedPublicKey.toHexString()
|
||||
return hexEncodedPrefixedPublicKey.removing05PrefixIfNeeded()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
|
||||
@objc(LKPublicChat)
|
||||
public final class PublicChat : NSObject, NSCoding {
|
||||
@objc(SNOpenGroup)
|
||||
public final class OpenGroup : NSObject, NSCoding {
|
||||
@objc public let id: String
|
||||
@objc public let idAsData: Data
|
||||
@objc public let channel: UInt64
|
||||
|
@ -39,5 +39,5 @@ public final class PublicChat : NSObject, NSCoding {
|
|||
coder.encode(isDeletable, forKey: "isDeletable")
|
||||
}
|
||||
|
||||
override public var description: String { return "\(displayName) (\(server))" }
|
||||
override public var description: String { "\(displayName) (\(server))" }
|
||||
}
|
|
@ -1,11 +1,12 @@
|
|||
import AFNetworking
|
||||
import PromiseKit
|
||||
import SessionSnodeKit
|
||||
import SessionUtilitiesKit
|
||||
|
||||
@objc(LKPublicChatAPI)
|
||||
public final class PublicChatAPI : DotNetAPI {
|
||||
@objc(SNOpenGroupAPI)
|
||||
public final class OpenGroupAPI : DotNetAPI {
|
||||
private static var moderators: [String:[UInt64:Set<String>]] = [:] // Server URL to (channel ID to set of moderator IDs)
|
||||
|
||||
@objc public static let defaultChats: [PublicChat] = [] // Currently unused
|
||||
|
||||
public static var displayNameUpdatees: [String:Set<String>] = [:]
|
||||
|
||||
// MARK: Settings
|
||||
|
@ -16,71 +17,19 @@ public final class PublicChatAPI : DotNetAPI {
|
|||
|
||||
public static let profilePictureType = "network.loki.messenger.avatar"
|
||||
|
||||
@objc public static let publicChatMessageType = "network.loki.messenger.publicChat"
|
||||
@objc public static let openGroupMessageType = "network.loki.messenger.openGroup"
|
||||
|
||||
// MARK: Convenience
|
||||
private static var userDisplayName: String {
|
||||
let userPublicKey = UserDefaults.standard[.masterHexEncodedPublicKey] ?? getUserHexEncodedPublicKey()
|
||||
return SSKEnvironment.shared.profileManager.profileNameForRecipient(withID: userPublicKey) ?? "Anonymous"
|
||||
}
|
||||
|
||||
// MARK: Database
|
||||
override internal class var authTokenCollection: String { "LokiGroupChatAuthTokenCollection" }
|
||||
|
||||
@objc public static let lastMessageServerIDCollection = "LokiGroupChatLastMessageServerIDCollection"
|
||||
@objc public static let lastDeletionServerIDCollection = "LokiGroupChatLastDeletionServerIDCollection"
|
||||
|
||||
private static func getLastMessageServerID(for group: UInt64, on server: String) -> UInt? {
|
||||
var result: UInt? = nil
|
||||
Storage.read { transaction in
|
||||
result = transaction.object(forKey: "\(server).\(group)", inCollection: lastMessageServerIDCollection) as! UInt?
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
private static func setLastMessageServerID(for group: UInt64, on server: String, to newValue: UInt64, using transaction: YapDatabaseReadWriteTransaction) {
|
||||
transaction.setObject(newValue, forKey: "\(server).\(group)", inCollection: lastMessageServerIDCollection)
|
||||
}
|
||||
|
||||
private static func removeLastMessageServerID(for group: UInt64, on server: String, using transaction: YapDatabaseReadWriteTransaction) {
|
||||
transaction.removeObject(forKey: "\(server).\(group)", inCollection: lastMessageServerIDCollection)
|
||||
}
|
||||
|
||||
private static func getLastDeletionServerID(for group: UInt64, on server: String) -> UInt? {
|
||||
var result: UInt? = nil
|
||||
Storage.read { transaction in
|
||||
result = transaction.object(forKey: "\(server).\(group)", inCollection: lastDeletionServerIDCollection) as! UInt?
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
private static func setLastDeletionServerID(for group: UInt64, on server: String, to newValue: UInt64, using transaction: YapDatabaseReadWriteTransaction) {
|
||||
transaction.setObject(newValue, forKey: "\(server).\(group)", inCollection: lastDeletionServerIDCollection)
|
||||
}
|
||||
|
||||
private static func removeLastDeletionServerID(for group: UInt64, on server: String, using transaction: YapDatabaseReadWriteTransaction) {
|
||||
transaction.removeObject(forKey: "\(server).\(group)", inCollection: lastDeletionServerIDCollection)
|
||||
}
|
||||
|
||||
public static func clearCaches(for channel: UInt64, on server: String) {
|
||||
Storage.writeSync { transaction in
|
||||
removeLastMessageServerID(for: channel, on: server, using: transaction)
|
||||
removeLastDeletionServerID(for: channel, on: server, using: transaction)
|
||||
Storage.removeOpenGroupPublicKey(for: server, using: transaction)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Open Group Public Key Validation
|
||||
public static func getOpenGroupServerPublicKey(for server: String) -> Promise<String> {
|
||||
if let publicKey = Storage.getOpenGroupPublicKey(for: server) {
|
||||
if let publicKey = Configuration.shared.storage.getOpenGroupPublicKey(for: server) {
|
||||
return Promise.value(publicKey)
|
||||
} else {
|
||||
return FileServerAPI.getPublicKey(for: server).then(on: DispatchQueue.global(qos: .default)) { publicKey -> Promise<String> in
|
||||
let url = URL(string: server)!
|
||||
let request = TSRequest(url: url)
|
||||
return OnionRequestAPI.sendOnionRequest(request, to: server, using: publicKey, isJSONRequired: false).map(on: DispatchQueue.global(qos: .default)) { _ -> String in
|
||||
Storage.writeSync { transaction in
|
||||
Storage.setOpenGroupPublicKey(for: server, to: publicKey, using: transaction)
|
||||
Configuration.shared.storage.with { transaction in
|
||||
Configuration.shared.storage.setOpenGroupPublicKey(for: server, to: publicKey, using: transaction)
|
||||
}
|
||||
return publicKey
|
||||
}
|
||||
|
@ -94,61 +43,62 @@ public final class PublicChatAPI : DotNetAPI {
|
|||
return AnyPromise.from(getMessages(for: group, on: server))
|
||||
}
|
||||
|
||||
public static func getMessages(for channel: UInt64, on server: String) -> Promise<[PublicChatMessage]> {
|
||||
public static func getMessages(for channel: UInt64, on server: String) -> Promise<[OpenGroupMessage]> {
|
||||
let storage = Configuration.shared.storage
|
||||
var queryParameters = "include_annotations=1"
|
||||
if let lastMessageServerID = getLastMessageServerID(for: channel, on: server) {
|
||||
if let lastMessageServerID = storage.getLastMessageServerID(for: channel, on: server) {
|
||||
queryParameters += "&since_id=\(lastMessageServerID)"
|
||||
} else {
|
||||
queryParameters += "&count=\(fallbackBatchCount)&include_deleted=0"
|
||||
}
|
||||
return getOpenGroupServerPublicKey(for: server).then(on: DispatchQueue.global(qos: .default)) { serverPublicKey in
|
||||
getAuthToken(for: server).then(on: DispatchQueue.global(qos: .default)) { token -> Promise<[PublicChatMessage]> in
|
||||
getAuthToken(for: server).then(on: DispatchQueue.global(qos: .default)) { token -> Promise<[OpenGroupMessage]> in
|
||||
let url = URL(string: "\(server)/channels/\(channel)/messages?\(queryParameters)")!
|
||||
let request = TSRequest(url: url)
|
||||
request.allHTTPHeaderFields = [ "Content-Type" : "application/json", "Authorization" : "Bearer \(token)" ]
|
||||
return OnionRequestAPI.sendOnionRequest(request, to: server, using: serverPublicKey).map(on: DispatchQueue.global(qos: .default)) { json in
|
||||
guard let rawMessages = json["data"] as? [JSON] else {
|
||||
print("[Loki] Couldn't parse messages for public chat channel with ID: \(channel) on server: \(server) from: \(json).")
|
||||
throw DotNetAPIError.parsingFailed
|
||||
SNLog("Couldn't parse messages for open group channel with ID: \(channel) on server: \(server) from: \(json).")
|
||||
throw Error.parsingFailed
|
||||
}
|
||||
return rawMessages.flatMap { message in
|
||||
return rawMessages.compactMap { message in
|
||||
let isDeleted = (message["is_deleted"] as? Int == 1)
|
||||
guard !isDeleted else { return nil }
|
||||
let dateFormatter = DateFormatter()
|
||||
dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZ"
|
||||
guard let annotations = message["annotations"] as? [JSON], let annotation = annotations.first(where: { $0["type"] as? String == publicChatMessageType }), let value = annotation["value"] as? JSON,
|
||||
guard let annotations = message["annotations"] as? [JSON], let annotation = annotations.first(where: { $0["type"] as? String == openGroupMessageType }), let value = annotation["value"] as? JSON,
|
||||
let serverID = message["id"] as? UInt64, let hexEncodedSignatureData = value["sig"] as? String, let signatureVersion = value["sigver"] as? UInt64,
|
||||
let body = message["text"] as? String, let user = message["user"] as? JSON, let hexEncodedPublicKey = user["username"] as? String,
|
||||
let timestamp = value["timestamp"] as? UInt64, let dateAsString = message["created_at"] as? String, let date = dateFormatter.date(from: dateAsString) else {
|
||||
print("[Loki] Couldn't parse message for public chat channel with ID: \(channel) on server: \(server) from: \(message).")
|
||||
SNLog("Couldn't parse message for open group channel with ID: \(channel) on server: \(server) from: \(message).")
|
||||
return nil
|
||||
}
|
||||
let serverTimestamp = UInt64(date.timeIntervalSince1970) * 1000
|
||||
var profilePicture: PublicChatMessage.ProfilePicture? = nil
|
||||
var profilePicture: OpenGroupMessage.ProfilePicture? = nil
|
||||
let displayName = user["name"] as? String ?? NSLocalizedString("Anonymous", comment: "")
|
||||
if let userAnnotations = user["annotations"] as? [JSON], let profilePictureAnnotation = userAnnotations.first(where: { $0["type"] as? String == profilePictureType }),
|
||||
let profilePictureValue = profilePictureAnnotation["value"] as? JSON, let profileKeyString = profilePictureValue["profileKey"] as? String, let profileKey = Data(base64Encoded: profileKeyString), let url = profilePictureValue["url"] as? String {
|
||||
profilePicture = PublicChatMessage.ProfilePicture(profileKey: profileKey, url: url)
|
||||
profilePicture = OpenGroupMessage.ProfilePicture(profileKey: profileKey, url: url)
|
||||
}
|
||||
let lastMessageServerID = getLastMessageServerID(for: channel, on: server)
|
||||
let lastMessageServerID = storage.getLastMessageServerID(for: channel, on: server)
|
||||
if serverID > (lastMessageServerID ?? 0) {
|
||||
Storage.writeSync { transaction in
|
||||
setLastMessageServerID(for: channel, on: server, to: serverID, using: transaction)
|
||||
storage.with { transaction in
|
||||
storage.setLastMessageServerID(for: channel, on: server, to: serverID, using: transaction)
|
||||
}
|
||||
}
|
||||
let quote: PublicChatMessage.Quote?
|
||||
let quote: OpenGroupMessage.Quote?
|
||||
if let quoteAsJSON = value["quote"] as? JSON, let quotedMessageTimestamp = quoteAsJSON["id"] as? UInt64, let quoteePublicKey = quoteAsJSON["author"] as? String,
|
||||
let quotedMessageBody = quoteAsJSON["text"] as? String {
|
||||
let quotedMessageServerID = message["reply_to"] as? UInt64
|
||||
quote = PublicChatMessage.Quote(quotedMessageTimestamp: quotedMessageTimestamp, quoteePublicKey: quoteePublicKey, quotedMessageBody: quotedMessageBody,
|
||||
quote = OpenGroupMessage.Quote(quotedMessageTimestamp: quotedMessageTimestamp, quoteePublicKey: quoteePublicKey, quotedMessageBody: quotedMessageBody,
|
||||
quotedMessageServerID: quotedMessageServerID)
|
||||
} else {
|
||||
quote = nil
|
||||
}
|
||||
let signature = PublicChatMessage.Signature(data: Data(hex: hexEncodedSignatureData), version: signatureVersion)
|
||||
let signature = OpenGroupMessage.Signature(data: Data(hex: hexEncodedSignatureData), version: signatureVersion)
|
||||
let attachmentsAsJSON = annotations.filter { $0["type"] as? String == attachmentType }
|
||||
let attachments: [PublicChatMessage.Attachment] = attachmentsAsJSON.compactMap { attachmentAsJSON in
|
||||
guard let value = attachmentAsJSON["value"] as? JSON, let kindAsString = value["lokiType"] as? String, let kind = PublicChatMessage.Attachment.Kind(rawValue: kindAsString),
|
||||
let attachments: [OpenGroupMessage.Attachment] = attachmentsAsJSON.compactMap { attachmentAsJSON in
|
||||
guard let value = attachmentAsJSON["value"] as? JSON, let kindAsString = value["lokiType"] as? String, let kind = OpenGroupMessage.Attachment.Kind(rawValue: kindAsString),
|
||||
let serverID = value["id"] as? UInt64, let contentType = value["contentType"] as? String, let size = value["size"] as? UInt, let url = value["url"] as? String else { return nil }
|
||||
let fileName = value["fileName"] as? String ?? UUID().description
|
||||
let width = value["width"] as? UInt ?? 0
|
||||
|
@ -159,25 +109,22 @@ public final class PublicChatAPI : DotNetAPI {
|
|||
let linkPreviewTitle = value["linkPreviewTitle"] as? String
|
||||
if kind == .linkPreview {
|
||||
guard linkPreviewURL != nil && linkPreviewTitle != nil else {
|
||||
print("[Loki] Ignoring public chat message with invalid link preview.")
|
||||
SNLog("Ignoring open group message with invalid link preview.")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return PublicChatMessage.Attachment(kind: kind, server: server, serverID: serverID, contentType: contentType, size: size, fileName: fileName, flags: flags,
|
||||
return OpenGroupMessage.Attachment(kind: kind, server: server, serverID: serverID, contentType: contentType, size: size, fileName: fileName, flags: flags,
|
||||
width: width, height: height, caption: caption, url: url, linkPreviewURL: linkPreviewURL, linkPreviewTitle: linkPreviewTitle)
|
||||
}
|
||||
let result = PublicChatMessage(serverID: serverID, senderPublicKey: hexEncodedPublicKey, displayName: displayName, profilePicture: profilePicture,
|
||||
body: body, type: publicChatMessageType, timestamp: timestamp, quote: quote, attachments: attachments, signature: signature, serverTimestamp: serverTimestamp)
|
||||
let result = OpenGroupMessage(serverID: serverID, senderPublicKey: hexEncodedPublicKey, displayName: displayName, profilePicture: profilePicture,
|
||||
body: body, type: openGroupMessageType, timestamp: timestamp, quote: quote, attachments: attachments, signature: signature, serverTimestamp: serverTimestamp)
|
||||
guard result.hasValidSignature() else {
|
||||
print("[Loki] Ignoring public chat message with invalid signature.")
|
||||
SNLog("Ignoring open group message with invalid signature.")
|
||||
return nil
|
||||
}
|
||||
var existingMessageID: String? = nil
|
||||
Storage.read { transaction in
|
||||
existingMessageID = OWSPrimaryStorage.shared().getIDForMessage(withServerID: UInt(result.serverID!), in: transaction)
|
||||
}
|
||||
let existingMessageID = storage.getIDForMessage(withServerID: UInt(result.serverID!))
|
||||
guard existingMessageID == nil else {
|
||||
print("[Loki] Ignoring duplicate public chat message.")
|
||||
SNLog("Ignoring duplicate open group message.")
|
||||
return nil
|
||||
}
|
||||
return result
|
||||
|
@ -189,18 +136,21 @@ public final class PublicChatAPI : DotNetAPI {
|
|||
|
||||
// MARK: Sending
|
||||
@objc(sendMessage:toGroup:onServer:)
|
||||
public static func objc_sendMessage(_ message: PublicChatMessage, to group: UInt64, on server: String) -> AnyPromise {
|
||||
public static func objc_sendMessage(_ message: OpenGroupMessage, to group: UInt64, on server: String) -> AnyPromise {
|
||||
return AnyPromise.from(sendMessage(message, to: group, on: server))
|
||||
}
|
||||
|
||||
public static func sendMessage(_ message: PublicChatMessage, to channel: UInt64, on server: String) -> Promise<PublicChatMessage> {
|
||||
print("[Loki] Sending message to public chat channel with ID: \(channel) on server: \(server).")
|
||||
let (promise, seal) = Promise<PublicChatMessage>.pending()
|
||||
DispatchQueue.global(qos: .userInitiated).async { [privateKey = userKeyPair.privateKey] in
|
||||
guard let signedMessage = message.sign(with: privateKey) else { return seal.reject(DotNetAPIError.signingFailed) }
|
||||
public static func sendMessage(_ message: OpenGroupMessage, to channel: UInt64, on server: String) -> Promise<OpenGroupMessage> {
|
||||
SNLog("Sending message to open group channel with ID: \(channel) on server: \(server).")
|
||||
let storage = Configuration.shared.storage
|
||||
guard let userKeyPair = storage.getUserKeyPair() else { return Promise(error: Error.generic) }
|
||||
guard let userDisplayName = storage.getUserDisplayName() else { return Promise(error: Error.generic) }
|
||||
let (promise, seal) = Promise<OpenGroupMessage>.pending()
|
||||
DispatchQueue.global(qos: .userInitiated).async { [privateKey = userKeyPair.privateKey()] in
|
||||
guard let signedMessage = message.sign(with: privateKey) else { return seal.reject(Error.signingFailed) }
|
||||
attempt(maxRetryCount: maxRetryCount, recoveringOn: DispatchQueue.global(qos: .default)) {
|
||||
getOpenGroupServerPublicKey(for: server).then(on: DispatchQueue.global(qos: .default)) { serverPublicKey in
|
||||
getAuthToken(for: server).then(on: DispatchQueue.global(qos: .default)) { token -> Promise<PublicChatMessage> in
|
||||
getAuthToken(for: server).then(on: DispatchQueue.global(qos: .default)) { token -> Promise<OpenGroupMessage> in
|
||||
let url = URL(string: "\(server)/channels/\(channel)/messages")!
|
||||
let parameters = signedMessage.toJSON()
|
||||
let request = TSRequest(url: url, method: "POST", parameters: parameters)
|
||||
|
@ -212,11 +162,11 @@ public final class PublicChatAPI : DotNetAPI {
|
|||
dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZ"
|
||||
guard let messageAsJSON = json["data"] as? JSON, let serverID = messageAsJSON["id"] as? UInt64, let body = messageAsJSON["text"] as? String,
|
||||
let dateAsString = messageAsJSON["created_at"] as? String, let date = dateFormatter.date(from: dateAsString) else {
|
||||
print("[Loki] Couldn't parse message for public chat channel with ID: \(channel) on server: \(server) from: \(json).")
|
||||
throw DotNetAPIError.parsingFailed
|
||||
SNLog("Couldn't parse message for open group channel with ID: \(channel) on server: \(server) from: \(json).")
|
||||
throw Error.parsingFailed
|
||||
}
|
||||
let timestamp = UInt64(date.timeIntervalSince1970) * 1000
|
||||
return PublicChatMessage(serverID: serverID, senderPublicKey: getUserHexEncodedPublicKey(), displayName: displayName, profilePicture: signedMessage.profilePicture, body: body, type: publicChatMessageType, timestamp: timestamp, quote: signedMessage.quote, attachments: signedMessage.attachments, signature: signedMessage.signature, serverTimestamp: timestamp)
|
||||
return OpenGroupMessage(serverID: serverID, senderPublicKey: userKeyPair.publicKey()!.toHexString(), displayName: displayName, profilePicture: signedMessage.profilePicture, body: body, type: openGroupMessageType, timestamp: timestamp, quote: signedMessage.quote, attachments: signedMessage.attachments, signature: signedMessage.signature, serverTimestamp: timestamp)
|
||||
}
|
||||
}
|
||||
}.handlingInvalidAuthTokenIfNeeded(for: server)
|
||||
|
@ -231,9 +181,10 @@ public final class PublicChatAPI : DotNetAPI {
|
|||
|
||||
// MARK: Deletion
|
||||
public static func getDeletedMessageServerIDs(for channel: UInt64, on server: String) -> Promise<[UInt64]> {
|
||||
print("[Loki] Getting deleted messages for public chat channel with ID: \(channel) on server: \(server).")
|
||||
SNLog("Getting deleted messages for open group channel with ID: \(channel) on server: \(server).")
|
||||
let storage = Configuration.shared.storage
|
||||
let queryParameters: String
|
||||
if let lastDeletionServerID = getLastDeletionServerID(for: channel, on: server) {
|
||||
if let lastDeletionServerID = storage.getLastDeletionServerID(for: channel, on: server) {
|
||||
queryParameters = "since_id=\(lastDeletionServerID)"
|
||||
} else {
|
||||
queryParameters = "count=\(fallbackBatchCount)"
|
||||
|
@ -245,18 +196,18 @@ public final class PublicChatAPI : DotNetAPI {
|
|||
request.allHTTPHeaderFields = [ "Content-Type" : "application/json", "Authorization" : "Bearer \(token)" ]
|
||||
return OnionRequestAPI.sendOnionRequest(request, to: server, using: serverPublicKey).map(on: DispatchQueue.global(qos: .default)) { json in
|
||||
guard let body = json["body"] as? JSON, let deletions = body["data"] as? [JSON] else {
|
||||
print("[Loki] Couldn't parse deleted messages for public chat channel with ID: \(channel) on server: \(server) from: \(json).")
|
||||
throw DotNetAPIError.parsingFailed
|
||||
SNLog("Couldn't parse deleted messages for open group channel with ID: \(channel) on server: \(server) from: \(json).")
|
||||
throw Error.parsingFailed
|
||||
}
|
||||
return deletions.flatMap { deletion in
|
||||
return deletions.compactMap { deletion in
|
||||
guard let serverID = deletion["id"] as? UInt64, let messageServerID = deletion["message_id"] as? UInt64 else {
|
||||
print("[Loki] Couldn't parse deleted message for public chat channel with ID: \(channel) on server: \(server) from: \(deletion).")
|
||||
SNLog("Couldn't parse deleted message for open group channel with ID: \(channel) on server: \(server) from: \(deletion).")
|
||||
return nil
|
||||
}
|
||||
let lastDeletionServerID = getLastDeletionServerID(for: channel, on: server)
|
||||
let lastDeletionServerID = storage.getLastDeletionServerID(for: channel, on: server)
|
||||
if serverID > (lastDeletionServerID ?? 0) {
|
||||
Storage.writeSync { transaction in
|
||||
setLastDeletionServerID(for: channel, on: server, to: serverID, using: transaction)
|
||||
storage.with { transaction in
|
||||
storage.setLastDeletionServerID(for: channel, on: server, to: serverID, using: transaction)
|
||||
}
|
||||
}
|
||||
return messageServerID
|
||||
|
@ -273,7 +224,7 @@ public final class PublicChatAPI : DotNetAPI {
|
|||
|
||||
public static func deleteMessage(with messageID: UInt, for channel: UInt64, on server: String, isSentByUser: Bool) -> Promise<Void> {
|
||||
let isModerationRequest = !isSentByUser
|
||||
print("[Loki] Deleting message with ID: \(messageID) for public chat channel with ID: \(channel) on server: \(server) (isModerationRequest = \(isModerationRequest)).")
|
||||
SNLog("Deleting message with ID: \(messageID) for open group channel with ID: \(channel) on server: \(server) (isModerationRequest = \(isModerationRequest)).")
|
||||
let urlAsString = isSentByUser ? "\(server)/channels/\(channel)/messages/\(messageID)" : "\(server)/loki/v1/moderation/message/\(messageID)"
|
||||
return attempt(maxRetryCount: maxRetryCount, recoveringOn: DispatchQueue.global(qos: .default)) {
|
||||
getOpenGroupServerPublicKey(for: server).then(on: DispatchQueue.global(qos: .default)) { serverPublicKey in
|
||||
|
@ -282,7 +233,7 @@ public final class PublicChatAPI : DotNetAPI {
|
|||
let request = TSRequest(url: url, method: "DELETE", parameters: [:])
|
||||
request.allHTTPHeaderFields = [ "Content-Type" : "application/json", "Authorization" : "Bearer \(token)" ]
|
||||
return OnionRequestAPI.sendOnionRequest(request, to: server, using: serverPublicKey, isJSONRequired: false).done(on: DispatchQueue.global(qos: .default)) { _ -> Void in
|
||||
print("[Loki] Deleted message with ID: \(messageID) on server: \(server).")
|
||||
SNLog("Deleted message with ID: \(messageID) on server: \(server).")
|
||||
}
|
||||
}
|
||||
}.handlingInvalidAuthTokenIfNeeded(for: server)
|
||||
|
@ -291,10 +242,10 @@ public final class PublicChatAPI : DotNetAPI {
|
|||
|
||||
// MARK: Display Name & Profile Picture
|
||||
public static func getDisplayNames(for channel: UInt64, on server: String) -> Promise<Void> {
|
||||
let publicChatID = "\(server).\(channel)"
|
||||
guard let publicKeys = displayNameUpdatees[publicChatID] else { return Promise.value(()) }
|
||||
displayNameUpdatees[publicChatID] = []
|
||||
print("[Loki] Getting display names for: \(publicKeys).")
|
||||
let openGroupID = "\(server).\(channel)"
|
||||
guard let publicKeys = displayNameUpdatees[openGroupID] else { return Promise.value(()) }
|
||||
displayNameUpdatees[openGroupID] = []
|
||||
SNLog("Getting display names for: \(publicKeys).")
|
||||
return getOpenGroupServerPublicKey(for: server).then(on: DispatchQueue.global(qos: .default)) { serverPublicKey in
|
||||
getAuthToken(for: server).then(on: DispatchQueue.global(qos: .default)) { token -> Promise<Void> in
|
||||
let queryParameters = "ids=\(publicKeys.map { "@\($0)" }.joined(separator: ","))&include_user_annotations=1"
|
||||
|
@ -302,16 +253,17 @@ public final class PublicChatAPI : DotNetAPI {
|
|||
let request = TSRequest(url: url)
|
||||
return OnionRequestAPI.sendOnionRequest(request, to: server, using: serverPublicKey).map(on: DispatchQueue.global(qos: .default)) { json in
|
||||
guard let data = json["data"] as? [JSON] else {
|
||||
print("[Loki] Couldn't parse display names for users: \(publicKeys) from: \(json).")
|
||||
throw DotNetAPIError.parsingFailed
|
||||
SNLog("Couldn't parse display names for users: \(publicKeys) from: \(json).")
|
||||
throw Error.parsingFailed
|
||||
}
|
||||
Storage.writeSync { transaction in
|
||||
let storage = Configuration.shared.storage
|
||||
storage.with { transaction in
|
||||
data.forEach { data in
|
||||
guard let user = data["user"] as? JSON, let hexEncodedPublicKey = user["username"] as? String, let rawDisplayName = user["name"] as? String else { return }
|
||||
let endIndex = hexEncodedPublicKey.endIndex
|
||||
let cutoffIndex = hexEncodedPublicKey.index(endIndex, offsetBy: -8)
|
||||
let displayName = "\(rawDisplayName) (...\(hexEncodedPublicKey[cutoffIndex..<endIndex]))"
|
||||
transaction.setObject(displayName, forKey: hexEncodedPublicKey, inCollection: "\(server).\(channel)")
|
||||
storage.setOpenGroupDisplayName(to: displayName, for: hexEncodedPublicKey, on: channel, server: server, using: transaction)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -325,7 +277,7 @@ public final class PublicChatAPI : DotNetAPI {
|
|||
}
|
||||
|
||||
public static func setDisplayName(to newDisplayName: String?, on server: String) -> Promise<Void> {
|
||||
print("[Loki] Updating display name on server: \(server).")
|
||||
SNLog("Updating display name on server: \(server).")
|
||||
let parameters: JSON = [ "name" : (newDisplayName ?? "") ]
|
||||
return attempt(maxRetryCount: maxRetryCount, recoveringOn: DispatchQueue.global(qos: .default)) {
|
||||
getOpenGroupServerPublicKey(for: server).then(on: DispatchQueue.global(qos: .default)) { serverPublicKey in
|
||||
|
@ -348,7 +300,7 @@ public final class PublicChatAPI : DotNetAPI {
|
|||
}
|
||||
|
||||
public static func setProfilePictureURL(to url: String?, using profileKey: Data, on server: String) -> Promise<Void> {
|
||||
print("[Loki] Updating profile picture on server: \(server).")
|
||||
SNLog("Updating profile picture on server: \(server).")
|
||||
var annotation: JSON = [ "type" : profilePictureType ]
|
||||
if let url = url {
|
||||
annotation["value"] = [ "profileKey" : profileKey.base64EncodedString(), "url" : url ]
|
||||
|
@ -361,7 +313,7 @@ public final class PublicChatAPI : DotNetAPI {
|
|||
let request = TSRequest(url: url, method: "PATCH", parameters: parameters)
|
||||
request.allHTTPHeaderFields = [ "Content-Type" : "application/json", "Authorization" : "Bearer \(token)" ]
|
||||
return OnionRequestAPI.sendOnionRequest(request, to: server, using: serverPublicKey).map(on: DispatchQueue.global(qos: .default)) { _ in }.recover(on: DispatchQueue.global(qos: .default)) { error in
|
||||
print("[Loki] Couldn't update profile picture due to error: \(error).")
|
||||
SNLog("Couldn't update profile picture due to error: \(error).")
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
@ -369,46 +321,16 @@ public final class PublicChatAPI : DotNetAPI {
|
|||
}
|
||||
}
|
||||
|
||||
static func updateProfileIfNeeded(for channel: UInt64, on server: String, from info: PublicChatInfo) {
|
||||
let storage = OWSPrimaryStorage.shared()
|
||||
let publicChatID = "\(server).\(channel)"
|
||||
Storage.writeSync { transaction in
|
||||
// Update user count
|
||||
storage.setUserCount(info.memberCount, forPublicChatWithID: publicChatID, in: transaction)
|
||||
let groupThread = TSGroupThread.getOrCreateThread(withGroupId: publicChatID.data(using: .utf8)!, groupType: .openGroup, transaction: transaction)
|
||||
// Update display name if needed
|
||||
let groupModel = groupThread.groupModel
|
||||
if groupModel.groupName != info.displayName {
|
||||
let newGroupModel = TSGroupModel(title: info.displayName, memberIds: groupModel.groupMemberIds, image: groupModel.groupImage, groupId: groupModel.groupId, groupType: groupModel.groupType, adminIds: groupModel.groupAdminIds)
|
||||
groupThread.groupModel = newGroupModel
|
||||
groupThread.save(with: transaction)
|
||||
}
|
||||
// Download and update profile picture if needed
|
||||
let oldProfilePictureURL = storage.getProfilePictureURL(forPublicChatWithID: publicChatID, in: transaction)
|
||||
if oldProfilePictureURL != info.profilePictureURL || groupModel.groupImage == nil {
|
||||
storage.setProfilePictureURL(info.profilePictureURL, forPublicChatWithID: publicChatID, in: transaction)
|
||||
if let profilePictureURL = info.profilePictureURL {
|
||||
let url = server.hasSuffix("/") ? "\(server)\(profilePictureURL)" : "\(server)/\(profilePictureURL)"
|
||||
FileServerAPI.downloadAttachment(from: url).map2 { data in
|
||||
let attachmentStream = TSAttachmentStream(contentType: OWSMimeTypeImageJpeg, byteCount: UInt32(data.count), sourceFilename: nil, caption: nil, albumMessageId: nil)
|
||||
try attachmentStream.write(data)
|
||||
groupThread.updateAvatar(with: attachmentStream)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Joining & Leaving
|
||||
@objc(getInfoForChannelWithID:onServer:)
|
||||
public static func objc_getInfo(for channel: UInt64, on server: String) -> AnyPromise {
|
||||
return AnyPromise.from(getInfo(for: channel, on: server))
|
||||
}
|
||||
|
||||
public static func getInfo(for channel: UInt64, on server: String) -> Promise<PublicChatInfo> {
|
||||
public static func getInfo(for channel: UInt64, on server: String) -> Promise<OpenGroupInfo> {
|
||||
return attempt(maxRetryCount: maxRetryCount, recoveringOn: DispatchQueue.global(qos: .default)) {
|
||||
getOpenGroupServerPublicKey(for: server).then(on: DispatchQueue.global(qos: .default)) { serverPublicKey in
|
||||
getAuthToken(for: server).then(on: DispatchQueue.global(qos: .default)) { token -> Promise<PublicChatInfo> in
|
||||
getAuthToken(for: server).then(on: DispatchQueue.global(qos: .default)) { token -> Promise<OpenGroupInfo> in
|
||||
let url = URL(string: "\(server)/channels/\(channel)?include_annotations=1")!
|
||||
let request = TSRequest(url: url)
|
||||
request.allHTTPHeaderFields = [ "Content-Type" : "application/json", "Authorization" : "Bearer \(token)" ]
|
||||
|
@ -421,16 +343,16 @@ public final class PublicChatAPI : DotNetAPI {
|
|||
let profilePictureURL = info["avatar"] as? String,
|
||||
let countInfo = data["counts"] as? JSON,
|
||||
let memberCount = countInfo["subscribers"] as? Int else {
|
||||
print("[Loki] Couldn't parse info for public chat channel with ID: \(channel) on server: \(server) from: \(json).")
|
||||
throw DotNetAPIError.parsingFailed
|
||||
SNLog("Couldn't parse info for open group channel with ID: \(channel) on server: \(server) from: \(json).")
|
||||
throw Error.parsingFailed
|
||||
}
|
||||
let storage = OWSPrimaryStorage.shared()
|
||||
Storage.writeSync { transaction in
|
||||
storage.setUserCount(memberCount, forPublicChatWithID: "\(server).\(channel)", in: transaction)
|
||||
let storage = Configuration.shared.storage
|
||||
storage.with { transaction in
|
||||
storage.setUserCount(to: memberCount, forOpenGroupWithID: "\(server).\(channel)", using: transaction)
|
||||
}
|
||||
let publicChatInfo = PublicChatInfo(displayName: displayName, profilePictureURL: profilePictureURL, memberCount: memberCount)
|
||||
updateProfileIfNeeded(for: channel, on: server, from: publicChatInfo)
|
||||
return publicChatInfo
|
||||
let openGroupInfo = OpenGroupInfo(displayName: displayName, profilePictureURL: profilePictureURL, memberCount: memberCount)
|
||||
Configuration.shared.openGroupAPIDelegate.updateProfileIfNeeded(for: channel, on: server, from: openGroupInfo)
|
||||
return openGroupInfo
|
||||
}
|
||||
}
|
||||
}.handlingInvalidAuthTokenIfNeeded(for: server)
|
||||
|
@ -445,7 +367,7 @@ public final class PublicChatAPI : DotNetAPI {
|
|||
let request = TSRequest(url: url, method: "POST", parameters: [:])
|
||||
request.allHTTPHeaderFields = [ "Content-Type" : "application/json", "Authorization" : "Bearer \(token)" ]
|
||||
return OnionRequestAPI.sendOnionRequest(request, to: server, using: serverPublicKey).done(on: DispatchQueue.global(qos: .default)) { _ -> Void in
|
||||
print("[Loki] Joined channel with ID: \(channel) on server: \(server).")
|
||||
SNLog("Joined channel with ID: \(channel) on server: \(server).")
|
||||
}
|
||||
}
|
||||
}.handlingInvalidAuthTokenIfNeeded(for: server)
|
||||
|
@ -460,7 +382,7 @@ public final class PublicChatAPI : DotNetAPI {
|
|||
let request = TSRequest(url: url, method: "DELETE", parameters: [:])
|
||||
request.allHTTPHeaderFields = [ "Content-Type" : "application/json", "Authorization" : "Bearer \(token)" ]
|
||||
return OnionRequestAPI.sendOnionRequest(request, to: server, using: serverPublicKey).done(on: DispatchQueue.global(qos: .default)) { _ -> Void in
|
||||
print("[Loki] Left channel with ID: \(channel) on server: \(server).")
|
||||
SNLog("Left channel with ID: \(channel) on server: \(server).")
|
||||
}
|
||||
}
|
||||
}.handlingInvalidAuthTokenIfNeeded(for: server)
|
||||
|
@ -491,8 +413,8 @@ public final class PublicChatAPI : DotNetAPI {
|
|||
request.allHTTPHeaderFields = [ "Content-Type" : "application/json", "Authorization" : "Bearer \(token)" ]
|
||||
return OnionRequestAPI.sendOnionRequest(request, to: server, using: serverPublicKey).map(on: DispatchQueue.global(qos: .default)) { json in
|
||||
guard let moderators = json["moderators"] as? [String] else {
|
||||
print("[Loki] Couldn't parse moderators for public chat channel with ID: \(channel) on server: \(server) from: \(json).")
|
||||
throw DotNetAPIError.parsingFailed
|
||||
SNLog("Couldn't parse moderators for open group channel with ID: \(channel) on server: \(server) from: \(json).")
|
||||
throw Error.parsingFailed
|
||||
}
|
||||
let moderatorsAsSet = Set(moderators);
|
||||
if self.moderators.keys.contains(server) {
|
||||
|
@ -515,11 +437,14 @@ public final class PublicChatAPI : DotNetAPI {
|
|||
// MARK: Error Handling
|
||||
internal extension Promise {
|
||||
|
||||
internal func handlingInvalidAuthTokenIfNeeded(for server: String) -> Promise<T> {
|
||||
return recover2 { error -> Promise<T> in
|
||||
func handlingInvalidAuthTokenIfNeeded(for server: String) -> Promise<T> {
|
||||
return recover(on: DispatchQueue.global(qos: .userInitiated)) { error -> Promise<T> in
|
||||
if case OnionRequestAPI.Error.httpRequestFailedAtDestination(let statusCode, _) = error, statusCode == 401 || statusCode == 403 {
|
||||
print("[Loki] Auth token for: \(server) expired; dropping it.")
|
||||
PublicChatAPI.removeAuthToken(for: server)
|
||||
SNLog("Auth token for: \(server) expired; dropping it.")
|
||||
let storage = Configuration.shared.storage
|
||||
storage.with { transaction in
|
||||
storage.removeAuthToken(for: server, using: transaction)
|
||||
}
|
||||
}
|
||||
throw error
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
|
||||
public protocol OpenGroupAPIDelegate {
|
||||
|
||||
func updateProfileIfNeeded(for channel: UInt64, on server: String, from info: OpenGroupInfo)
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
|
||||
public struct PublicChatInfo {
|
||||
public struct OpenGroupInfo {
|
||||
public let displayName: String
|
||||
public let profilePictureURL: String?
|
||||
public let memberCount: Int
|
|
@ -1,7 +1,9 @@
|
|||
import PromiseKit
|
||||
import Curve25519Kit
|
||||
import SessionUtilitiesKit
|
||||
|
||||
@objc(LKPublicChatMessage)
|
||||
public final class PublicChatMessage : NSObject {
|
||||
@objc(SNOpenGroupMessage)
|
||||
public final class OpenGroupMessage : NSObject {
|
||||
public let serverID: UInt64?
|
||||
public let senderPublicKey: String
|
||||
public let displayName: String
|
||||
|
@ -74,7 +76,8 @@ public final class PublicChatMessage : NSObject {
|
|||
}
|
||||
|
||||
// MARK: Initialization
|
||||
public init(serverID: UInt64?, senderPublicKey: String, displayName: String, profilePicture: ProfilePicture?, body: String, type: String, timestamp: UInt64, quote: Quote?, attachments: [Attachment], signature: Signature?, serverTimestamp: UInt64) {
|
||||
public init(serverID: UInt64?, senderPublicKey: String, displayName: String, profilePicture: ProfilePicture?, body: String,
|
||||
type: String, timestamp: UInt64, quote: Quote?, attachments: [Attachment], signature: Signature?, serverTimestamp: UInt64) {
|
||||
self.serverID = serverID
|
||||
self.senderPublicKey = senderPublicKey
|
||||
self.displayName = displayName
|
||||
|
@ -89,7 +92,9 @@ public final class PublicChatMessage : NSObject {
|
|||
super.init()
|
||||
}
|
||||
|
||||
@objc public convenience init(senderPublicKey: String, displayName: String, body: String, type: String, timestamp: UInt64, quotedMessageTimestamp: UInt64, quoteePublicKey: String?, quotedMessageBody: String?, quotedMessageServerID: UInt64, signatureData: Data?, signatureVersion: UInt64, serverTimestamp: UInt64) {
|
||||
@objc public convenience init(senderPublicKey: String, displayName: String, body: String, type: String, timestamp: UInt64,
|
||||
quotedMessageTimestamp: UInt64, quoteePublicKey: String?, quotedMessageBody: String?, quotedMessageServerID: UInt64,
|
||||
signatureData: Data?, signatureVersion: UInt64, serverTimestamp: UInt64) {
|
||||
let quote: Quote?
|
||||
if quotedMessageTimestamp != 0, let quoteeHexEncodedPublicKey = quoteePublicKey, let quotedMessageBody = quotedMessageBody {
|
||||
let quotedMessageServerID = (quotedMessageServerID != 0) ? quotedMessageServerID : nil
|
||||
|
@ -107,25 +112,25 @@ public final class PublicChatMessage : NSObject {
|
|||
}
|
||||
|
||||
// MARK: Crypto
|
||||
internal func sign(with privateKey: Data) -> PublicChatMessage? {
|
||||
internal func sign(with privateKey: Data) -> OpenGroupMessage? {
|
||||
guard let data = getValidationData(for: signatureVersion) else {
|
||||
print("[Loki] Failed to sign public chat message.")
|
||||
SNLog("Failed to sign open group message.")
|
||||
return nil
|
||||
}
|
||||
let userKeyPair = OWSIdentityManager.shared().identityKeyPair()!
|
||||
guard let signatureData = try? Ed25519.sign(data, with: userKeyPair) else {
|
||||
print("[Loki] Failed to sign public chat message.")
|
||||
let userKeyPair = Configuration.shared.storage.getUserKeyPair()
|
||||
guard let signatureData = Ed25519.sign(data, with: userKeyPair) else {
|
||||
SNLog("Failed to sign open group message.")
|
||||
return nil
|
||||
}
|
||||
let signature = Signature(data: signatureData, version: signatureVersion)
|
||||
return PublicChatMessage(serverID: serverID, senderPublicKey: senderPublicKey, displayName: displayName, profilePicture: profilePicture, body: body, type: type, timestamp: timestamp, quote: quote, attachments: attachments, signature: signature, serverTimestamp: serverTimestamp)
|
||||
return OpenGroupMessage(serverID: serverID, senderPublicKey: senderPublicKey, displayName: displayName, profilePicture: profilePicture, body: body, type: type, timestamp: timestamp, quote: quote, attachments: attachments, signature: signature, serverTimestamp: serverTimestamp)
|
||||
}
|
||||
|
||||
internal func hasValidSignature() -> Bool {
|
||||
guard let signature = signature else { return false }
|
||||
guard let data = getValidationData(for: signature.version) else { return false }
|
||||
let publicKey = Data(hex: self.senderPublicKey.removing05PrefixIfNeeded())
|
||||
return (try? Ed25519.verifySignature(signature.data, publicKey: publicKey, data: data)) ?? false
|
||||
return Ed25519.verifySignature(signature.data, publicKey: publicKey, data: data)
|
||||
}
|
||||
|
||||
// MARK: JSON
|
||||
|
@ -143,7 +148,6 @@ public final class PublicChatMessage : NSObject {
|
|||
}
|
||||
let annotation: JSON = [ "type" : type, "value" : value ]
|
||||
let attachmentAnnotations: [JSON] = attachments.map { attachment in
|
||||
let type: String
|
||||
var attachmentValue: JSON = [
|
||||
// Fields required by the .NET API
|
||||
"version" : 1, "type" : attachment.dotNETType,
|
||||
|
@ -151,7 +155,7 @@ public final class PublicChatMessage : NSObject {
|
|||
"lokiType" : attachment.kind.rawValue, "server" : attachment.server, "id" : attachment.serverID, "contentType" : attachment.contentType, "size" : attachment.size, "fileName" : attachment.fileName, "width" : attachment.width, "height" : attachment.height, "url" : attachment.url
|
||||
]
|
||||
if let caption = attachment.caption {
|
||||
attachmentValue["caption"] = attachment.caption
|
||||
attachmentValue["caption"] = caption
|
||||
}
|
||||
if let linkPreviewURL = attachment.linkPreviewURL {
|
||||
attachmentValue["linkPreviewUrl"] = linkPreviewURL
|
||||
|
@ -169,7 +173,8 @@ public final class PublicChatMessage : NSObject {
|
|||
}
|
||||
|
||||
// MARK: Convenience
|
||||
@objc public func addAttachment(kind: String, server: String, serverID: UInt64, contentType: String, size: UInt, fileName: String, flags: UInt, width: UInt, height: UInt, caption: String?, url: String, linkPreviewURL: String?, linkPreviewTitle: String?) {
|
||||
@objc public func addAttachment(kind: String, server: String, serverID: UInt64, contentType: String, size: UInt,
|
||||
fileName: String, flags: UInt, width: UInt, height: UInt, caption: String?, url: String, linkPreviewURL: String?, linkPreviewTitle: String?) {
|
||||
guard let kind = Attachment.Kind(rawValue: kind) else { preconditionFailure() }
|
||||
let attachment = Attachment(kind: kind, server: server, serverID: serverID, contentType: contentType, size: size, fileName: fileName, flags: flags, width: width, height: height, caption: caption, url: url, linkPreviewURL: linkPreviewURL, linkPreviewTitle: linkPreviewTitle)
|
||||
attachments.append(attachment)
|
|
@ -6,6 +6,8 @@ public protocol SessionMessagingKitStorageProtocol : SessionStore, PreKeyStore,
|
|||
func withAsync(_ work: (Any) -> Void, completion: () -> Void)
|
||||
|
||||
func getUserPublicKey() -> String?
|
||||
func getUserKeyPair() -> ECKeyPair?
|
||||
func getUserDisplayName() -> String?
|
||||
func getOrGenerateRegistrationID(using transaction: Any) -> UInt32
|
||||
func isClosedGroup(_ publicKey: String) -> Bool
|
||||
func getClosedGroupPrivateKey(for publicKey: String) -> String?
|
||||
|
@ -13,4 +15,19 @@ public protocol SessionMessagingKitStorageProtocol : SessionStore, PreKeyStore,
|
|||
func markJobAsSucceeded(_ job: Job, using transaction: Any)
|
||||
func markJobAsFailed(_ job: Job, using transaction: Any)
|
||||
func getSenderCertificate(for publicKey: String) -> SMKSenderCertificate
|
||||
func getAuthToken(for server: String) -> String?
|
||||
func setAuthToken(for server: String, to newValue: String, using transaction: Any)
|
||||
func removeAuthToken(for server: String, using transaction: Any)
|
||||
func getOpenGroupPublicKey(for server: String) -> String?
|
||||
func setOpenGroupPublicKey(for server: String, to newValue: String, using transaction: Any)
|
||||
func getLastMessageServerID(for group: UInt64, on server: String) -> UInt?
|
||||
func setLastMessageServerID(for group: UInt64, on server: String, to newValue: UInt64, using transaction: Any)
|
||||
func removeLastMessageServerID(for group: UInt64, on server: String, using transaction: Any)
|
||||
func getLastDeletionServerID(for group: UInt64, on server: String) -> UInt64?
|
||||
func setLastDeletionServerID(for group: UInt64, on server: String, to newValue: UInt64, using transaction: Any)
|
||||
func removeLastDeletionServerID(for group: UInt64, on server: String, using transaction: Any)
|
||||
func setUserCount(to newValue: Int, forOpenGroupWithID: String, using transaction: Any)
|
||||
func getIDForMessage(withServerID serverID: UInt) -> UInt?
|
||||
func setOpenGroupDisplayName(to displayName: String, for publicKey: String, on channel: UInt64, server: String, using transaction: Any)
|
||||
func setLastProfilePictureUploadDate(_ date: Date) // Stored in user defaults so no transaction is needed
|
||||
}
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
|
||||
@objc public protocol AttachmentStream {
|
||||
var encryptionKey: Data { get set }
|
||||
var digest: Data { get set }
|
||||
var serverId: UInt64 { get set }
|
||||
var isUploaded: Bool { get set }
|
||||
var downloadURL: String { get set }
|
||||
|
||||
func readDataFromFile() throws -> Data
|
||||
func save()
|
||||
}
|
|
@ -1,107 +1,100 @@
|
|||
import AFNetworking
|
||||
import CryptoSwift
|
||||
import PromiseKit
|
||||
import SessionMetadataKit
|
||||
import SessionProtocolKit
|
||||
import SessionSnodeKit
|
||||
import SessionUtilitiesKit
|
||||
|
||||
/// Base class for `FileServerAPI` and `PublicChatAPI`.
|
||||
/// Base class for `FileServerAPI` and `OpenGroupAPI`.
|
||||
public class DotNetAPI : NSObject {
|
||||
|
||||
internal static var userKeyPair: ECKeyPair { OWSIdentityManager.shared().identityKeyPair()! }
|
||||
|
||||
// MARK: Settings
|
||||
private static let attachmentType = "network.loki"
|
||||
private static let maxRetryCount: UInt = 4
|
||||
|
||||
// MARK: Error
|
||||
@objc(LKDotNetAPIError)
|
||||
public class DotNetAPIError : NSError { // Not called `Error` for Obj-C interoperablity
|
||||
|
||||
@objc public static let generic = DotNetAPIError(domain: "DotNetAPIErrorDomain", code: 1, userInfo: [ NSLocalizedDescriptionKey : "An error occurred." ])
|
||||
@objc public static let parsingFailed = DotNetAPIError(domain: "DotNetAPIErrorDomain", code: 2, userInfo: [ NSLocalizedDescriptionKey : "Invalid file server response." ])
|
||||
@objc public static let signingFailed = DotNetAPIError(domain: "DotNetAPIErrorDomain", code: 3, userInfo: [ NSLocalizedDescriptionKey : "Couldn't sign message." ])
|
||||
@objc public static let encryptionFailed = DotNetAPIError(domain: "DotNetAPIErrorDomain", code: 4, userInfo: [ NSLocalizedDescriptionKey : "Couldn't encrypt file." ])
|
||||
@objc public static let decryptionFailed = DotNetAPIError(domain: "DotNetAPIErrorDomain", code: 5, userInfo: [ NSLocalizedDescriptionKey : "Couldn't decrypt file." ])
|
||||
@objc public static let maxFileSizeExceeded = DotNetAPIError(domain: "DotNetAPIErrorDomain", code: 6, userInfo: [ NSLocalizedDescriptionKey : "Maximum file size exceeded." ])
|
||||
}
|
||||
public enum Error : LocalizedError {
|
||||
case generic
|
||||
case parsingFailed
|
||||
case signingFailed
|
||||
case encryptionFailed
|
||||
case decryptionFailed
|
||||
case maxFileSizeExceeded
|
||||
|
||||
// MARK: Storage
|
||||
/// To be overridden by subclasses.
|
||||
internal class var authTokenCollection: String { preconditionFailure("authTokenCollection is abstract and must be overridden.") }
|
||||
|
||||
internal static func getAuthToken(for server: String) -> Promise<String> {
|
||||
if let token = getAuthTokenFromDatabase(for: server) {
|
||||
return Promise.value(token)
|
||||
} else {
|
||||
return requestNewAuthToken(for: server).then2 { submitAuthToken($0, for: server) }.map2 { token in
|
||||
Storage.writeSync { transaction in
|
||||
setAuthToken(for: server, to: token, in: transaction)
|
||||
}
|
||||
return token
|
||||
public var errorDescription: String? {
|
||||
switch self {
|
||||
case .generic: return "An error occurred."
|
||||
case .parsingFailed: return "Invalid file server response."
|
||||
case .signingFailed: return "Couldn't sign message."
|
||||
case .encryptionFailed: return "Couldn't encrypt file."
|
||||
case .decryptionFailed: return "Couldn't decrypt file."
|
||||
case .maxFileSizeExceeded: return "Maximum file size exceeded."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static func getAuthTokenFromDatabase(for server: String) -> String? {
|
||||
var result: String? = nil
|
||||
Storage.read { transaction in
|
||||
result = transaction.object(forKey: server, inCollection: authTokenCollection) as? String
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
private static func setAuthToken(for server: String, to newValue: String, in transaction: YapDatabaseReadWriteTransaction) {
|
||||
transaction.setObject(newValue, forKey: server, inCollection: authTokenCollection)
|
||||
}
|
||||
|
||||
public static func removeAuthToken(for server: String) {
|
||||
Storage.writeSync { transaction in
|
||||
transaction.removeObject(forKey: server, inCollection: authTokenCollection)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Lifecycle
|
||||
override private init() { }
|
||||
|
||||
// MARK: Private API
|
||||
private static func requestNewAuthToken(for server: String) -> Promise<String> {
|
||||
print("[Loki] Requesting auth token for server: \(server).")
|
||||
let queryParameters = "pubKey=\(getUserHexEncodedPublicKey())"
|
||||
SNLog("Requesting auth token for server: \(server).")
|
||||
guard let userKeyPair = Configuration.shared.storage.getUserKeyPair() else { return Promise(error: Error.generic) }
|
||||
let queryParameters = "pubKey=\(userKeyPair.publicKey().toHexString())"
|
||||
let url = URL(string: "\(server)/loki/v1/get_challenge?\(queryParameters)")!
|
||||
let request = TSRequest(url: url)
|
||||
let serverPublicKeyPromise = (server == FileServerAPI.server) ? Promise.value(FileServerAPI.fileServerPublicKey)
|
||||
: PublicChatAPI.getOpenGroupServerPublicKey(for: server)
|
||||
return serverPublicKeyPromise.then2 { serverPublicKey in
|
||||
let serverPublicKeyPromise = (server == FileServerAPI.server) ? Promise.value(FileServerAPI.publicKey)
|
||||
: OpenGroupAPI.getOpenGroupServerPublicKey(for: server)
|
||||
return serverPublicKeyPromise.then(on: DispatchQueue.global(qos: .userInitiated)) { serverPublicKey in
|
||||
OnionRequestAPI.sendOnionRequest(request, to: server, using: serverPublicKey)
|
||||
}.map2 { json in
|
||||
}.map(on: DispatchQueue.global(qos: .userInitiated)) { json in
|
||||
guard let base64EncodedChallenge = json["cipherText64"] as? String, let base64EncodedServerPublicKey = json["serverPubKey64"] as? String,
|
||||
let challenge = Data(base64Encoded: base64EncodedChallenge), var serverPublicKey = Data(base64Encoded: base64EncodedServerPublicKey) else {
|
||||
throw DotNetAPIError.parsingFailed
|
||||
throw Error.parsingFailed
|
||||
}
|
||||
// Discard the "05" prefix if needed
|
||||
if serverPublicKey.count == 33 {
|
||||
let hexEncodedServerPublicKey = serverPublicKey.toHexString()
|
||||
serverPublicKey = Data.data(fromHex: hexEncodedServerPublicKey.substring(from: 2))!
|
||||
let startIndex = hexEncodedServerPublicKey.index(hexEncodedServerPublicKey.startIndex, offsetBy: 2)
|
||||
serverPublicKey = Data.data(fromHex: String(hexEncodedServerPublicKey[startIndex..<hexEncodedServerPublicKey.endIndex]))!
|
||||
}
|
||||
// The challenge is prefixed by the 16 bit IV
|
||||
guard let tokenAsData = try? DiffieHellman.decrypt(challenge, publicKey: serverPublicKey, privateKey: userKeyPair.privateKey),
|
||||
guard let tokenAsData = try? DiffieHellman.decrypt(challenge, publicKey: serverPublicKey, privateKey: userKeyPair.privateKey()),
|
||||
let token = String(bytes: tokenAsData, encoding: .utf8) else {
|
||||
throw DotNetAPIError.decryptionFailed
|
||||
throw Error.decryptionFailed
|
||||
}
|
||||
return token
|
||||
}
|
||||
}
|
||||
|
||||
private static func submitAuthToken(_ token: String, for server: String) -> Promise<String> {
|
||||
print("[Loki] Submitting auth token for server: \(server).")
|
||||
SNLog("Submitting auth token for server: \(server).")
|
||||
let url = URL(string: "\(server)/loki/v1/submit_challenge")!
|
||||
let parameters = [ "pubKey" : getUserHexEncodedPublicKey(), "token" : token ]
|
||||
guard let userPublicKey = Configuration.shared.storage.getUserPublicKey() else { return Promise(error: Error.generic) }
|
||||
let parameters = [ "pubKey" : userPublicKey, "token" : token ]
|
||||
let request = TSRequest(url: url, method: "POST", parameters: parameters)
|
||||
let serverPublicKeyPromise = (server == FileServerAPI.server) ? Promise.value(FileServerAPI.fileServerPublicKey)
|
||||
: PublicChatAPI.getOpenGroupServerPublicKey(for: server)
|
||||
return serverPublicKeyPromise.then2 { serverPublicKey in
|
||||
let serverPublicKeyPromise = (server == FileServerAPI.server) ? Promise.value(FileServerAPI.publicKey)
|
||||
: OpenGroupAPI.getOpenGroupServerPublicKey(for: server)
|
||||
return serverPublicKeyPromise.then(on: DispatchQueue.global(qos: .userInitiated)) { serverPublicKey in
|
||||
OnionRequestAPI.sendOnionRequest(request, to: server, using: serverPublicKey)
|
||||
}.map2 { _ in token }
|
||||
}.map(on: DispatchQueue.global(qos: .userInitiated)) { _ in token }
|
||||
}
|
||||
|
||||
// MARK: Public API
|
||||
public static func getAuthToken(for server: String) -> Promise<String> {
|
||||
let storage = Configuration.shared.storage
|
||||
if let token = storage.getAuthToken(for: server) {
|
||||
return Promise.value(token)
|
||||
} else {
|
||||
return requestNewAuthToken(for: server).then(on: DispatchQueue.global(qos: .userInitiated)) { submitAuthToken($0, for: server) }.map(on: DispatchQueue.global(qos: .userInitiated)) { token in
|
||||
storage.with { transaction in
|
||||
storage.setAuthToken(for: server, to: token, using: transaction)
|
||||
}
|
||||
return token
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@objc(downloadAttachmentFrom:)
|
||||
public static func objc_downloadAttachment(from url: String) -> AnyPromise {
|
||||
return AnyPromise.from(downloadAttachment(from: url))
|
||||
|
@ -119,17 +112,17 @@ public class DotNetAPI : NSObject {
|
|||
}
|
||||
let request = AFHTTPRequestSerializer().request(withMethod: "GET", urlString: sanitizedURL, parameters: nil, error: &error)
|
||||
if let error = error {
|
||||
print("[Loki] Couldn't download attachment due to error: \(error).")
|
||||
SNLog("Couldn't download attachment due to error: \(error).")
|
||||
return Promise(error: error)
|
||||
}
|
||||
let serverPublicKeyPromise = FileServerAPI.server.contains(host) ? Promise.value(FileServerAPI.fileServerPublicKey)
|
||||
: PublicChatAPI.getOpenGroupServerPublicKey(for: host)
|
||||
return attempt(maxRetryCount: maxRetryCount, recoveringOn: SnodeAPI.workQueue) {
|
||||
serverPublicKeyPromise.then2 { serverPublicKey in
|
||||
return OnionRequestAPI.sendOnionRequest(request, to: host, using: serverPublicKey, isJSONRequired: false).map2 { json in
|
||||
let serverPublicKeyPromise = FileServerAPI.server.contains(host) ? Promise.value(FileServerAPI.publicKey)
|
||||
: OpenGroupAPI.getOpenGroupServerPublicKey(for: host)
|
||||
return attempt(maxRetryCount: maxRetryCount, recoveringOn: DispatchQueue.global(qos: .userInitiated)) {
|
||||
serverPublicKeyPromise.then(on: DispatchQueue.global(qos: .userInitiated)) { serverPublicKey in
|
||||
return OnionRequestAPI.sendOnionRequest(request, to: host, using: serverPublicKey, isJSONRequired: false).map(on: DispatchQueue.global(qos: .userInitiated)) { json in
|
||||
guard let body = json["body"] as? JSON, let data = body["data"] as? [UInt8] else {
|
||||
print("[Loki] Couldn't parse attachment from: \(json).")
|
||||
throw DotNetAPIError.parsingFailed
|
||||
SNLog("Couldn't parse attachment from: \(json).")
|
||||
throw Error.parsingFailed
|
||||
}
|
||||
return Data(data)
|
||||
}
|
||||
|
@ -138,27 +131,27 @@ public class DotNetAPI : NSObject {
|
|||
}
|
||||
|
||||
@objc(uploadAttachment:withID:toServer:)
|
||||
public static func objc_uploadAttachment(_ attachment: TSAttachmentStream, with attachmentID: String, to server: String) -> AnyPromise {
|
||||
public static func objc_uploadAttachment(_ attachment: AttachmentStream, with attachmentID: String, to server: String) -> AnyPromise {
|
||||
return AnyPromise.from(uploadAttachment(attachment, with: attachmentID, to: server))
|
||||
}
|
||||
|
||||
public static func uploadAttachment(_ attachment: TSAttachmentStream, with attachmentID: String, to server: String) -> Promise<Void> {
|
||||
public static func uploadAttachment(_ attachment: AttachmentStream, with attachmentID: String, to server: String) -> Promise<Void> {
|
||||
let isEncryptionRequired = (server == FileServerAPI.server)
|
||||
return Promise<Void>() { seal in
|
||||
func proceed(with token: String) {
|
||||
// Get the attachment
|
||||
let data: Data
|
||||
guard let unencryptedAttachmentData = try? attachment.readDataFromFile() else {
|
||||
print("[Loki] Couldn't read attachment from disk.")
|
||||
return seal.reject(DotNetAPIError.generic)
|
||||
SNLog("Couldn't read attachment from disk.")
|
||||
return seal.reject(Error.generic)
|
||||
}
|
||||
// Encrypt the attachment if needed
|
||||
if isEncryptionRequired {
|
||||
var encryptionKey = NSData()
|
||||
var digest = NSData()
|
||||
guard let encryptedAttachmentData = Cryptography.encryptAttachmentData(unencryptedAttachmentData, outKey: &encryptionKey, outDigest: &digest) else {
|
||||
print("[Loki] Couldn't encrypt attachment.")
|
||||
return seal.reject(DotNetAPIError.encryptionFailed)
|
||||
SNLog("Couldn't encrypt attachment.")
|
||||
return seal.reject(Error.encryptionFailed)
|
||||
}
|
||||
attachment.encryptionKey = encryptionKey as Data
|
||||
attachment.digest = digest as Data
|
||||
|
@ -167,36 +160,36 @@ public class DotNetAPI : NSObject {
|
|||
data = unencryptedAttachmentData
|
||||
}
|
||||
// Check the file size if needed
|
||||
print("[Loki] File size: \(data.count) bytes.")
|
||||
SNLog("File size: \(data.count) bytes.")
|
||||
if Double(data.count) > Double(FileServerAPI.maxFileSize) / FileServerAPI.fileSizeORMultiplier {
|
||||
return seal.reject(DotNetAPIError.maxFileSizeExceeded)
|
||||
return seal.reject(Error.maxFileSizeExceeded)
|
||||
}
|
||||
// Create the request
|
||||
let url = "\(server)/files"
|
||||
let parameters: JSON = [ "type" : attachmentType, "Content-Type" : "application/binary" ]
|
||||
var error: NSError?
|
||||
var request = AFHTTPRequestSerializer().multipartFormRequest(withMethod: "POST", urlString: url, parameters: parameters, constructingBodyWith: { formData in
|
||||
let request = AFHTTPRequestSerializer().multipartFormRequest(withMethod: "POST", urlString: url, parameters: parameters, constructingBodyWith: { formData in
|
||||
let uuid = UUID().uuidString
|
||||
print("[Loki] File UUID: \(uuid).")
|
||||
SNLog("File UUID: \(uuid).")
|
||||
formData.appendPart(withFileData: data, name: "content", fileName: uuid, mimeType: "application/binary")
|
||||
}, error: &error)
|
||||
request.addValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
|
||||
if let error = error {
|
||||
print("[Loki] Couldn't upload attachment due to error: \(error).")
|
||||
SNLog("Couldn't upload attachment due to error: \(error).")
|
||||
return seal.reject(error)
|
||||
}
|
||||
// Send the request
|
||||
let serverPublicKeyPromise = (server == FileServerAPI.server) ? Promise.value(FileServerAPI.fileServerPublicKey)
|
||||
: PublicChatAPI.getOpenGroupServerPublicKey(for: server)
|
||||
let serverPublicKeyPromise = (server == FileServerAPI.server) ? Promise.value(FileServerAPI.publicKey)
|
||||
: OpenGroupAPI.getOpenGroupServerPublicKey(for: server)
|
||||
attachment.isUploaded = false
|
||||
attachment.save()
|
||||
let _ = serverPublicKeyPromise.then2 { serverPublicKey in
|
||||
let _ = serverPublicKeyPromise.then(on: DispatchQueue.global(qos: .userInitiated)) { serverPublicKey in
|
||||
OnionRequestAPI.sendOnionRequest(request, to: server, using: serverPublicKey)
|
||||
}.done2 { json in
|
||||
}.done(on: DispatchQueue.global(qos: .userInitiated)) { json in
|
||||
// Parse the server ID & download URL
|
||||
guard let data = json["data"] as? JSON, let serverID = data["id"] as? UInt64, let downloadURL = data["url"] as? String else {
|
||||
print("[Loki] Couldn't parse attachment from: \(json).")
|
||||
return seal.reject(DotNetAPIError.parsingFailed)
|
||||
SNLog("Couldn't parse attachment from: \(json).")
|
||||
return seal.reject(Error.parsingFailed)
|
||||
}
|
||||
// Update the attachment
|
||||
attachment.serverId = serverID
|
||||
|
@ -204,7 +197,7 @@ public class DotNetAPI : NSObject {
|
|||
attachment.downloadURL = downloadURL
|
||||
attachment.save()
|
||||
seal.fulfill(())
|
||||
}.catch2 { error in
|
||||
}.catch(on: DispatchQueue.global(qos: .userInitiated)) { error in
|
||||
seal.reject(error)
|
||||
}
|
||||
}
|
||||
|
@ -215,8 +208,8 @@ public class DotNetAPI : NSObject {
|
|||
} else {
|
||||
getAuthToken(for: server).done(on: DispatchQueue.global(qos: .userInitiated)) { token in
|
||||
proceed(with: token)
|
||||
}.catch2 { error in
|
||||
print("[Loki] Couldn't upload attachment due to error: \(error).")
|
||||
}.catch(on: DispatchQueue.global(qos: .userInitiated)) { error in
|
||||
SNLog("Couldn't upload attachment due to error: \(error).")
|
||||
seal.reject(error)
|
||||
}
|
||||
}
|
|
@ -49,7 +49,7 @@ public enum SharedSenderKeys {
|
|||
#endif
|
||||
guard let ratchet = Configuration.shared.storage.getClosedGroupRatchet(for: groupPublicKey, senderPublicKey: senderPublicKey, from: .current) else {
|
||||
let error = RatchetingError.loadingFailed(groupPublicKey: groupPublicKey, senderPublicKey: senderPublicKey)
|
||||
print("[Loki] \(error.errorDescription!)")
|
||||
SNLog("\(error.errorDescription!)")
|
||||
throw error
|
||||
}
|
||||
do {
|
||||
|
@ -57,7 +57,7 @@ public enum SharedSenderKeys {
|
|||
Configuration.shared.storage.setClosedGroupRatchet(for: groupPublicKey, senderPublicKey: senderPublicKey, ratchet: result, in: .current, using: transaction)
|
||||
return result
|
||||
} catch {
|
||||
print("[Loki] Couldn't step ratchet due to error: \(error).")
|
||||
SNLog("Couldn't step ratchet due to error: \(error).")
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
@ -70,14 +70,14 @@ public enum SharedSenderKeys {
|
|||
let collection: ClosedGroupRatchetCollectionType = (isRetry) ? .old : .current
|
||||
guard let ratchet = Configuration.shared.storage.getClosedGroupRatchet(for: groupPublicKey, senderPublicKey: senderPublicKey, from: collection) else {
|
||||
let error = RatchetingError.loadingFailed(groupPublicKey: groupPublicKey, senderPublicKey: senderPublicKey)
|
||||
print("[Loki] \(error.errorDescription!)")
|
||||
SNLog("\(error.errorDescription!)")
|
||||
throw error
|
||||
}
|
||||
if targetKeyIndex < ratchet.keyIndex {
|
||||
// There's no need to advance the ratchet if this is invoked for an old key index
|
||||
guard ratchet.messageKeys.count > targetKeyIndex else {
|
||||
let error = RatchetingError.messageKeyMissing(targetKeyIndex: targetKeyIndex, groupPublicKey: groupPublicKey, senderPublicKey: senderPublicKey)
|
||||
print("[Loki] \(error.errorDescription!)")
|
||||
SNLog("\(error.errorDescription!)")
|
||||
throw error
|
||||
}
|
||||
return ratchet
|
||||
|
@ -89,7 +89,7 @@ public enum SharedSenderKeys {
|
|||
result = try step(result)
|
||||
currentKeyIndex = result.keyIndex
|
||||
} catch {
|
||||
print("[Loki] Couldn't step ratchet due to error: \(error).")
|
||||
SNLog("Couldn't step ratchet due to error: \(error).")
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
|
|
@ -35,7 +35,7 @@ import SessionUtilitiesKit
|
|||
do {
|
||||
return try DiffieHellman.encrypt(plaintext, using: symmetricKey)
|
||||
} catch {
|
||||
print("[Loki] Couldn't encrypt message using fallback session cipher due to error: \(error).")
|
||||
SNLog("Couldn't encrypt message using fallback session cipher due to error: \(error).")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
@ -45,7 +45,7 @@ import SessionUtilitiesKit
|
|||
do {
|
||||
return try DiffieHellman.decrypt(ivAndCiphertext, using: symmetricKey)
|
||||
} catch {
|
||||
print("[Loki] Couldn't decrypt message using fallback session cipher due to error: \(error).")
|
||||
SNLog("Couldn't decrypt message using fallback session cipher due to error: \(error).")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import Foundation
|
||||
|
||||
@objc(LKSessionCipher)
|
||||
@objc(SNSessionCipher)
|
||||
public final class LokiSessionCipher : SessionCipher {
|
||||
private let sessionResetImplementation: SessionRestorationProtocol?
|
||||
private let sessionStore: SessionStore
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
|
||||
@objc(LKSessionRestorationProtocol)
|
||||
@objc(SNSessionRestorationProtocol)
|
||||
public protocol SessionRestorationProtocol {
|
||||
|
||||
func validatePreKeyWhisperMessage(for publicKey: String, preKeyWhisperMessage: PreKeyWhisperMessage, using transaction: Any) throws
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
|
||||
@objc(LKSessionRestorationStatus)
|
||||
@objc(SNSessionRestorationStatus)
|
||||
public enum SessionRestorationStatus : Int {
|
||||
case none, initiated, requestReceived
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@ import PromiseKit
|
|||
|
||||
public extension AnyPromise {
|
||||
|
||||
public static func from<T : Any>(_ promise: Promise<T>) -> AnyPromise {
|
||||
static func from<T : Any>(_ promise: Promise<T>) -> AnyPromise {
|
||||
let result = AnyPromise(promise)
|
||||
result.retainUntilComplete()
|
||||
return result
|
|
@ -0,0 +1,17 @@
|
|||
import PromiseKit
|
||||
|
||||
public extension AnyPromise {
|
||||
|
||||
/**
|
||||
* Sometimes there isn't a straightforward candidate to retain a promise. In that case we tell the
|
||||
* promise to self retain until it completes, to avoid the risk it's GC'd before completion.
|
||||
*/
|
||||
@objc
|
||||
func retainUntilComplete() {
|
||||
var retainCycle: AnyPromise? = self
|
||||
_ = self.ensure {
|
||||
assert(retainCycle != nil)
|
||||
retainCycle = nil
|
||||
}
|
||||
}
|
||||
}
|
|
@ -144,7 +144,7 @@ private extension String {
|
|||
}
|
||||
}
|
||||
|
||||
@objc(LKMnemonic)
|
||||
@objc(SNMnemonic)
|
||||
public final class ObjCMnemonic : NSObject {
|
||||
|
||||
override private init() { }
|
||||
|
|
|
@ -654,6 +654,16 @@
|
|||
C3A71D752558A0F60043A11F /* OWSUnidentifiedDelivery.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3A71D732558A0F60043A11F /* OWSUnidentifiedDelivery.pb.swift */; };
|
||||
C3A71F892558BA9F0043A11F /* Mnemonic.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3A71F882558BA9F0043A11F /* Mnemonic.swift */; };
|
||||
C3A7211A2558BCA10043A11F /* DiffieHellman.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3A71D662558A0170043A11F /* DiffieHellman.swift */; };
|
||||
C3A721382558BDFA0043A11F /* OpenGroupMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3A721342558BDF90043A11F /* OpenGroupMessage.swift */; };
|
||||
C3A721392558BDFA0043A11F /* OpenGroupAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3A721352558BDF90043A11F /* OpenGroupAPI.swift */; };
|
||||
C3A7213A2558BDFA0043A11F /* OpenGroupInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3A721362558BDFA0043A11F /* OpenGroupInfo.swift */; };
|
||||
C3A7213B2558BDFA0043A11F /* OpenGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3A721372558BDFA0043A11F /* OpenGroup.swift */; };
|
||||
C3A721902558C0CD0043A11F /* FileServerAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3A7218F2558C0CD0043A11F /* FileServerAPI.swift */; };
|
||||
C3A7219A2558C1660043A11F /* AnyPromise+Conversion.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3A721992558C1660043A11F /* AnyPromise+Conversion.swift */; };
|
||||
C3A7222A2558C1E40043A11F /* DotNetAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3A722292558C1E40043A11F /* DotNetAPI.swift */; };
|
||||
C3A7225E2558C38D0043A11F /* AnyPromise+Retaining.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3A7225D2558C38D0043A11F /* AnyPromise+Retaining.swift */; };
|
||||
C3A722802558C4E10043A11F /* AttachmentStream.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3A7227F2558C4E10043A11F /* AttachmentStream.swift */; };
|
||||
C3A722922558C8940043A11F /* OpenGroupAPIDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3A722912558C8940043A11F /* OpenGroupAPIDelegate.swift */; };
|
||||
C3AABDDF2553ECF00042FF4C /* Array+Description.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2A5D12553860800C340D1 /* Array+Description.swift */; };
|
||||
C3BBE0762554CDA60050F1E3 /* Configuration.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3BBE0752554CDA60050F1E3 /* Configuration.swift */; };
|
||||
C3BBE0802554CDD70050F1E3 /* Storage.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3BBE07F2554CDD70050F1E3 /* Storage.swift */; };
|
||||
|
@ -1693,6 +1703,16 @@
|
|||
C3A71D722558A0F60043A11F /* SMKProto.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = SMKProto.swift; path = Protos/SMKProto.swift; sourceTree = "<group>"; };
|
||||
C3A71D732558A0F60043A11F /* OWSUnidentifiedDelivery.pb.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = OWSUnidentifiedDelivery.pb.swift; path = Protos/OWSUnidentifiedDelivery.pb.swift; sourceTree = "<group>"; };
|
||||
C3A71F882558BA9F0043A11F /* Mnemonic.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Mnemonic.swift; sourceTree = "<group>"; };
|
||||
C3A721342558BDF90043A11F /* OpenGroupMessage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OpenGroupMessage.swift; sourceTree = "<group>"; };
|
||||
C3A721352558BDF90043A11F /* OpenGroupAPI.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OpenGroupAPI.swift; sourceTree = "<group>"; };
|
||||
C3A721362558BDFA0043A11F /* OpenGroupInfo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OpenGroupInfo.swift; sourceTree = "<group>"; };
|
||||
C3A721372558BDFA0043A11F /* OpenGroup.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OpenGroup.swift; sourceTree = "<group>"; };
|
||||
C3A7218F2558C0CD0043A11F /* FileServerAPI.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FileServerAPI.swift; sourceTree = "<group>"; };
|
||||
C3A721992558C1660043A11F /* AnyPromise+Conversion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AnyPromise+Conversion.swift"; sourceTree = "<group>"; };
|
||||
C3A722292558C1E40043A11F /* DotNetAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DotNetAPI.swift; sourceTree = "<group>"; };
|
||||
C3A7225D2558C38D0043A11F /* AnyPromise+Retaining.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AnyPromise+Retaining.swift"; sourceTree = "<group>"; };
|
||||
C3A7227F2558C4E10043A11F /* AttachmentStream.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachmentStream.swift; sourceTree = "<group>"; };
|
||||
C3A722912558C8940043A11F /* OpenGroupAPIDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenGroupAPIDelegate.swift; sourceTree = "<group>"; };
|
||||
C3AA6BB824CE8F1B002358B6 /* Migrating Translations from Android.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = "Migrating Translations from Android.md"; sourceTree = "<group>"; };
|
||||
C3AECBEA24EF5244005743DE /* fa */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fa; path = translations/fa.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
C3BBE0752554CDA60050F1E3 /* Configuration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Configuration.swift; sourceTree = "<group>"; };
|
||||
|
@ -3377,9 +3397,31 @@
|
|||
name = Metadata;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
C3A721332558BDDF0043A11F /* Open Groups */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
C3A721372558BDFA0043A11F /* OpenGroup.swift */,
|
||||
C3A721352558BDF90043A11F /* OpenGroupAPI.swift */,
|
||||
C3A722912558C8940043A11F /* OpenGroupAPIDelegate.swift */,
|
||||
C3A721362558BDFA0043A11F /* OpenGroupInfo.swift */,
|
||||
C3A721342558BDF90043A11F /* OpenGroupMessage.swift */,
|
||||
);
|
||||
path = "Open Groups";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
C3A7215C2558C0AC0043A11F /* File Server */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
C3A7218F2558C0CD0043A11F /* FileServerAPI.swift */,
|
||||
);
|
||||
path = "File Server";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
C3BBE0B32554F0D30050F1E3 /* Utilities */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
C3A7227F2558C4E10043A11F /* AttachmentStream.swift */,
|
||||
C3A722292558C1E40043A11F /* DotNetAPI.swift */,
|
||||
C3BBE0C62554F1570050F1E3 /* FixedWidthInteger+BigEndian.swift */,
|
||||
C3A71D0A2558989C0043A11F /* MessageWrapper.swift */,
|
||||
C3BBE0B42554F0E10050F1E3 /* ProofOfWork.swift */,
|
||||
|
@ -3432,6 +3474,8 @@
|
|||
children = (
|
||||
C3C2A68B255388D500C340D1 /* Meta */,
|
||||
C3C2A5D72553860B00C340D1 /* AESGCM.swift */,
|
||||
C3A721992558C1660043A11F /* AnyPromise+Conversion.swift */,
|
||||
C3A7225D2558C38D0043A11F /* AnyPromise+Retaining.swift */,
|
||||
C3C2A5D12553860800C340D1 /* Array+Description.swift */,
|
||||
C3C2ABD12553C6C900C340D1 /* Data+SecureRandom.swift */,
|
||||
C3C2A5D52553860A00C340D1 /* Dictionary+Description.swift */,
|
||||
|
@ -3473,6 +3517,8 @@
|
|||
C300A5BB2554AFFB00555489 /* Messages */,
|
||||
C300A5F02554B08500555489 /* Sending & Receiving */,
|
||||
C352A2F325574B3300338F3E /* Jobs */,
|
||||
C3A7215C2558C0AC0043A11F /* File Server */,
|
||||
C3A721332558BDDF0043A11F /* Open Groups */,
|
||||
C3BBE0B32554F0D30050F1E3 /* Utilities */,
|
||||
);
|
||||
path = SessionMessagingKit;
|
||||
|
@ -5025,6 +5071,7 @@
|
|||
C352A36D2557858E00338F3E /* NSTimer+Proxying.m in Sources */,
|
||||
C3C2ABD22553C6C900C340D1 /* Data+SecureRandom.swift in Sources */,
|
||||
C3A7211A2558BCA10043A11F /* DiffieHellman.swift in Sources */,
|
||||
C3A7225E2558C38D0043A11F /* AnyPromise+Retaining.swift in Sources */,
|
||||
C3471F6825553E7600297E91 /* ECKeyPair+Utilities.m in Sources */,
|
||||
C3BBE0AA2554D4DE0050F1E3 /* Dictionary+Description.swift in Sources */,
|
||||
C3C2AC2E2553CBEB00C340D1 /* String+Trimming.swift in Sources */,
|
||||
|
@ -5034,6 +5081,7 @@
|
|||
C3BBE0A72554D4DE0050F1E3 /* Promise+Retrying.swift in Sources */,
|
||||
C3BBE0A92554D4DE0050F1E3 /* HTTP.swift in Sources */,
|
||||
C300A60D2554B31900555489 /* Logging.swift in Sources */,
|
||||
C3A7219A2558C1660043A11F /* AnyPromise+Conversion.swift in Sources */,
|
||||
C300A6322554B6D100555489 /* NSDate+Timestamp.mm in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
|
@ -5043,6 +5091,7 @@
|
|||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
C3471F4C25553AB000297E91 /* MessageReceiver+Decryption.swift in Sources */,
|
||||
C3A722802558C4E10043A11F /* AttachmentStream.swift in Sources */,
|
||||
C300A5D32554B05A00555489 /* TypingIndicator.swift in Sources */,
|
||||
C3471ECB2555356A00297E91 /* MessageSender+Encryption.swift in Sources */,
|
||||
C352A32F2557549C00338F3E /* NotifyPNServerJob.swift in Sources */,
|
||||
|
@ -5050,15 +5099,20 @@
|
|||
C3C2A74D2553A39700C340D1 /* VisibleMessage.swift in Sources */,
|
||||
C352A3932557883D00338F3E /* JobDelegate.swift in Sources */,
|
||||
C352A31325574F5200338F3E /* MessageReceiveJob.swift in Sources */,
|
||||
C3A722922558C8940043A11F /* OpenGroupAPIDelegate.swift in Sources */,
|
||||
C3C2A7562553A3AB00C340D1 /* VisibleMessage+Quote.swift in Sources */,
|
||||
C300A5FC2554B0A000555489 /* MessageReceiver.swift in Sources */,
|
||||
C3A7213B2558BDFA0043A11F /* OpenGroup.swift in Sources */,
|
||||
C352A3892557876500338F3E /* JobQueue.swift in Sources */,
|
||||
C3BBE0B52554F0E10050F1E3 /* ProofOfWork.swift in Sources */,
|
||||
C3A721902558C0CD0043A11F /* FileServerAPI.swift in Sources */,
|
||||
C3BBE0802554CDD70050F1E3 /* Storage.swift in Sources */,
|
||||
C3C2A7842553AAF300C340D1 /* SNProto.swift in Sources */,
|
||||
C3C2A7682553A3D900C340D1 /* VisibleMessage+Contact.swift in Sources */,
|
||||
C3A721392558BDFA0043A11F /* OpenGroupAPI.swift in Sources */,
|
||||
C3A71D0B2558989C0043A11F /* MessageWrapper.swift in Sources */,
|
||||
C3A71D1E25589AC30043A11F /* WebSocketProto.swift in Sources */,
|
||||
C3A721382558BDFA0043A11F /* OpenGroupMessage.swift in Sources */,
|
||||
C3C2A7852553AAF300C340D1 /* SessionProtos.pb.swift in Sources */,
|
||||
C3C2A7712553A41E00C340D1 /* ControlMessage.swift in Sources */,
|
||||
C300A5BD2554B00D00555489 /* ReadReceipt.swift in Sources */,
|
||||
|
@ -5066,6 +5120,7 @@
|
|||
C3471F4225553A4D00297E91 /* Threading.swift in Sources */,
|
||||
C300A5DD2554B06600555489 /* ClosedGroupUpdate.swift in Sources */,
|
||||
C3471FA42555439E00297E91 /* Notification+MessageSender.swift in Sources */,
|
||||
C3A7222A2558C1E40043A11F /* DotNetAPI.swift in Sources */,
|
||||
C3A71D1F25589AC30043A11F /* WebSocketResources.pb.swift in Sources */,
|
||||
C352A349255781F400338F3E /* AttachmentDownloadJob.swift in Sources */,
|
||||
C352A30925574D8500338F3E /* Message+Destination.swift in Sources */,
|
||||
|
@ -5074,6 +5129,7 @@
|
|||
C3C2A75F2553A3C500C340D1 /* VisibleMessage+LinkPreview.swift in Sources */,
|
||||
C3C2A74425539EB700C340D1 /* Message.swift in Sources */,
|
||||
C300A5C92554B04E00555489 /* SessionRequest.swift in Sources */,
|
||||
C3A7213A2558BDFA0043A11F /* OpenGroupInfo.swift in Sources */,
|
||||
C352A35B2557824E00338F3E /* AttachmentUploadJob.swift in Sources */,
|
||||
C3BBE0C72554F1570050F1E3 /* FixedWidthInteger+BigEndian.swift in Sources */,
|
||||
C300A5B22554AF9800555489 /* VisibleMessage+Profile.swift in Sources */,
|
||||
|
|
Loading…
Reference in New Issue