Update SessionMessagingKit for open groups

This commit is contained in:
nielsandriesse 2020-11-09 11:53:07 +11:00
parent a3382f41d4
commit 362e2e9c03
22 changed files with 396 additions and 289 deletions

View File

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

View File

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

@ -1 +1 @@
Subproject commit 38df69dfa06ce734db625bdf39534d654528a916
Subproject commit 2bd2badebb8fd11a6f7312b2c490cf9a70a09874

View File

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

View File

@ -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()
}
}
}

View File

@ -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))" }
}

View File

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

View File

@ -0,0 +1,5 @@
public protocol OpenGroupAPIDelegate {
func updateProfileIfNeeded(for channel: UInt64, on server: String, from info: OpenGroupInfo)
}

View File

@ -1,5 +1,5 @@
public struct PublicChatInfo {
public struct OpenGroupInfo {
public let displayName: String
public let profilePictureURL: String?
public let memberCount: Int

View File

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

View File

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

View File

@ -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()
}

View File

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

View File

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

View File

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

View File

@ -1,6 +1,6 @@
import Foundation
@objc(LKSessionCipher)
@objc(SNSessionCipher)
public final class LokiSessionCipher : SessionCipher {
private let sessionResetImplementation: SessionRestorationProtocol?
private let sessionStore: SessionStore

View File

@ -1,5 +1,5 @@
@objc(LKSessionRestorationProtocol)
@objc(SNSessionRestorationProtocol)
public protocol SessionRestorationProtocol {
func validatePreKeyWhisperMessage(for publicKey: String, preKeyWhisperMessage: PreKeyWhisperMessage, using transaction: Any) throws

View File

@ -1,5 +1,5 @@
@objc(LKSessionRestorationStatus)
@objc(SNSessionRestorationStatus)
public enum SessionRestorationStatus : Int {
case none, initiated, requestReceived
}

View File

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

View File

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

View File

@ -144,7 +144,7 @@ private extension String {
}
}
@objc(LKMnemonic)
@objc(SNMnemonic)
public final class ObjCMnemonic : NSObject {
override private init() { }

View File

@ -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 */,