Implement feedback

pull/232/head
nielsandriesse 6 years ago
parent 70eaa5982c
commit 20a847de95

@ -1 +1 @@
Subproject commit 8f3d6d46718795227074d6545776206cf18c0244
Subproject commit 8b8d1d35f47af7071caaaee0aca8c1f5806bbe8e

@ -519,7 +519,6 @@
B80C6B572384A56D00FDBC8B /* DeviceLinksVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = B80C6B562384A56D00FDBC8B /* DeviceLinksVC.swift */; };
B80C6B592384C4E700FDBC8B /* DeviceNameModal.swift in Sources */ = {isa = PBXBuildFile; fileRef = B80C6B582384C4E700FDBC8B /* DeviceNameModal.swift */; };
B80C6B5B2384C7F900FDBC8B /* DeviceNameModalDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B80C6B5A2384C7F900FDBC8B /* DeviceNameModalDelegate.swift */; };
B82584A02315024B001B41CB /* LokiRSSFeedPoller.swift in Sources */ = {isa = PBXBuildFile; fileRef = B825849F2315024B001B41CB /* LokiRSSFeedPoller.swift */; };
B82B40882399EB0E00A248E7 /* LandingVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = B82B40872399EB0E00A248E7 /* LandingVC.swift */; };
B82B408A2399EC0600A248E7 /* FakeChatView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B82B40892399EC0600A248E7 /* FakeChatView.swift */; };
B82B408C239A068800A248E7 /* RegisterVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = B82B408B239A068800A248E7 /* RegisterVC.swift */; };
@ -1340,7 +1339,6 @@
B80C6B562384A56D00FDBC8B /* DeviceLinksVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceLinksVC.swift; sourceTree = "<group>"; };
B80C6B582384C4E700FDBC8B /* DeviceNameModal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceNameModal.swift; sourceTree = "<group>"; };
B80C6B5A2384C7F900FDBC8B /* DeviceNameModalDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceNameModalDelegate.swift; sourceTree = "<group>"; };
B825849F2315024B001B41CB /* LokiRSSFeedPoller.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LokiRSSFeedPoller.swift; sourceTree = "<group>"; };
B82B40872399EB0E00A248E7 /* LandingVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LandingVC.swift; sourceTree = "<group>"; };
B82B40892399EC0600A248E7 /* FakeChatView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FakeChatView.swift; sourceTree = "<group>"; };
B82B408B239A068800A248E7 /* RegisterVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RegisterVC.swift; sourceTree = "<group>"; };
@ -2616,7 +2614,6 @@
children = (
B8CCF63B239757C10091D419 /* Components */,
C32B405424A961E1001117B5 /* Dependencies */,
B8BFFF392355426100102A27 /* Shelved */,
B8CCF63C239757DB0091D419 /* Utilities */,
B8CCF63D2397580E0091D419 /* View Controllers */,
);
@ -2664,14 +2661,6 @@
path = Loki;
sourceTree = "<group>";
};
B8BFFF392355426100102A27 /* Shelved */ = {
isa = PBXGroup;
children = (
B825849F2315024B001B41CB /* LokiRSSFeedPoller.swift */,
);
path = Shelved;
sourceTree = "<group>";
};
B8C9689223FA1B05005F64E0 /* Redesign */ = {
isa = PBXGroup;
children = (
@ -3895,7 +3884,6 @@
4C4AEC4520EC343B0020E72B /* DismissableTextField.swift in Sources */,
4CB5F26720F6E1E2004D1B42 /* MenuActionsViewController.swift in Sources */,
3496955E219B605E00DCFE74 /* PhotoLibrary.swift in Sources */,
B82584A02315024B001B41CB /* LokiRSSFeedPoller.swift in Sources */,
C353F8F7244808E90011121A /* PNModeSheet.swift in Sources */,
45D231771DC7E8F10034FA89 /* SessionResetJob.swift in Sources */,
340FC8A9204DAC8D007AEB0F /* NotificationSettingsOptionsViewController.m in Sources */,

@ -1,73 +0,0 @@
import FeedKit
@objc(LKRSSFeedPoller)
public final class LokiRSSFeedPoller : NSObject {
private let feed: LokiRSSFeed
private var timer: Timer? = nil
private var hasStarted = false
private let interval: TimeInterval = 8 * 60
@objc(initForFeed:)
public init(for feed: LokiRSSFeed) {
self.feed = feed
super.init()
}
@objc public func startIfNeeded() {
if hasStarted { return }
timer = Timer.scheduledTimer(withTimeInterval: interval, repeats: true) { [weak self] _ in self?.poll() }
poll() // Perform initial update
hasStarted = true
}
@objc public func stop() {
timer?.invalidate()
hasStarted = false
}
private func poll() {
let feed = self.feed
let url = feed.server
let _ = LokiRSSFeedProxy.fetchContent(for: url).done { xml in
guard let data = xml.data(using: String.Encoding.utf8) else { return print("[Loki] Failed to parse RSS feed for: \(feed.server).") }
FeedParser(data: data).parseAsync { wrapper in
guard case .rss(let x) = wrapper, let items = x.items else { return print("[Loki] Failed to parse RSS feed for: \(feed.server).") }
items.reversed().forEach { item in
guard let title = item.title, let description = item.description, let date = item.pubDate else { return }
let timestamp = UInt64(date.timeIntervalSince1970 * 1000)
let urlRegex = try! NSRegularExpression(pattern: "<a\\s+(?:[^>]*?\\s+)?href=\"([^\"]*)\".*?>(.*?)<.*?\\/a>")
var bodyAsHTML = "\(title)<br><br>\(description)".replacingOccurrences(of: "</p>", with: "</p><br>")
while true {
guard let match = urlRegex.firstMatch(in: bodyAsHTML, options: [], range: NSRange(location: 0, length: bodyAsHTML.utf16.count)) else { break }
let matchRange = match.range(at: 0)
let urlRange = match.range(at: 1)
let descriptionRange = match.range(at: 2)
let url = (bodyAsHTML as NSString).substring(with: urlRange)
let description = (bodyAsHTML as NSString).substring(with: descriptionRange)
bodyAsHTML = (bodyAsHTML as NSString).replacingCharacters(in: matchRange, with: "\(description) (\(url))") as String
}
guard let bodyAsData = bodyAsHTML.data(using: String.Encoding.unicode) else { return }
let options = [ NSAttributedString.DocumentReadingOptionKey.documentType : NSAttributedString.DocumentType.html ]
guard let body = try? NSAttributedString(data: bodyAsData, options: options, documentAttributes: nil).string else { return }
let id = LKGroupUtilities.getEncodedRSSFeedIDAsData(feed.id)
let groupContext = SSKProtoGroupContext.builder(id: id, type: .deliver)
groupContext.setName(feed.displayName)
let dataMessage = SSKProtoDataMessage.builder()
dataMessage.setTimestamp(timestamp)
dataMessage.setGroup(try! groupContext.build())
dataMessage.setBody(body)
let content = SSKProtoContent.builder()
content.setDataMessage(try! dataMessage.build())
let envelope = SSKProtoEnvelope.builder(type: .ciphertext, timestamp: timestamp)
envelope.setSource(NSLocalizedString("Loki", comment: ""))
envelope.setSourceDevice(OWSDevicePrimaryDeviceId)
envelope.setContent(try! content.build().serializedData())
try! Storage.writeSync { transaction in
SSKEnvironment.shared.messageManager.throws_processEnvelope(try! envelope.build(), plaintextData: try! content.build().serializedData(), wasReceivedByUD: false, transaction: transaction, serverID: 0)
}
}
}
}
}
}

@ -154,7 +154,7 @@ final class JoinPublicChatVC : BaseVC, UIPageViewControllerDataSource, UIPageVie
.catch(on: .main) { [weak self] error in
var title = NSLocalizedString("Couldn't Join", comment: "")
var message = ""
if case LokiHTTPClient.HTTPError.networkError(let statusCode, _, _) = error, (statusCode == 401 || statusCode == 403) {
if case HTTP.Error.httpRequestFailed(let statusCode, _) = error, statusCode == 401 || statusCode == 403 {
title = NSLocalizedString("Unauthorized", comment: "")
message = NSLocalizedString("Please ask the open group operator to add you to the group.", comment: "")
}

@ -95,11 +95,6 @@ NS_ASSUME_NONNULL_BEGIN
if ([self isVersion:previousVersion atLeast:@"2.0.0" andLessThan:@"2.3.0"] && [self.tsAccountManager isRegistered]) {
[self clearBloomFilterCache];
}
// Loki
if ([self isVersion:previousVersion lessThan:@"1.2.1"] && [self.tsAccountManager isRegistered]) {
[self updatePublicChatMapping];
}
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[[[OWSDatabaseMigrationRunner alloc] init] runAllOutstandingWithCompletion:completion];
@ -168,43 +163,6 @@ NS_ASSUME_NONNULL_BEGIN
}
}
# pragma mark Loki - Upgrading to Public Chat Manager
// Versions less than or equal to 1.2.0 didn't store public chat mappings
+ (void)updatePublicChatMapping
{
[LKStorage writeSyncWithBlock:^(YapDatabaseReadWriteTransaction * _Nonnull transaction) {
for (LKPublicChat *chat in LKPublicChatAPI.defaultChats) {
TSGroupThread *thread = [TSGroupThread threadWithGroupId:[LKGroupUtilities getEncodedOpenGroupIDAsData:chat.id] transaction:transaction];
if (thread != nil) {
[LKDatabaseUtilities setPublicChat:chat threadID:thread.uniqueId transaction:transaction];
} else {
// Update the group type and group ID for private group chat version.
// If the thread is still using the old group ID, it needs to be updated.
thread = [TSGroupThread threadWithGroupId:chat.idAsData transaction:transaction];
if (thread != nil) {
thread.groupModel.groupType = openGroup;
[thread.groupModel updateGroupId:[LKGroupUtilities getEncodedOpenGroupIDAsData:chat.id]];
[thread saveWithTransaction:transaction];
[LKDatabaseUtilities setPublicChat:chat threadID:thread.uniqueId transaction:transaction];
}
}
}
// Update RSS feeds here
LKRSSFeed *lokiNewsFeed = [[LKRSSFeed alloc] initWithId:@"loki.network.feed" server:@"https://loki.network/feed/" displayName:NSLocalizedString(@"Loki News", @"") isDeletable:true];
LKRSSFeed *lokiMessengerUpdatesFeed = [[LKRSSFeed alloc] initWithId:@"loki.network.messenger-updates.feed" server:@"https://loki.network/category/messenger-updates/feed/" displayName:NSLocalizedString(@"Session Updates", @"") isDeletable:false];
NSArray *feeds = @[ lokiNewsFeed, lokiMessengerUpdatesFeed ];
for (LKRSSFeed *feed in feeds) {
TSGroupThread *thread = [TSGroupThread threadWithGroupId:[feed.id dataUsingEncoding:NSUTF8StringEncoding] transaction:transaction];
if (thread != nil) {
thread.groupModel.groupType = rssFeed;
[thread.groupModel updateGroupId:[LKGroupUtilities getEncodedRSSFeedIDAsData:feed.id]];
[thread saveWithTransaction:transaction];
}
}
} error:nil];
}
@end
NS_ASSUME_NONNULL_END

@ -1,127 +0,0 @@
import PromiseKit
import SessionMetadataKit
internal class LokiFileServerProxy : LokiHTTPClient {
private let server: String
private let keyPair = Curve25519.generateKeyPair()
private static let fileServerPublicKey: Data = {
let base64EncodedPublicKey = "BWJQnVm97sQE3Q1InB4Vuo+U/T1hmwHBv0ipkiv8tzEc"
let publicKeyWithPrefix = Data(base64Encoded: base64EncodedPublicKey)!
let hexEncodedPublicKeyWithPrefix = publicKeyWithPrefix.toHexString()
let hexEncodedPublicKey = hexEncodedPublicKeyWithPrefix.removing05PrefixIfNeeded()
return Data(hex: hexEncodedPublicKey)
}()
// MARK: Error
internal enum Error : LocalizedError {
case symmetricKeyGenerationFailed
case endpointParsingFailed
case proxyResponseParsingFailed
case fileServerHTTPError(code: Int, message: Any?)
internal var errorDescription: String? {
switch self {
case .symmetricKeyGenerationFailed: return "Couldn't generate symmetric key."
case .endpointParsingFailed: return "Couldn't parse endpoint."
case .proxyResponseParsingFailed: return "Couldn't parse file server proxy response."
case .fileServerHTTPError(let httpStatusCode, let message): return "File server returned \(httpStatusCode) with description: \(message ?? "no description provided.")."
}
}
}
// MARK: Initialization
internal init(for server: String) {
self.server = server
super.init()
}
// MARK: Proxying
override internal func perform(_ request: TSRequest, withCompletionQueue queue: DispatchQueue = DispatchQueue.main) -> SnodeAPI.RawResponsePromise {
let isLokiFileServer = (server == FileServerAPI.server)
guard isLokiFileServer else { return super.perform(request, withCompletionQueue: queue) } // Don't proxy open group requests for now
return performLokiFileServerNSURLRequest(request, withCompletionQueue: queue)
}
internal func performLokiFileServerNSURLRequest(_ request: NSURLRequest, withCompletionQueue queue: DispatchQueue = DispatchQueue.main) -> SnodeAPI.RawResponsePromise {
var headers = getCanonicalHeaders(for: request)
return Promise<SnodeAPI.RawResponse> { [server = self.server, keyPair = self.keyPair, httpSession = self.httpSession] seal in
DispatchQueue.global(qos: .userInitiated).async {
let uncheckedSymmetricKey = try? Curve25519.generateSharedSecret(fromPublicKey: LokiFileServerProxy.fileServerPublicKey, privateKey: keyPair.privateKey)
guard let symmetricKey = uncheckedSymmetricKey else { return seal.reject(Error.symmetricKeyGenerationFailed) }
SnodeAPI.getRandomSnode().then2 { proxy -> Promise<Any> in
let url = "\(proxy.address):\(proxy.port)/file_proxy"
guard let urlAsString = request.url?.absoluteString, let serverURLEndIndex = urlAsString.range(of: server)?.upperBound,
serverURLEndIndex < urlAsString.endIndex else { throw Error.endpointParsingFailed }
let endpointStartIndex = urlAsString.index(after: serverURLEndIndex)
let endpoint = String(urlAsString[endpointStartIndex..<urlAsString.endIndex])
let parametersAsString: String
if let tsRequest = request as? TSRequest {
headers["Content-Type"] = "application/json"
let parametersAsData = try JSONSerialization.data(withJSONObject: tsRequest.parameters, options: [ .fragmentsAllowed ])
parametersAsString = !tsRequest.parameters.isEmpty ? String(bytes: parametersAsData, encoding: .utf8)! : "null"
} else {
headers["Content-Type"] = request.allHTTPHeaderFields!["Content-Type"]
if let parametersAsInputStream = request.httpBodyStream, let parametersAsData = try? Data(from: parametersAsInputStream) {
parametersAsString = "{ \"fileUpload\" : \"\(String(data: parametersAsData.base64EncodedData(), encoding: .utf8) ?? "null")\" }"
} else {
parametersAsString = "null"
}
}
let proxyRequestParameters: JSON = [
"body" : parametersAsString,
"endpoint": endpoint,
"method" : request.httpMethod,
"headers" : headers
]
let proxyRequestParametersAsData = try JSONSerialization.data(withJSONObject: proxyRequestParameters, options: [ .fragmentsAllowed ])
let ivAndCipherText = try DiffieHellman.encrypt(proxyRequestParametersAsData, using: symmetricKey)
let base64EncodedPublicKey = Data(hex: keyPair.hexEncodedPublicKey).base64EncodedString() // The file server expects an 05 prefixed public key
let proxyRequestHeaders = [
"X-Loki-File-Server-Target" : "/loki/v1/secure_rpc",
"X-Loki-File-Server-Verb" : "POST",
"X-Loki-File-Server-Headers" : "{ \"X-Loki-File-Server-Ephemeral-Key\" : \"\(base64EncodedPublicKey)\" }",
"Connection" : "close", // TODO: Is this necessary?
"Content-Type" : "application/json"
]
let (promise, resolver) = SnodeAPI.RawResponsePromise.pending()
let proxyRequest = AFHTTPRequestSerializer().request(withMethod: "POST", urlString: url, parameters: nil, error: nil)
proxyRequest.allHTTPHeaderFields = proxyRequestHeaders
proxyRequest.httpBody = "{ \"cipherText64\" : \"\(ivAndCipherText.base64EncodedString())\" }".data(using: String.Encoding.utf8)!
proxyRequest.timeoutInterval = request.timeoutInterval
var task: URLSessionDataTask!
task = httpSession.dataTask(with: proxyRequest as URLRequest) { response, result, error in
if let error = error {
let nmError = NetworkManagerError.taskError(task: task, underlyingError: error)
let nsError: NSError = nmError as NSError
nsError.isRetryable = false
resolver.reject(nsError)
} else {
resolver.fulfill(result)
}
}
task.resume()
return promise
}.map2 { rawResponse in
guard let responseAsData = rawResponse as? Data, let responseAsJSON = try? JSONSerialization.jsonObject(with: responseAsData, options: [ .fragmentsAllowed ]) as? JSON, let base64EncodedCipherText = responseAsJSON["data"] as? String,
let meta = responseAsJSON["meta"] as? JSON, let statusCode = meta["code"] as? Int, let cipherText = Data(base64Encoded: base64EncodedCipherText) else {
print("[Loki] Received an invalid response.")
throw Error.proxyResponseParsingFailed
}
let isSuccess = (200...299) ~= statusCode
guard isSuccess else { throw HTTPError.networkError(code: statusCode, response: nil, underlyingError: Error.fileServerHTTPError(code: statusCode, message: nil)) }
let uncheckedJSONAsData = try DiffieHellman.decrypt(cipherText, using: symmetricKey)
if uncheckedJSONAsData.isEmpty { return () }
let uncheckedJSON = try? JSONSerialization.jsonObject(with: uncheckedJSONAsData, options: [ .fragmentsAllowed ]) as? JSON
guard let json = uncheckedJSON else { throw HTTPError.networkError(code: -1, response: nil, underlyingError: Error.proxyResponseParsingFailed) }
return json
}.done2 { rawResponse in
seal.fulfill(rawResponse)
}.catch2 { error in
print("[Loki] File server proxy request failed with error: \(error.localizedDescription).")
seal.reject(HTTPError.from(error: error) ?? error)
}
}
}
}
}

@ -1,75 +0,0 @@
import PromiseKit
/// Base class for `LokiSnodeProxy` and `LokiFileServerProxy`.
public class LokiHTTPClient {
internal lazy var httpSession: AFHTTPSessionManager = {
let result = AFHTTPSessionManager(sessionConfiguration: .ephemeral)
let securityPolicy = AFSecurityPolicy.default()
securityPolicy.allowInvalidCertificates = true
securityPolicy.validatesDomainName = false
result.securityPolicy = securityPolicy
result.responseSerializer = AFHTTPResponseSerializer()
result.completionQueue = DispatchQueue.global(qos: .default)
return result
}()
internal func perform(_ request: TSRequest, withCompletionQueue queue: DispatchQueue = DispatchQueue.main) -> SnodeAPI.RawResponsePromise {
return TSNetworkManager.shared().perform(request, withCompletionQueue: queue).map2 { $0.responseObject }.recover2 { error -> SnodeAPI.RawResponsePromise in
throw HTTPError.from(error: error) ?? error
}
}
internal func getCanonicalHeaders(for request: NSURLRequest) -> [String:Any] {
guard let headers = request.allHTTPHeaderFields else { return [:] }
return headers.mapValues { value in
switch value.lowercased() {
case "true": return true
case "false": return false
default: return value
}
}
}
}
// MARK: - HTTP Error
public extension LokiHTTPClient {
public enum HTTPError : LocalizedError {
case networkError(code: Int, response: Any?, underlyingError: Error?)
internal static func from(error: Error) -> LokiHTTPClient.HTTPError? {
if let error = error as? NetworkManagerError {
if case NetworkManagerError.taskError(_, let underlyingError) = error, let nsError = underlyingError as? NSError {
var response = nsError.userInfo[AFNetworkingOperationFailingURLResponseDataErrorKey]
// Deserialize response if needed
if let data = response as? Data, let json = try? JSONSerialization.jsonObject(with: data, options: [ .fragmentsAllowed ]) as? JSON {
response = json
}
return LokiHTTPClient.HTTPError.networkError(code: error.statusCode, response: response, underlyingError: underlyingError)
}
return LokiHTTPClient.HTTPError.networkError(code: error.statusCode, response: nil, underlyingError: error)
}
return nil
}
public var errorDescription: String? {
switch self {
case .networkError(let code, let body, let underlyingError): return underlyingError?.localizedDescription ?? "HTTP request failed with status code: \(code), message: \(body ?? "nil")."
}
}
internal var statusCode: Int {
switch self {
case .networkError(let code, _, _): return code
}
}
internal var isNetworkError: Bool {
switch self {
case .networkError(_, _, let underlyingError): return underlyingError != nil && IsNSErrorNetworkFailure(underlyingError)
}
}
}
}

@ -68,7 +68,10 @@ public class DotNetAPI : NSObject {
let queryParameters = "pubKey=\(getUserHexEncodedPublicKey())"
let url = URL(string: "\(server)/loki/v1/get_challenge?\(queryParameters)")!
let request = TSRequest(url: url)
return LokiFileServerProxy(for: server).perform(request, withCompletionQueue: DispatchQueue.global(qos: .default)).map2 { rawResponse in
let serverPublicKeyPromise = (server == FileServerAPI.server) ? Promise { $0.fulfill(server) } : PublicChatAPI.getOpenGroupServerPublicKey(for: server)
return serverPublicKeyPromise.then2 { serverPublicKey in
OnionRequestAPI.sendOnionRequest(request, to: server, using: serverPublicKey)
}.map2 { rawResponse in
guard let json = rawResponse as? JSON, 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
@ -92,7 +95,10 @@ public class DotNetAPI : NSObject {
let url = URL(string: "\(server)/loki/v1/submit_challenge")!
let parameters = [ "pubKey" : getUserHexEncodedPublicKey(), "token" : token ]
let request = TSRequest(url: url, method: "POST", parameters: parameters)
return LokiFileServerProxy(for: server).perform(request, withCompletionQueue: DispatchQueue.global(qos: .default)).map2 { _ in token }
let serverPublicKeyPromise = (server == FileServerAPI.server) ? Promise { $0.fulfill(server) } : PublicChatAPI.getOpenGroupServerPublicKey(for: server)
return serverPublicKeyPromise.then2 { serverPublicKey in
OnionRequestAPI.sendOnionRequest(request, to: server, using: serverPublicKey)
}.map2 { _ in token }
}
// MARK: Public API
@ -126,8 +132,7 @@ public class DotNetAPI : NSObject {
data = unencryptedAttachmentData
}
// Check the file size if needed
let isLokiFileServer = (server == FileServerAPI.server)
if isLokiFileServer && data.count > FileServerAPI.maxFileSize {
if data.count > FileServerAPI.maxFileSize {
return seal.reject(DotNetAPIError.maxFileSizeExceeded)
}
// Create the request
@ -143,10 +148,16 @@ public class DotNetAPI : NSObject {
return seal.reject(error)
}
// Send the request
func parseResponse(_ responseObject: Any) {
let serverPublicKeyPromise = (server == FileServerAPI.server) ? Promise { $0.fulfill(FileServerAPI.fileServerPublicKey) }
: PublicChatAPI.getOpenGroupServerPublicKey(for: server)
attachment.isUploaded = false
attachment.save()
let _ = serverPublicKeyPromise.then2 { serverPublicKey in
OnionRequestAPI.sendOnionRequest(request, to: server, using: serverPublicKey)
}.done2 { json in
// Parse the server ID & download URL
guard let json = responseObject as? JSON, 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: \(responseObject).")
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)
}
// Update the attachment
@ -155,38 +166,8 @@ public class DotNetAPI : NSObject {
attachment.downloadURL = downloadURL
attachment.save()
seal.fulfill(())
}
let isProxyingRequired = (server == FileServerAPI.server) // Don't proxy open group requests for now
if isProxyingRequired {
attachment.isUploaded = false
attachment.save()
let _ = LokiFileServerProxy(for: server).performLokiFileServerNSURLRequest(request as NSURLRequest).done2 { responseObject in
parseResponse(responseObject)
}.catch2 { error in
seal.reject(error)
}
} else {
let task = AFURLSessionManager(sessionConfiguration: .default).uploadTask(withStreamedRequest: request as URLRequest, progress: { rawProgress in
// Broadcast progress updates
let progress = max(0.1, rawProgress.fractionCompleted)
let userInfo: [String:Any] = [ kAttachmentUploadProgressKey : progress, kAttachmentUploadAttachmentIDKey : attachmentID ]
DispatchQueue.main.async {
NotificationCenter.default.post(name: .attachmentUploadProgress, object: nil, userInfo: userInfo)
}
}, completionHandler: { response, responseObject, error in
if let error = error {
print("[Loki] Couldn't upload attachment due to error: \(error).")
return seal.reject(error)
}
let statusCode = (response as! HTTPURLResponse).statusCode
let isSuccessful = (200...299) ~= statusCode
guard isSuccessful else {
print("[Loki] Couldn't upload attachment.")
return seal.reject(DotNetAPIError.generic)
}
parseResponse(responseObject)
})
task.resume()
}.catch2 { error in
seal.reject(error)
}
}
if server == FileServerAPI.server {
@ -210,7 +191,7 @@ internal extension Promise {
internal func handlingInvalidAuthTokenIfNeeded(for server: String) -> Promise<T> {
return recover2 { error -> Promise<T> in
if let error = error as? NetworkManagerError, (error.statusCode == 401 || error.statusCode == 403) {
if case HTTP.Error.httpRequestFailed(let statusCode, _) = error, statusCode == 401 || statusCode == 403 {
print("[Loki] Auth token for: \(server) expired; dropping it.")
DotNetAPI.clearAuthToken(for: server)
}

@ -4,21 +4,14 @@ import PromiseKit
public final class FileServerAPI : DotNetAPI {
// MARK: Settings
private static let deviceLinkType = "network.loki.messenger.devicemapping"
private static let attachmentType = "net.app.core.oembed"
private static let deviceLinkType = "network.loki.messenger.devicemapping"
internal static let fileServerPublicKey = "62509D59BDEEC404DD0D489C1E15BA8F94FD3D619B01C1BF48A9922BFCB7311C"
public static let maxFileSize = 10_000_000 // 10 MB
@objc public static let server = "https://file.getsession.org"
internal static var useOnionRequests = true
private static let fileServerPublicKey: String = {
let base64EncodedPublicKey = "BWJQnVm97sQE3Q1InB4Vuo+U/T1hmwHBv0ipkiv8tzEc"
let publicKeyWithPrefix = Data(base64Encoded: base64EncodedPublicKey)!
let hexEncodedPublicKeyWithPrefix = publicKeyWithPrefix.toHexString()
return hexEncodedPublicKeyWithPrefix.removing05PrefixIfNeeded()
}()
// MARK: Storage
override internal class var authTokenCollection: String { return "LokiStorageAuthTokenCollection" }
@ -51,69 +44,52 @@ public final class FileServerAPI : DotNetAPI {
public static func getDeviceLinks(associatedWith hexEncodedPublicKeys: Set<String>) -> Promise<Set<DeviceLink>> {
let hexEncodedPublicKeysDescription = "[ \(hexEncodedPublicKeys.joined(separator: ", ")) ]"
print("[Loki] Getting device links for: \(hexEncodedPublicKeysDescription).")
func handleRawResponseForDeviceLinks(rawResponse: JSON, data: [JSON]) -> Set<DeviceLink> {
return Set(data.flatMap { data -> [DeviceLink] in
guard let annotations = data["annotations"] as? [JSON], !annotations.isEmpty else { return [] }
guard let annotation = annotations.first(where: { $0["type"] as? String == deviceLinkType }),
let value = annotation["value"] as? JSON, let rawDeviceLinks = value["authorisations"] as? [JSON],
let hexEncodedPublicKey = data["username"] as? String else {
print("[Loki] Couldn't parse device links from: \(rawResponse).")
return []
}
return rawDeviceLinks.compactMap { rawDeviceLink in
guard let masterPublicKey = rawDeviceLink["primaryDevicePubKey"] as? String, let slavePublicKey = rawDeviceLink["secondaryDevicePubKey"] as? String,
let base64EncodedSlaveSignature = rawDeviceLink["requestSignature"] as? String else {
print("[Loki] Couldn't parse device link for user: \(hexEncodedPublicKey) from: \(rawResponse).")
return nil
}
let masterSignature: Data?
if let base64EncodedMasterSignature = rawDeviceLink["grantSignature"] as? String {
masterSignature = Data(base64Encoded: base64EncodedMasterSignature)
} else {
masterSignature = nil
}
let slaveSignature = Data(base64Encoded: base64EncodedSlaveSignature)
let master = DeviceLink.Device(publicKey: masterPublicKey, signature: masterSignature)
let slave = DeviceLink.Device(publicKey: slavePublicKey, signature: slaveSignature)
let deviceLink = DeviceLink(between: master, and: slave)
if let masterSignature = masterSignature {
guard DeviceLinkingUtilities.hasValidMasterSignature(deviceLink) else {
print("[Loki] Received a device link with an invalid master signature.")
return nil
}
}
guard DeviceLinkingUtilities.hasValidSlaveSignature(deviceLink) else {
print("[Loki] Received a device link with an invalid slave signature.")
return nil
}
return deviceLink
}
})
}
return getAuthToken(for: server).then2 { token -> Promise<Set<DeviceLink>> in
let queryParameters = "ids=\(hexEncodedPublicKeys.map { "@\($0)" }.joined(separator: ","))&include_user_annotations=1"
let url = URL(string: "\(server)/users?\(queryParameters)")!
let request = TSRequest(url: url)
if (useOnionRequests) {
return OnionRequestAPI.sendOnionRequestLsrpcDest(request, server: server, using: fileServerPublicKey).map2 { rawResponse -> Set<DeviceLink> in
guard let data = rawResponse["data"] as? [JSON] else {
print("[Loki] Couldn't parse device links for users: \(hexEncodedPublicKeys) from: \(rawResponse).")
throw DotNetAPIError.parsingFailed
}
return handleRawResponseForDeviceLinks(rawResponse: rawResponse, data: data)
}.map2 { deviceLinks in
storage.setDeviceLinks(deviceLinks)
return deviceLinks
}
}
return LokiFileServerProxy(for: server).perform(request, withCompletionQueue: DispatchQueue.global(qos: .default)).map2 { rawResponse -> Set<DeviceLink> in
guard let json = rawResponse as? JSON, let data = json["data"] as? [JSON] else {
return OnionRequestAPI.sendOnionRequest(request, to: server, using: fileServerPublicKey).map2 { rawResponse -> Set<DeviceLink> in
guard let data = rawResponse["data"] as? [JSON] else {
print("[Loki] Couldn't parse device links for users: \(hexEncodedPublicKeys) from: \(rawResponse).")
throw DotNetAPIError.parsingFailed
}
return handleRawResponseForDeviceLinks(rawResponse: json, data: data)
return Set(data.flatMap { data -> [DeviceLink] in
guard let annotations = data["annotations"] as? [JSON], !annotations.isEmpty else { return [] }
guard let annotation = annotations.first(where: { $0["type"] as? String == deviceLinkType }),
let value = annotation["value"] as? JSON, let rawDeviceLinks = value["authorisations"] as? [JSON],
let hexEncodedPublicKey = data["username"] as? String else {
print("[Loki] Couldn't parse device links from: \(rawResponse).")
return []
}
return rawDeviceLinks.compactMap { rawDeviceLink in
guard let masterPublicKey = rawDeviceLink["primaryDevicePubKey"] as? String, let slavePublicKey = rawDeviceLink["secondaryDevicePubKey"] as? String,
let base64EncodedSlaveSignature = rawDeviceLink["requestSignature"] as? String else {
print("[Loki] Couldn't parse device link for user: \(hexEncodedPublicKey) from: \(rawResponse).")
return nil
}
let masterSignature: Data?
if let base64EncodedMasterSignature = rawDeviceLink["grantSignature"] as? String {
masterSignature = Data(base64Encoded: base64EncodedMasterSignature)
} else {
masterSignature = nil
}
let slaveSignature = Data(base64Encoded: base64EncodedSlaveSignature)
let master = DeviceLink.Device(publicKey: masterPublicKey, signature: masterSignature)
let slave = DeviceLink.Device(publicKey: slavePublicKey, signature: slaveSignature)
let deviceLink = DeviceLink(between: master, and: slave)
if let masterSignature = masterSignature {
guard DeviceLinkingUtilities.hasValidMasterSignature(deviceLink) else {
print("[Loki] Received a device link with an invalid master signature.")
return nil
}
}
guard DeviceLinkingUtilities.hasValidSlaveSignature(deviceLink) else {
print("[Loki] Received a device link with an invalid slave signature.")
return nil
}
return deviceLink
}
})
}.map2 { deviceLinks in
storage.setDeviceLinks(deviceLinks)
return deviceLinks
@ -134,10 +110,7 @@ public final class FileServerAPI : DotNetAPI {
let request = TSRequest(url: url, method: "PATCH", parameters: parameters)
request.allHTTPHeaderFields = [ "Content-Type" : "application/json", "Authorization" : "Bearer \(token)" ]
return attempt(maxRetryCount: 8, recoveringOn: SnodeAPI.workQueue) {
if (useOnionRequests) {
return OnionRequestAPI.sendOnionRequestLsrpcDest(request, server: server, using: fileServerPublicKey).map2 { _ in }
}
return LokiFileServerProxy(for: server).perform(request).map2 { _ in }
OnionRequestAPI.sendOnionRequest(request, to: server, using: fileServerPublicKey).map2 { _ in }
}.handlingInvalidAuthTokenIfNeeded(for: server).recover2 { error in
print("Couldn't update device links due to error: \(error).")
throw error
@ -193,19 +166,9 @@ public final class FileServerAPI : DotNetAPI {
print("[Loki] Couldn't upload profile picture due to error: \(error).")
return Promise(error: error)
}
if (useOnionRequests) {
return OnionRequestAPI.sendOnionRequestLsrpcDest(request, server: server, using: fileServerPublicKey).map2 { json in
guard let data = json["data"] as? JSON, let downloadURL = data["url"] as? String else {
print("[Loki] Couldn't parse profile picture from: \(json).")
throw DotNetAPIError.parsingFailed
}
UserDefaults.standard[.lastProfilePictureUpload] = Date()
return downloadURL
}
}
return LokiFileServerProxy(for: server).performLokiFileServerNSURLRequest(request as NSURLRequest).map2 { responseObject in
guard let json = responseObject as? JSON, let data = json["data"] as? JSON, let downloadURL = data["url"] as? String else {
print("[Loki] Couldn't parse profile picture from: \(responseObject).")
return OnionRequestAPI.sendOnionRequest(request, to: server, using: fileServerPublicKey).map2 { json in
guard let data = json["data"] as? JSON, let downloadURL = data["url"] as? String else {
print("[Loki] Couldn't parse profile picture from: \(json).")
throw DotNetAPIError.parsingFailed
}
UserDefaults.standard[.lastProfilePictureUpload] = Date()
@ -214,21 +177,21 @@ public final class FileServerAPI : DotNetAPI {
}
// MARK: Open Group Server Public Key
public static func getOpenGroupKey(for openGroupServer: String) -> Promise<String> {
let serverURL = URL(string: openGroupServer)!
let url = URL(string: "\(server)/loki/v1/getOpenGroupKey/\(serverURL.host!)")!
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, using a dummy token
let token = "loki" // Tokenless request; use a dummy token
request.allHTTPHeaderFields = [ "Content-Type" : "application/json", "Authorization" : "Bearer \(token)" ]
return OnionRequestAPI.sendOnionRequestLsrpcDest(request, server: server, using: fileServerPublicKey).map2 { rawResponse in
guard let bodyAsString = rawResponse["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 }
return OnionRequestAPI.sendOnionRequest(request, to: server, using: fileServerPublicKey).map2 { 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 {
print("[Loki] Couldn't parse open group public key from: \(body).")
throw DotNetAPIError.parsingFailed
}
let publicKeyWithPrefix = Data(base64Encoded: base64EncodedPublicKey)!
let hexEncodedPublicKeyWithPrefix = publicKeyWithPrefix.toHexString()
return hexEncodedPublicKeyWithPrefix.removing05PrefixIfNeeded()
let prefixedPublicKey = Data(base64Encoded: base64EncodedPublicKey)!
let hexEncodedPrefixedPublicKey = prefixedPublicKey.toHexString()
return hexEncodedPrefixedPublicKey.removing05PrefixIfNeeded()
}
}
}

@ -18,9 +18,9 @@ extension OnionRequestAPI {
}
/// - Note: Sync. Don't call from the main thread.
private static func encrypt(_ plaintext: Data, using x25519Key: String?) throws -> EncryptionResult {
private static func encrypt(_ plaintext: Data, using hexEncodedX25519PublicKey: String?) throws -> EncryptionResult {
guard !Thread.isMainThread else { preconditionFailure("It's illegal to call encrypt(_:forSnode:) from the main thread.") }
guard let hexEncodedX25519PublicKey = x25519Key else { throw Error.snodePublicKeySetMissing }
guard let hexEncodedX25519PublicKey = hexEncodedX25519PublicKey else { throw Error.snodePublicKeySetMissing }
let x25519PublicKey = Data(hex: hexEncodedX25519PublicKey)
let ephemeralKeyPair = Curve25519.generateKeyPair()
let ephemeralSharedSecret = try Curve25519.generateSharedSecret(fromPublicKey: x25519PublicKey, privateKey: ephemeralKeyPair.privateKey)
@ -30,24 +30,26 @@ extension OnionRequestAPI {
return (ciphertext, Data(bytes: symmetricKey), ephemeralKeyPair.publicKey)
}
/// Encrypts `payload` for `snode` and returns the result. Use this to build the core of an onion request.
internal static func encrypt(_ payload: JSON, using x25519Key: String?, to destination: JSON) -> Promise<EncryptionResult> {
/// Encrypts `payload` for `destination` and returns the result. Use this to build the core of an onion request.
internal static func encrypt(_ payload: JSON, for destination: Destination) -> Promise<EncryptionResult> {
let (promise, seal) = Promise<EncryptionResult>.pending()
DispatchQueue.global(qos: .userInitiated).async {
do {
// The wrapper is not needed when it is a file server onion request
guard JSONSerialization.isValidJSONObject(payload) else { return seal.reject(HTTP.Error.invalidJSON) }
if let destination = destination["destination"] {
// Wrapping isn't needed for file server or open group onion requests
switch destination {
case .snode(let snode):
guard let snodeX25519PublicKey = snode.publicKeySet?.x25519Key else { return seal.reject(Error.snodePublicKeySetMissing) }
let payloadAsData = try JSONSerialization.data(withJSONObject: payload, options: [ .fragmentsAllowed ])
let payloadAsString = String(data: payloadAsData, encoding: .utf8)! // Snodes only accept this as a string
let wrapper: JSON = [ "body" : payloadAsString, "headers" : "" ]
guard JSONSerialization.isValidJSONObject(wrapper) else { return seal.reject(HTTP.Error.invalidJSON) }
let plaintext = try JSONSerialization.data(withJSONObject: wrapper, options: [ .fragmentsAllowed ])
let result = try encrypt(plaintext, using: x25519Key)
let result = try encrypt(plaintext, using: snodeX25519PublicKey)
seal.fulfill(result)
} else {
case .server(_, let serverX25519PublicKey):
let plaintext = try JSONSerialization.data(withJSONObject: payload, options: [ .fragmentsAllowed ])
let result = try encrypt(plaintext, using: x25519Key)
let result = try encrypt(plaintext, using: serverX25519PublicKey)
seal.fulfill(result)
}
} catch (let error) {
@ -58,16 +60,31 @@ extension OnionRequestAPI {
}
/// Encrypts the previous encryption result (i.e. that of the hop after this one) for this hop. Use this to build the layers of an onion request.
internal static func encryptHop(with x25519Key: String?, to destination: JSON, using previousEncryptionResult: EncryptionResult) -> Promise<EncryptionResult> {
internal static func encryptHop(from lhs: Destination, to rhs: Destination, using previousEncryptionResult: EncryptionResult) -> Promise<EncryptionResult> {
let (promise, seal) = Promise<EncryptionResult>.pending()
DispatchQueue.global(qos: .userInitiated).async {
var parameters = destination
var parameters: JSON
switch rhs {
case .snode(let snode):
guard let snodeED25519PublicKey = snode.publicKeySet?.ed25519Key else { return seal.reject(Error.snodePublicKeySetMissing) }
parameters = [ "destination" : snodeED25519PublicKey ]
case .server(let host, _):
parameters = [ "host" : host, "target" : "/loki/v1/lsrpc", "method" : "POST" ]
}
parameters["ciphertext"] = previousEncryptionResult.ciphertext.base64EncodedString()
parameters["ephemeral_key"] = previousEncryptionResult.ephemeralPublicKey.toHexString()
let x25519PublicKey: String
switch lhs {
case .snode(let snode):
guard let snodeX25519PublicKey = snode.publicKeySet?.x25519Key else { return seal.reject(Error.snodePublicKeySetMissing) }
x25519PublicKey = snodeX25519PublicKey
case .server(_, let serverX25519PublicKey):
x25519PublicKey = serverX25519PublicKey
}
do {
guard JSONSerialization.isValidJSONObject(parameters) else { return seal.reject(HTTP.Error.invalidJSON) }
let plaintext = try JSONSerialization.data(withJSONObject: parameters, options: [ .fragmentsAllowed ])
let result = try encrypt(plaintext, using: x25519Key)
let result = try encrypt(plaintext, using: x25519PublicKey)
seal.fulfill(result)
} catch (let error) {
seal.reject(error)

@ -19,10 +19,17 @@ public enum OnionRequestAPI {
private static var guardSnodeCount: UInt { return pathCount } // One per path
// MARK: Destination
internal enum Destination {
case snode(Snode)
case server(host: String, x25519PublicKey: String)
}
// MARK: Error
internal enum Error : LocalizedError {
case httpRequestFailedAtTargetSnode(statusCode: UInt, json: JSON)
case insufficientSnodes
case invalidURL
case missingSnodeVersion
case randomDataGenerationFailed
case snodePublicKeySetMissing
@ -32,6 +39,7 @@ public enum OnionRequestAPI {
switch self {
case .httpRequestFailedAtTargetSnode(let statusCode): return "HTTP request failed at target snode with status code: \(statusCode)."
case .insufficientSnodes: return "Couldn't find enough snodes to build a path."
case .invalidURL: return "Invalid URL"
case .missingSnodeVersion: return "Missing snode version."
case .randomDataGenerationFailed: return "Couldn't generate random data."
case .snodePublicKeySetMissing: return "Missing snode public key set."
@ -138,7 +146,7 @@ public enum OnionRequestAPI {
/// Returns a `Path` to be used for building an onion request. Builds new paths as needed.
///
/// - Note: Exposed for testing purposes.
internal static func getPath(excluding snode: Snode?) -> Promise<Path> {
private static func getPath(excluding snode: Snode?) -> Promise<Path> {
guard pathSize >= 1 else { preconditionFailure("Can't build path of size zero.") }
if paths.count < pathCount {
let storage = OWSPrimaryStorage.shared()
@ -181,34 +189,29 @@ public enum OnionRequestAPI {
}
/// Builds an onion around `payload` and returns the result.
private static func buildOnion(around payload: JSON, targetedAt snode: Snode?, to destination: JSON = [:], using x25519Key: String? = nil) -> Promise<OnionBuildingResult> {
private static func buildOnion(around payload: JSON, targetedAt destination: Destination) -> Promise<OnionBuildingResult> {
var guardSnode: Snode!
var targetSnodeSymmetricKey: Data! // Needed by invoke(_:on:with:) to decrypt the response sent back by the target snode
var targetSnodeSymmetricKey: Data! // Needed by invoke(_:on:with:) to decrypt the response sent back by the destination
var encryptionResult: EncryptionResult!
return getPath(excluding: snode).then2 { path -> Promise<EncryptionResult> in
var snodeToExclude: Snode?
if case .snode(let snode) = destination { snodeToExclude = snode }
return getPath(excluding: snodeToExclude).then2 { path -> Promise<EncryptionResult> in
guardSnode = path.first!
// Encrypt in reverse order, i.e. the target snode first
var dest = destination
var x25519PublicKey = x25519Key
if let snode = snode {
dest = [ "destination": snode.publicKeySet!.ed25519Key ]
x25519PublicKey = snode.publicKeySet?.x25519Key
}
return encrypt(payload, using: x25519PublicKey, to: dest).then2 { r -> Promise<EncryptionResult> in
// Encrypt in reverse order, i.e. the destination first
return encrypt(payload, for: destination).then2 { r -> Promise<EncryptionResult> in
targetSnodeSymmetricKey = r.symmetricKey
// Recursively encrypt the layers of the onion (again in reverse order)
encryptionResult = r
var path = path
var destination = dest
var rhs = destination
func addLayer() -> Promise<EncryptionResult> {
if path.isEmpty {
return Promise<EncryptionResult> { $0.fulfill(encryptionResult) }
} else {
let lhs = path.removeLast()
let x25519Key = lhs.publicKeySet?.x25519Key
return OnionRequestAPI.encryptHop(with: x25519Key, to: destination, using: encryptionResult).then2 { r -> Promise<EncryptionResult> in
let lhs = Destination.snode(path.removeLast())
return OnionRequestAPI.encryptHop(from: lhs, to: rhs, using: encryptionResult).then2 { r -> Promise<EncryptionResult> in
encryptionResult = r
destination = [ "destination": lhs.publicKeySet!.ed25519Key ]
rhs = lhs
return addLayer()
}
}
@ -219,47 +222,48 @@ public enum OnionRequestAPI {
}
// MARK: Internal API
internal static func getCanonicalHeaders(for request: NSURLRequest) -> [String:Any] {
guard let headers = request.allHTTPHeaderFields else { return [:] }
return headers.mapValues { value in
switch value.lowercased() {
case "true": return true
case "false": return false
default: return value
}
}
}
/// Sends an onion request to `snode`. Builds new paths as needed.
internal static func sendOnionRequestSnodeDest(invoking method: Snode.Method, on snode: Snode, with parameters: JSON, associatedWith publicKey: String) -> Promise<JSON> {
internal static func sendOnionRequest(to snode: Snode, invoking method: Snode.Method, with parameters: JSON, associatedWith publicKey: String) -> Promise<JSON> {
let payload: JSON = [ "method" : method.rawValue, "params" : parameters ]
let promise = sendOnionRequest(on: snode, with: payload, to: [:], using: nil, associatedWith: publicKey)
promise.recover2 { error -> Promise<JSON> in
return sendOnionRequest(with: payload, to: Destination.snode(snode), associatedWith: publicKey).recover2 { error -> Promise<JSON> in
guard case OnionRequestAPI.Error.httpRequestFailedAtTargetSnode(let statusCode, let json) = error else { throw error }
throw SnodeAPI.handleError(withStatusCode: statusCode, json: json, forSnode: snode, associatedWith: publicKey) ?? error
}
return promise
}
/// Sends an onion request to `file server`. Builds new paths as needed.
internal static func sendOnionRequestLsrpcDest(_ request: NSURLRequest, server: String, using x25519Key: String, noJSON: Bool = false) -> Promise<JSON> {
var headers = getCanonicalHeaders(for: request)
let urlAsString = request.url!.absoluteString
let serverURLEndIndex = urlAsString.range(of: server)!.upperBound
/// Sends an onion request to `server`. Builds new paths as needed.
internal static func sendOnionRequest(_ request: NSURLRequest, to server: String, using x25519Key: String, isJSONRequired: Bool = true) -> Promise<JSON> {
let rawHeaders = request.allHTTPHeaderFields ?? [:]
var headers: JSON = rawHeaders.mapValues { value in
switch value.lowercased() {
case "true": return true
case "false": return false
default: return value
}
}
guard let url = request.url?.absoluteString, let host = request.url?.host else { return Promise(error: Error.invalidURL) }
var endpoint = ""
if server.count < urlAsString.count {
let endpointStartIndex = urlAsString.index(after: serverURLEndIndex)
endpoint = String(urlAsString[endpointStartIndex..<urlAsString.endIndex])
if server.count < url.count {
guard let serverEndIndex = url.range(of: server)?.upperBound else { return Promise(error: Error.invalidURL) }
let endpointStartIndex = url.index(after: serverEndIndex)
endpoint = String(url[endpointStartIndex..<url.endIndex])
}
let parametersAsString: String
if let tsRequest = request as? TSRequest {
headers["Content-Type"] = "application/json"
let parametersAsData = try! JSONSerialization.data(withJSONObject: tsRequest.parameters, options: [ .fragmentsAllowed ])
parametersAsString = !tsRequest.parameters.isEmpty ? String(bytes: parametersAsData, encoding: .utf8)! : "null"
let tsRequestParameters = tsRequest.parameters
if !tsRequestParameters.isEmpty {
guard let parameters = try? JSONSerialization.data(withJSONObject: tsRequestParameters, options: [ .fragmentsAllowed ]) else {
return Promise(error: HTTP.Error.invalidJSON)
}
parametersAsString = String(bytes: parameters, encoding: .utf8) ?? "null"
} else {
parametersAsString = "null"
}
} else {
headers["Content-Type"] = request.allHTTPHeaderFields!["Content-Type"]
if let parametersAsInputStream = request.httpBodyStream, let parametersAsData = try? Data(from: parametersAsInputStream) {
parametersAsString = "{ \"fileUpload\" : \"\(String(data: parametersAsData.base64EncodedData(), encoding: .utf8) ?? "null")\" }"
if let parametersAsInputStream = request.httpBodyStream, let parameters = try? Data(from: parametersAsInputStream) {
parametersAsString = "{ \"fileUpload\" : \"\(String(data: parameters.base64EncodedData(), encoding: .utf8) ?? "null")\" }"
} else {
parametersAsString = "null"
}
@ -270,22 +274,19 @@ public enum OnionRequestAPI {
"method" : request.httpMethod,
"headers" : headers
]
let destination: JSON = [ "host" : request.url?.host,
"target" : "/loki/v1/lsrpc",
"method" : "POST"]
let promise = sendOnionRequest(on: nil, with: payload, to: destination, using: x25519Key, associatedWith: getUserHexEncodedPublicKey(), noJSON: noJSON)
promise.recover2{ error -> Promise<JSON> in
// TODO: File Server API handle Error
throw error
let destination = Destination.server(host: host, x25519PublicKey: x25519Key)
let promise = sendOnionRequest(with: payload, to: destination, associatedWith: getUserHexEncodedPublicKey(), isJSONRequired: isJSONRequired)
promise.catch2 { error in
print("[Loki] [Onion Request API] Couldn't reach server: \(server) due to error: \(error).")
}
return promise
}
internal static func sendOnionRequest(on snode: Snode?, with payload: JSON, to destination: JSON, using x25519Key: String?, associatedWith publicKey: String, noJSON: Bool = false) -> Promise<JSON> {
internal static func sendOnionRequest(with payload: JSON, to destination: Destination, associatedWith publicKey: String, isJSONRequired: Bool = true) -> Promise<JSON> {
let (promise, seal) = Promise<JSON>.pending()
var guardSnode: Snode!
DispatchQueue.global(qos: .userInitiated).async {
buildOnion(around: payload, targetedAt: snode, to: destination, using: x25519Key).done2 { intermediate in
buildOnion(around: payload, targetedAt: destination).done2 { intermediate in
guardSnode = intermediate.guardSnode
let url = "\(guardSnode.address):\(guardSnode.port)/onion_req"
let finalEncryptionResult = intermediate.finalEncryptionResult
@ -310,18 +311,16 @@ public enum OnionRequestAPI {
print("[Loki] The user's clock is out of sync with the service node network.")
seal.reject(SnodeAPI.SnodeAPIError.clockOutOfSync)
} else {
if noJSON {
let body = ["body": bodyAsString]
guard 200...299 ~= statusCode else { return seal.reject(Error.httpRequestFailedAtTargetSnode(statusCode: UInt(statusCode), json: body)) }
seal.fulfill(body)
let body: JSON
if !isJSONRequired {
body = [ "result" : bodyAsString ]
} else {
guard let bodyAsData = bodyAsString.data(using: .utf8),
let body = try JSONSerialization.jsonObject(with: bodyAsData, options: [ .fragmentsAllowed ]) as? JSON else { return seal.reject(HTTP.Error.invalidJSON) }
guard 200...299 ~= statusCode else { return seal.reject(Error.httpRequestFailedAtTargetSnode(statusCode: UInt(statusCode), json: body)) }
seal.fulfill(body)
let b = try JSONSerialization.jsonObject(with: bodyAsData, options: [ .fragmentsAllowed ]) as? JSON else { return seal.reject(HTTP.Error.invalidJSON) }
body = b
}
guard 200...299 ~= statusCode else { return seal.reject(Error.httpRequestFailedAtTargetSnode(statusCode: UInt(statusCode), json: body)) }
seal.fulfill(body)
}
} catch (let error) {
seal.reject(error)

@ -7,9 +7,7 @@ public final class PublicChatAPI : DotNetAPI {
@objc public static let defaultChats: [PublicChat] = [] // Currently unused
public static var displayNameUpdatees: [String:Set<String>] = [:]
public static var useOnionRequests = true
// MARK: Settings
private static let attachmentType = "net.app.core.oembed"
private static let channelInfoType = "net.patter-app.settings"
@ -32,28 +30,6 @@ public final class PublicChatAPI : DotNetAPI {
@objc public static let lastMessageServerIDCollection = "LokiGroupChatLastMessageServerIDCollection"
@objc public static let lastDeletionServerIDCollection = "LokiGroupChatLastDeletionServerIDCollection"
@objc public static let openGroupPublicKeyCollection = "LokiGroupChatPublicKeyCollection"
private static func getOpenGroupPublicKey(on server: String) -> String? {
var result: String? = nil
storage.dbReadConnection.read { transaction in
result = transaction.object(forKey: "\(server)", inCollection: openGroupPublicKeyCollection) as! String?
}
return result
}
private static func setOpneGroupPublicKey(on server: String, value publicKey: String) {
try! Storage.writeSync { transaction in
transaction.setObject(publicKey, forKey: "\(server)", inCollection: openGroupPublicKeyCollection)
}
}
private static func removeOpenGroupPublicKey(on server: String) {
try! Storage.writeSync { transaction in
transaction.removeObject(forKey: "\(server)", inCollection: openGroupPublicKeyCollection)
}
}
private static func getLastMessageServerID(for group: UInt64, on server: String) -> UInt? {
var result: UInt? = nil
storage.dbReadConnection.read { transaction in
@ -97,20 +73,24 @@ public final class PublicChatAPI : DotNetAPI {
public static func clearCaches(for channel: UInt64, on server: String) {
removeLastMessageServerID(for: channel, on: server)
removeLastDeletionServerID(for: channel, on: server)
removeOpenGroupPublicKey(on: server)
try! Storage.writeSync { transaction in
Storage.removeOpenGroupPublicKey(for: server, using: transaction)
}
}
// MARK: Open Group Public Key Validation
public static func getOpenGroupServerPublicKey(on server: String) -> Promise<String> {
if let publicKey = getOpenGroupPublicKey(on: server) {
public static func getOpenGroupServerPublicKey(for server: String) -> Promise<String> {
if let publicKey = Storage.getOpenGroupPublicKey(for: server) {
return Promise.value(publicKey)
} else {
return FileServerAPI.getOpenGroupKey(for: server).then2 { hexEncodedPublicKey -> Promise<String> in
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.sendOnionRequestLsrpcDest(request, server: server, using: hexEncodedPublicKey, noJSON: true).map2 { _ -> String in
setOpneGroupPublicKey(on: server, value: hexEncodedPublicKey)
return hexEncodedPublicKey
return OnionRequestAPI.sendOnionRequest(request, to: server, using: publicKey, isJSONRequired: false).map(on: DispatchQueue.global(qos: .default)) { _ -> String in
try! Storage.writeSync { transaction in
Storage.setOpenGroupPublicKey(for: server, to: publicKey, using: transaction)
}
return publicKey
}
}
}
@ -123,98 +103,88 @@ public final class PublicChatAPI : DotNetAPI {
}
public static func getMessages(for channel: UInt64, on server: String) -> Promise<[PublicChatMessage]> {
func handleMessages(rawResponse: Any) throws -> [PublicChatMessage] {
guard let json = rawResponse as? JSON, let rawMessages = json["data"] as? [JSON] else {
print("[Loki] Couldn't parse messages for public chat channel with ID: \(channel) on server: \(server) from: \(rawResponse).")
throw DotNetAPIError.parsingFailed
}
return rawMessages.flatMap { message in
let isDeleted = (message["is_deleted"] as? Int == 1)
guard !isDeleted else { return nil }
guard let annotations = message["annotations"] as? [JSON], let annotation = annotations.first(where: { $0["type"] as? String == publicChatMessageType }), 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 else {
print("[Loki] Couldn't parse message for public chat channel with ID: \(channel) on server: \(server) from: \(message).")
return nil
}
var profilePicture: PublicChatMessage.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)
}
let lastMessageServerID = getLastMessageServerID(for: channel, on: server)
if serverID > (lastMessageServerID ?? 0) { setLastMessageServerID(for: channel, on: server, to: serverID) }
let quote: PublicChatMessage.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,
quotedMessageServerID: quotedMessageServerID)
} else {
quote = nil
}
let signature = PublicChatMessage.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 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
let height = value["height"] as? UInt ?? 0
let flags = (value["flags"] as? UInt) ?? 0
let caption = value["caption"] as? String
let linkPreviewURL = value["linkPreviewUrl"] as? String
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.")
return nil
}
}
return PublicChatMessage.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)
guard result.hasValidSignature() else {
print("[Loki] Ignoring public chat message with invalid signature.")
return nil
}
var existingMessageID: String? = nil
storage.dbReadConnection.read { transaction in
existingMessageID = storage.getIDForMessage(withServerID: UInt(result.serverID!), in: transaction)
}
guard existingMessageID == nil else {
print("[Loki] Ignoring duplicate public chat message.")
return nil
}
return result
}.sorted { $0.timestamp < $1.timestamp }
}
var queryParameters = "include_annotations=1"
if let lastMessageServerID = getLastMessageServerID(for: channel, on: server) {
queryParameters += "&since_id=\(lastMessageServerID)"
} else {
queryParameters += "&count=\(fallbackBatchCount)&include_deleted=0"
}
return getAuthToken(for: server).then(on: DispatchQueue.global(qos: .default)) { token -> Promise<[PublicChatMessage]> in
let url = URL(string: "\(server)/channels/\(channel)/messages?\(queryParameters)")!
let request = TSRequest(url: url)
request.allHTTPHeaderFields = [ "Content-Type" : "application/json", "Authorization" : "Bearer \(token)" ]
if (useOnionRequests) {
return getOpenGroupServerPublicKey(on: server).then2 { hexEncodedPublickey in
OnionRequestAPI.sendOnionRequestLsrpcDest(request, server: server, using: hexEncodedPublickey).map2 { rawResponse in
return try handleMessages(rawResponse: rawResponse)
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
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)) { rawResponse in
guard let json = rawResponse as? JSON, let rawMessages = json["data"] as? [JSON] else {
print("[Loki] Couldn't parse messages for public chat channel with ID: \(channel) on server: \(server) from: \(rawResponse).")
throw DotNetAPIError.parsingFailed
}
return rawMessages.flatMap { message in
let isDeleted = (message["is_deleted"] as? Int == 1)
guard !isDeleted else { return nil }
guard let annotations = message["annotations"] as? [JSON], let annotation = annotations.first(where: { $0["type"] as? String == publicChatMessageType }), 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 else {
print("[Loki] Couldn't parse message for public chat channel with ID: \(channel) on server: \(server) from: \(message).")
return nil
}
var profilePicture: PublicChatMessage.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)
}
let lastMessageServerID = getLastMessageServerID(for: channel, on: server)
if serverID > (lastMessageServerID ?? 0) { setLastMessageServerID(for: channel, on: server, to: serverID) }
let quote: PublicChatMessage.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,
quotedMessageServerID: quotedMessageServerID)
} else {
quote = nil
}
let signature = PublicChatMessage.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 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
let height = value["height"] as? UInt ?? 0
let flags = (value["flags"] as? UInt) ?? 0
let caption = value["caption"] as? String
let linkPreviewURL = value["linkPreviewUrl"] as? String
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.")
return nil
}
}
return PublicChatMessage.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)
guard result.hasValidSignature() else {
print("[Loki] Ignoring public chat message with invalid signature.")
return nil
}
var existingMessageID: String? = nil
storage.dbReadConnection.read { transaction in
existingMessageID = storage.getIDForMessage(withServerID: UInt(result.serverID!), in: transaction)
}
guard existingMessageID == nil else {
print("[Loki] Ignoring duplicate public chat message.")
return nil
}
return result
}.sorted { $0.timestamp < $1.timestamp }
}
}
return LokiFileServerProxy(for: server).perform(request).map(on: DispatchQueue.global(qos: .default)) { rawResponse in
return try handleMessages(rawResponse: rawResponse)
}
}.handlingInvalidAuthTokenIfNeeded(for: server)
}
@ -225,41 +195,31 @@ public final class PublicChatAPI : DotNetAPI {
}
public static func sendMessage(_ message: PublicChatMessage, to channel: UInt64, on server: String) -> Promise<PublicChatMessage> {
func handleSendMessageResult(rawResponse: Any, with displayName: String, for signedMessage: PublicChatMessage) throws -> PublicChatMessage {
// ISO8601DateFormatter doesn't support milliseconds before iOS 11
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZ"
guard let json = rawResponse as? JSON, 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: \(rawResponse).")
throw DotNetAPIError.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)
}
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) }
attempt(maxRetryCount: maxRetryCount, recoveringOn: DispatchQueue.global(qos: .default)) {
getAuthToken(for: server).then(on: DispatchQueue.global(qos: .default)) { token -> Promise<PublicChatMessage> in
let url = URL(string: "\(server)/channels/\(channel)/messages")!
let parameters = signedMessage.toJSON()
let request = TSRequest(url: url, method: "POST", parameters: parameters)
request.allHTTPHeaderFields = [ "Content-Type" : "application/json", "Authorization" : "Bearer \(token)" ]
let displayName = userDisplayName
if (useOnionRequests) {
return getOpenGroupServerPublicKey(on: server).then2 { hexEncodedPublicKey in
OnionRequestAPI.sendOnionRequestLsrpcDest(request, server: server, using: hexEncodedPublicKey).map2 { rawResponse in
return try handleSendMessageResult(rawResponse: rawResponse, with: displayName, for: signedMessage)
getOpenGroupServerPublicKey(for: server).then(on: DispatchQueue.global(qos: .default)) { serverPublicKey in
getAuthToken(for: server).then(on: DispatchQueue.global(qos: .default)) { token -> Promise<PublicChatMessage> in
let url = URL(string: "\(server)/channels/\(channel)/messages")!
let parameters = signedMessage.toJSON()
let request = TSRequest(url: url, method: "POST", parameters: parameters)
request.allHTTPHeaderFields = [ "Content-Type" : "application/json", "Authorization" : "Bearer \(token)" ]
let displayName = userDisplayName
return OnionRequestAPI.sendOnionRequest(request, to: server, using: serverPublicKey).map(on: DispatchQueue.global(qos: .default)) { rawResponse in
// ISO8601DateFormatter doesn't support milliseconds before iOS 11
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZ"
guard let json = rawResponse as? JSON, 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: \(rawResponse).")
throw DotNetAPIError.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)
}
}
return LokiFileServerProxy(for: server).perform(request).map(on: DispatchQueue.global(qos: .default)) { rawResponse in
return try handleSendMessageResult(rawResponse: rawResponse, with: displayName, for: signedMessage)
}
}.handlingInvalidAuthTokenIfNeeded(for: server)
}.done(on: DispatchQueue.global(qos: .default)) { message in
seal.fulfill(message)
@ -272,23 +232,6 @@ public final class PublicChatAPI : DotNetAPI {
// MARK: Deletion
public static func getDeletedMessageServerIDs(for channel: UInt64, on server: String) -> Promise<[UInt64]> {
func handleDeletedMessageServerIDs(rawResponse: Any) throws -> [UInt64] {
guard let json = rawResponse as? JSON, let deletions = json["data"] as? [JSON] else {
print("[Loki] Couldn't parse deleted messages for public chat channel with ID: \(channel) on server: \(server) from: \(rawResponse).")
throw DotNetAPIError.parsingFailed
}
return deletions.flatMap { 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).")
return nil
}
let lastDeletionServerID = getLastDeletionServerID(for: channel, on: server)
if serverID > (lastDeletionServerID ?? 0) { setLastDeletionServerID(for: channel, on: server, to: serverID) }
return messageServerID
}
}
print("[Loki] Getting deleted messages for public chat channel with ID: \(channel) on server: \(server).")
let queryParameters: String
if let lastDeletionServerID = getLastDeletionServerID(for: channel, on: server) {
@ -296,20 +239,27 @@ public final class PublicChatAPI : DotNetAPI {
} else {
queryParameters = "count=\(fallbackBatchCount)"
}
return getAuthToken(for: server).then(on: DispatchQueue.global(qos: .default)) { token -> Promise<[UInt64]> in
let url = URL(string: "\(server)/loki/v1/channel/\(channel)/deletes?\(queryParameters)")!
let request = TSRequest(url: url)
request.allHTTPHeaderFields = [ "Content-Type" : "application/json", "Authorization" : "Bearer \(token)" ]
if (useOnionRequests) {
return getOpenGroupServerPublicKey(on: server).then2 { hexEncodedPublicKey in
OnionRequestAPI.sendOnionRequestLsrpcDest(request, server: server, using: hexEncodedPublicKey).map2 { rawResponse in
return try handleDeletedMessageServerIDs(rawResponse: rawResponse)
return getOpenGroupServerPublicKey(for: server).then(on: DispatchQueue.global(qos: .default)) { serverPublicKey in
getAuthToken(for: server).then(on: DispatchQueue.global(qos: .default)) { token -> Promise<[UInt64]> in
let url = URL(string: "\(server)/loki/v1/channel/\(channel)/deletes?\(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)) { rawResponse in
guard let json = rawResponse as? JSON, let deletions = json["data"] as? [JSON] else {
print("[Loki] Couldn't parse deleted messages for public chat channel with ID: \(channel) on server: \(server) from: \(rawResponse).")
throw DotNetAPIError.parsingFailed
}
return deletions.flatMap { 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).")
return nil
}
let lastDeletionServerID = getLastDeletionServerID(for: channel, on: server)
if serverID > (lastDeletionServerID ?? 0) { setLastDeletionServerID(for: channel, on: server, to: serverID) }
return messageServerID
}
}
}
return LokiFileServerProxy(for: server).perform(request).map(on: DispatchQueue.global(qos: .default)) { rawResponse in
return try handleDeletedMessageServerIDs(rawResponse: rawResponse)
}
}.handlingInvalidAuthTokenIfNeeded(for: server)
}
@ -323,61 +273,46 @@ public final class PublicChatAPI : DotNetAPI {
print("[Loki] Deleting message with ID: \(messageID) for public chat 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)) {
getAuthToken(for: server).then(on: DispatchQueue.global(qos: .default)) { token -> Promise<Void> in
let url = URL(string: urlAsString)!
let request = TSRequest(url: url, method: "DELETE", parameters: [:])
request.allHTTPHeaderFields = [ "Content-Type" : "application/json", "Authorization" : "Bearer \(token)" ]
if (useOnionRequests) {
return getOpenGroupServerPublicKey(on: server).then2 { hexEncodedPublicKey in
OnionRequestAPI.sendOnionRequestLsrpcDest(request, server: server, using: hexEncodedPublicKey).done2 { result -> Void in
print("[Loki] Deleted message with ID: \(messageID) on server: \(server).")
}
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 url = URL(string: urlAsString)!
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)) { result -> Void in
print("[Loki] Deleted message with ID: \(messageID) on server: \(server).")
}
}
return LokiFileServerProxy(for: server).perform(request).done(on: DispatchQueue.global(qos: .default)) { result -> Void in
print("[Loki] Deleted message with ID: \(messageID) on server: \(server).")
}
}.handlingInvalidAuthTokenIfNeeded(for: server)
}
}
// MARK: Display Name & Profile Picture
public static func getDisplayNames(for channel: UInt64, on server: String) -> Promise<Void> {
func handleDisplayNames(rawResponse: Any, for hexEncodedPublicKeys: Set<String>) throws {
guard let json = rawResponse as? JSON, let data = json["data"] as? [JSON] else {
print("[Loki] Couldn't parse display names for users: \(hexEncodedPublicKeys) from: \(rawResponse).")
throw DotNetAPIError.parsingFailed
}
try! Storage.writeSync { 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)")
}
}
}
let publicChatID = "\(server).\(channel)"
guard let hexEncodedPublicKeys = displayNameUpdatees[publicChatID] else { return Promise.value(()) }
guard let publicKeys = displayNameUpdatees[publicChatID] else { return Promise.value(()) }
displayNameUpdatees[publicChatID] = []
print("[Loki] Getting display names for: \(hexEncodedPublicKeys).")
return getAuthToken(for: server).then(on: DispatchQueue.global(qos: .default)) { token -> Promise<Void> in
let queryParameters = "ids=\(hexEncodedPublicKeys.map { "@\($0)" }.joined(separator: ","))&include_user_annotations=1"
let url = URL(string: "\(server)/users?\(queryParameters)")!
let request = TSRequest(url: url)
if (useOnionRequests) {
return getOpenGroupServerPublicKey(on: server).then2 { hexEncodedPublicKey in
OnionRequestAPI.sendOnionRequestLsrpcDest(request, server: server, using: hexEncodedPublicKey).map2 { rawResponse in
try handleDisplayNames(rawResponse: rawResponse, for: hexEncodedPublicKeys)
print("[Loki] 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"
let url = URL(string: "\(server)/users?\(queryParameters)")!
let request = TSRequest(url: url)
return OnionRequestAPI.sendOnionRequest(request, to: server, using: serverPublicKey).map(on: DispatchQueue.global(qos: .default)) { rawResponse in
guard let json = rawResponse as? JSON, let data = json["data"] as? [JSON] else {
print("[Loki] Couldn't parse display names for users: \(publicKeys) from: \(rawResponse).")
throw DotNetAPIError.parsingFailed
}
try! Storage.writeSync { 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)")
}
}
}
}
return LokiFileServerProxy(for: server).perform(request).map(on: DispatchQueue.global(qos: .default)) { rawResponse in
try handleDisplayNames(rawResponse: rawResponse, for: hexEncodedPublicKeys)
}
}.handlingInvalidAuthTokenIfNeeded(for: server)
}
@ -390,22 +325,16 @@ public final class PublicChatAPI : DotNetAPI {
print("[Loki] Updating display name on server: \(server).")
let parameters: JSON = [ "name" : (newDisplayName ?? "") ]
return attempt(maxRetryCount: maxRetryCount, recoveringOn: DispatchQueue.global(qos: .default)) {
getAuthToken(for: server).then(on: DispatchQueue.global(qos: .default)) { token -> Promise<Void> in
let url = URL(string: "\(server)/users/me")!
let request = TSRequest(url: url, method: "PATCH", parameters: parameters)
request.allHTTPHeaderFields = [ "Content-Type" : "application/json", "Authorization" : "Bearer \(token)" ]
if (useOnionRequests) {
return getOpenGroupServerPublicKey(on: server).then2 { hexEncodedPublicKey in
OnionRequestAPI.sendOnionRequestLsrpcDest(request, server: server, using: hexEncodedPublicKey).map2 { _ in }.recover(on: DispatchQueue.global(qos: .default)) { error in
print("Couldn't update display name due to error: \(error).")
throw error
}
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 url = URL(string: "\(server)/users/me")!
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("Couldn't update display name due to error: \(error).")
throw error
}
}
return LokiFileServerProxy(for: server).perform(request).map(on: DispatchQueue.global(qos: .default)) { _ in }.recover(on: DispatchQueue.global(qos: .default)) { error in
print("Couldn't update display name due to error: \(error).")
throw error
}
}.handlingInvalidAuthTokenIfNeeded(for: server)
}
}
@ -423,22 +352,16 @@ public final class PublicChatAPI : DotNetAPI {
}
let parameters: JSON = [ "annotations" : [ annotation ] ]
return attempt(maxRetryCount: maxRetryCount, recoveringOn: DispatchQueue.global(qos: .default)) {
getAuthToken(for: server).then(on: DispatchQueue.global(qos: .default)) { token -> Promise<Void> in
let url = URL(string: "\(server)/users/me")!
let request = TSRequest(url: url, method: "PATCH", parameters: parameters)
request.allHTTPHeaderFields = [ "Content-Type" : "application/json", "Authorization" : "Bearer \(token)" ]
if (useOnionRequests) {
return getOpenGroupServerPublicKey(on: server).then2 { hexEncodedPublicKey in
OnionRequestAPI.sendOnionRequestLsrpcDest(request, server: server, using: hexEncodedPublicKey).map2 { _ in }.recover(on: DispatchQueue.global(qos: .default)) { error in
print("[Loki] Couldn't update profile picture due to error: \(error).")
throw error
}
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 url = URL(string: "\(server)/users/me")!
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).")
throw error
}
}
return LokiFileServerProxy(for: server).perform(request).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).")
throw error
}
}.handlingInvalidAuthTokenIfNeeded(for: server)
}
}
@ -461,10 +384,10 @@ public final class PublicChatAPI : DotNetAPI {
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 avatarURL = info.profilePictureURL {
if let profilePictureURL = info.profilePictureURL {
let configuration = URLSessionConfiguration.default
let manager = AFURLSessionManager.init(sessionConfiguration: configuration)
let url = URL(string: "\(server)\(avatarURL)")!
let url = URL(string: "\(server)\(profilePictureURL)")!
let request = URLRequest(url: url)
let task = manager.downloadTask(with: request, progress: nil,
destination: { (targetPath: URL, response: URLResponse) -> URL in
@ -495,84 +418,64 @@ public final class PublicChatAPI : DotNetAPI {
}
public static func getInfo(for channel: UInt64, on server: String) -> Promise<PublicChatInfo> {
func handleInfo(rawResponse: Any) throws -> PublicChatInfo {
guard let json = rawResponse as? JSON,
let data = json["data"] as? JSON,
let annotations = data["annotations"] as? [JSON],
let annotation = annotations.first,
let info = annotation["value"] as? JSON,
let displayName = info["name"] as? String,
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: \(rawResponse).")
throw DotNetAPIError.parsingFailed
}
let storage = OWSPrimaryStorage.shared()
try! Storage.writeSync { transaction in
storage.setUserCount(memberCount, forPublicChatWithID: "\(server).\(channel)", in: transaction)
}
let publicChatInfo = PublicChatInfo(displayName: displayName, profilePictureURL: profilePictureURL, memberCount: memberCount)
updateProfileIfNeeded(for: channel, on: server, from: publicChatInfo)
return publicChatInfo
}
return attempt(maxRetryCount: maxRetryCount, recoveringOn: DispatchQueue.global(qos: .default)) {
getAuthToken(for: server).then(on: DispatchQueue.global(qos: .default)) { token -> Promise<PublicChatInfo> 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)" ]
if (useOnionRequests) {
return getOpenGroupServerPublicKey(on: server).then2 { hexEncodedPublicKey in
return OnionRequestAPI.sendOnionRequestLsrpcDest(request, server: server, using: hexEncodedPublicKey).map2 { rawResponse in
return try handleInfo(rawResponse: rawResponse)
getOpenGroupServerPublicKey(for: server).then(on: DispatchQueue.global(qos: .default)) { serverPublicKey in
getAuthToken(for: server).then(on: DispatchQueue.global(qos: .default)) { token -> Promise<PublicChatInfo> 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)" ]
return OnionRequestAPI.sendOnionRequest(request, to: server, using: serverPublicKey).map(on: DispatchQueue.global(qos: .default)) { rawResponse in
guard let json = rawResponse as? JSON,
let data = json["data"] as? JSON,
let annotations = data["annotations"] as? [JSON],
let annotation = annotations.first,
let info = annotation["value"] as? JSON,
let displayName = info["name"] as? String,
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: \(rawResponse).")
throw DotNetAPIError.parsingFailed
}
let storage = OWSPrimaryStorage.shared()
try! Storage.writeSync { transaction in
storage.setUserCount(memberCount, forPublicChatWithID: "\(server).\(channel)", in: transaction)
}
let publicChatInfo = PublicChatInfo(displayName: displayName, profilePictureURL: profilePictureURL, memberCount: memberCount)
updateProfileIfNeeded(for: channel, on: server, from: publicChatInfo)
return publicChatInfo
}
}
return LokiFileServerProxy(for: server).perform(request).map(on: DispatchQueue.global(qos: .default)) { rawResponse in
return try handleInfo(rawResponse: rawResponse)
}
}.handlingInvalidAuthTokenIfNeeded(for: server)
}
}
public static func join(_ channel: UInt64, on server: String) -> Promise<Void> {
return attempt(maxRetryCount: maxRetryCount, recoveringOn: DispatchQueue.global(qos: .default)) {
getAuthToken(for: server).then(on: DispatchQueue.global(qos: .default)) { token -> Promise<Void> in
let url = URL(string: "\(server)/channels/\(channel)/subscribe")!
let request = TSRequest(url: url, method: "POST", parameters: [:])
request.allHTTPHeaderFields = [ "Content-Type" : "application/json", "Authorization" : "Bearer \(token)" ]
if (useOnionRequests) {
return getOpenGroupServerPublicKey(on: server).then2 { hexEncodedPublicKey in
OnionRequestAPI.sendOnionRequestLsrpcDest(request, server: server, using: hexEncodedPublicKey).done2 { result -> Void in
print("[Loki] Joined channel with ID: \(channel) on server: \(server).")
}
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 url = URL(string: "\(server)/channels/\(channel)/subscribe")!
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)) { result -> Void in
print("[Loki] Joined channel with ID: \(channel) on server: \(server).")
}
}
return LokiFileServerProxy(for: server).perform(request).done(on: DispatchQueue.global(qos: .default)) { result -> Void in
print("[Loki] Joined channel with ID: \(channel) on server: \(server).")
}
}.handlingInvalidAuthTokenIfNeeded(for: server)
}
}
public static func leave(_ channel: UInt64, on server: String) -> Promise<Void> {
return attempt(maxRetryCount: maxRetryCount, recoveringOn: DispatchQueue.global(qos: .default)) {
getAuthToken(for: server).then(on: DispatchQueue.global(qos: .default)) { token -> Promise<Void> in
let url = URL(string: "\(server)/channels/\(channel)/subscribe")!
let request = TSRequest(url: url, method: "DELETE", parameters: [:])
request.allHTTPHeaderFields = [ "Content-Type" : "application/json", "Authorization" : "Bearer \(token)" ]
if (useOnionRequests) {
return getOpenGroupServerPublicKey(on: server).then2 { hexEncodedPublicKey in
OnionRequestAPI.sendOnionRequestLsrpcDest(request, server: server, using: hexEncodedPublicKey).done2 { result -> Void in
print("[Loki] Left channel with ID: \(channel) on server: \(server).")
}
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 url = URL(string: "\(server)/channels/\(channel)/subscribe")!
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)) { result -> Void in
print("[Loki] Left channel with ID: \(channel) on server: \(server).")
}
}
return LokiFileServerProxy(for: server).perform(request).done(on: DispatchQueue.global(qos: .default)) { result -> Void in
print("[Loki] Left channel with ID: \(channel) on server: \(server).")
}
}.handlingInvalidAuthTokenIfNeeded(for: server)
}
}
@ -587,45 +490,32 @@ public final class PublicChatAPI : DotNetAPI {
let url = URL(string: "\(server)/loki/v1/channels/\(channel)/messages/\(messageID)/report")!
let request = TSRequest(url: url, method: "POST", parameters: [:])
// Only used for the Loki Public Chat which doesn't require authentication
if (useOnionRequests) {
return getOpenGroupServerPublicKey(on: server).then2 { hexEncodedPublicKey in
OnionRequestAPI.sendOnionRequestLsrpcDest(request, server: server, using: hexEncodedPublicKey).map2 { _ in}
}
return getOpenGroupServerPublicKey(for: server).then(on: DispatchQueue.global(qos: .default)) { serverPublicKey in
OnionRequestAPI.sendOnionRequest(request, to: server, using: serverPublicKey).map(on: DispatchQueue.global(qos: .default)) { _ in}
}
return LokiFileServerProxy(for: server).perform(request).map(on: DispatchQueue.global(qos: .default)) { _ in }
}
// MARK: Moderators
public static func getModerators(for channel: UInt64, on server: String) -> Promise<Set<String>> {
func handleModerators(rawResponse: Any) throws -> Set<String> {
guard let json = rawResponse as? JSON, let moderators = json["moderators"] as? [String] else {
print("[Loki] Couldn't parse moderators for public chat channel with ID: \(channel) on server: \(server) from: \(rawResponse).")
throw DotNetAPIError.parsingFailed
}
let moderatorsAsSet = Set(moderators);
if self.moderators.keys.contains(server) {
self.moderators[server]![channel] = moderatorsAsSet
} else {
self.moderators[server] = [ channel : moderatorsAsSet ]
}
return moderatorsAsSet
}
return getAuthToken(for: server).then(on: DispatchQueue.global(qos: .default)) { token -> Promise<Set<String>> in
let url = URL(string: "\(server)/loki/v1/channel/\(channel)/get_moderators")!
let request = TSRequest(url: url)
request.allHTTPHeaderFields = [ "Content-Type" : "application/json", "Authorization" : "Bearer \(token)" ]
if (useOnionRequests) {
return getOpenGroupServerPublicKey(on: server).then2 { hexEncodedPublicKey in
OnionRequestAPI.sendOnionRequestLsrpcDest(request, server: server, using: hexEncodedPublicKey).map2 { rawResponse in
return try handleModerators(rawResponse: rawResponse)
return getOpenGroupServerPublicKey(for: server).then(on: DispatchQueue.global(qos: .default)) { serverPublicKey in
getAuthToken(for: server).then(on: DispatchQueue.global(qos: .default)) { token -> Promise<Set<String>> in
let url = URL(string: "\(server)/loki/v1/channel/\(channel)/get_moderators")!
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)) { rawResponse in
guard let json = rawResponse as? JSON, let moderators = json["moderators"] as? [String] else {
print("[Loki] Couldn't parse moderators for public chat channel with ID: \(channel) on server: \(server) from: \(rawResponse).")
throw DotNetAPIError.parsingFailed
}
let moderatorsAsSet = Set(moderators);
if self.moderators.keys.contains(server) {
self.moderators[server]![channel] = moderatorsAsSet
} else {
self.moderators[server] = [ channel : moderatorsAsSet ]
}
return moderatorsAsSet
}
}
return LokiFileServerProxy(for: server).perform(request).map(on: DispatchQueue.global(qos: .default)) { rawResponse in
return try handleModerators(rawResponse: rawResponse)
}
}.handlingInvalidAuthTokenIfNeeded(for: server)
}

@ -56,20 +56,7 @@ public final class PublicChatManager : NSObject {
return Promise(error: Error.chatCreationFailed)
}
}
if (PublicChatAPI.useOnionRequests) {
return PublicChatAPI.getOpenGroupServerPublicKey(on: server).then2 { publicKey in
return PublicChatAPI.getAuthToken(for: server).then2 { token in
return PublicChatAPI.getInfo(for: channel, on: server)
}.map2 { channelInfo -> PublicChat in
guard let chat = self.addChat(server: server, channel: channel, name: channelInfo.displayName) else { throw Error.chatCreationFailed }
return chat
}
}
}
// TODO: Remove this when we use onion request totally
return PublicChatAPI.getAuthToken(for: server).then2 { token in
return PublicChatAPI.getInfo(for: channel, on: server)
}.map2 { channelInfo -> PublicChat in
return PublicChatAPI.getInfo(for: channel, on: server).map2 { channelInfo -> PublicChat in
guard let chat = self.addChat(server: server, channel: channel, name: channelInfo.displayName) else { throw Error.chatCreationFailed }
return chat
}

@ -0,0 +1,22 @@
public extension Storage {
// MARK: Open Group Public Keys
internal static let openGroupPublicKeyCollection = "LokiOpenGroupPublicKeyCollection"
internal static func getOpenGroupPublicKey(for server: String) -> String? {
var result: String? = nil
read { transaction in
result = transaction.object(forKey: server, inCollection: openGroupPublicKeyCollection) as? String
}
return result
}
internal static func setOpenGroupPublicKey(for server: String, to publicKey: String, using transaction: YapDatabaseReadWriteTransaction) {
transaction.setObject(publicKey, forKey: server, inCollection: openGroupPublicKeyCollection)
}
internal static func removeOpenGroupPublicKey(for server: String, using transaction: YapDatabaseReadWriteTransaction) {
transaction.removeObject(forKey: server, inCollection: openGroupPublicKeyCollection)
}
}

@ -1,17 +0,0 @@
@objc(LKRSSFeed)
public final class LokiRSSFeed : NSObject {
@objc public let id: String
@objc public let server: String
@objc public let displayName: String
@objc public let isDeletable: Bool
@objc public init(id: String, server: String, displayName: String, isDeletable: Bool) {
self.id = "rss://\(id)"
self.server = server
self.displayName = displayName
self.isDeletable = isDeletable
}
override public var description: String { return displayName }
}

@ -1,26 +0,0 @@
import PromiseKit
public enum LokiRSSFeedProxy {
public enum Error : LocalizedError {
case proxyResponseParsingFailed
public var errorDescription: String? {
switch self {
case .proxyResponseParsingFailed: return "Couldn't parse RSS feed proxy response."
}
}
}
public static func fetchContent(for url: String) -> Promise<String> {
let server = FileServerAPI.server
let endpoints = [ "messenger-updates/feed" : "loki/v1/rss/messenger", "loki.network/feed" : "loki/v1/rss/loki" ]
let endpoint = endpoints.first { url.lowercased().contains($0.key) }!.value
let url = URL(string: server + "/" + endpoint)!
let request = TSRequest(url: url)
return LokiFileServerProxy(for: server).perform(request).map2 { response -> String in
guard let json = response as? JSON, let xml = json["data"] as? String else { throw Error.proxyResponseParsingFailed }
return xml
}
}
}

@ -47,7 +47,7 @@ public final class SnodeAPI : NSObject {
// MARK: Core
internal static func invoke(_ method: Snode.Method, on snode: Snode, associatedWith publicKey: String, parameters: JSON) -> RawResponsePromise {
if useOnionRequests {
return OnionRequestAPI.sendOnionRequestSnodeDest(invoking: method, on: snode, with: parameters, associatedWith: publicKey).map2 { $0 as Any }
return OnionRequestAPI.sendOnionRequest(to: snode, invoking: method, with: parameters, associatedWith: publicKey).map2 { $0 as Any }
} else {
let url = "\(snode.address):\(snode.port)/storage_rpc/v1"
return HTTP.execute(.post, url, parameters: parameters).map2 { $0 as Any }.recover2 { error -> Promise<Any> in

@ -1,11 +1,11 @@
import PromiseKit
internal enum HTTP {
public enum HTTP {
private static let urlSession = URLSession(configuration: .ephemeral, delegate: urlSessionDelegate, delegateQueue: nil)
private static let urlSessionDelegate = URLSessionDelegateImplementation()
// MARK: Settings
private static let timeout: TimeInterval = 20
public static let timeout: TimeInterval = 20
// MARK: URL Session Delegate Implementation
private final class URLSessionDelegateImplementation : NSObject, URLSessionDelegate {
@ -17,7 +17,7 @@ internal enum HTTP {
}
// MARK: Verb
internal enum Verb : String {
public enum Verb : String {
case get = "GET"
case put = "PUT"
case post = "POST"
@ -25,12 +25,12 @@ internal enum HTTP {
}
// MARK: Error
internal enum Error : LocalizedError {
public enum Error : LocalizedError {
case generic
case httpRequestFailed(statusCode: UInt, json: JSON?)
case invalidJSON
var errorDescription: String? {
public var errorDescription: String? {
switch self {
case .generic: return "An error occurred."
case .httpRequestFailed(let statusCode, _): return "HTTP request failed with status code: \(statusCode)."
@ -40,7 +40,7 @@ internal enum HTTP {
}
// MARK: Main
internal static func execute(_ verb: Verb, _ url: String, parameters: JSON? = nil, timeout: TimeInterval = HTTP.timeout) -> Promise<JSON> {
public static func execute(_ verb: Verb, _ url: String, parameters: JSON? = nil, timeout: TimeInterval = HTTP.timeout) -> Promise<JSON> {
var request = URLRequest(url: URL(string: url)!)
request.httpMethod = verb.rawValue
if let parameters = parameters {

Loading…
Cancel
Save