From bf54d78b9919cbe5d5b5bc50d46525f90d0fdb46 Mon Sep 17 00:00:00 2001
From: Niels Andriesse <andriesseniels@gmail.com>
Date: Thu, 20 Feb 2020 16:59:05 +1100
Subject: [PATCH] Untie profile picture from auth token

---
 Signal/Signal-Info.plist                      |  2 +-
 Signal/src/AppDelegate.m                      | 21 +++++++-
 SignalMessaging/profiles/OWSProfileManager.m  |  6 +--
 .../src/Loki/API/LokiFileServerAPI.swift      | 53 ++++++++-----------
 .../src/Loki/Utilities/LKUserDefaults.swift   | 11 +++-
 5 files changed, 56 insertions(+), 37 deletions(-)

diff --git a/Signal/Signal-Info.plist b/Signal/Signal-Info.plist
index 342d37adb..378a5f3ca 100644
--- a/Signal/Signal-Info.plist
+++ b/Signal/Signal-Info.plist
@@ -5,7 +5,7 @@
 	<key>BuildDetails</key>
 	<dict>
 		<key>CarthageVersion</key>
-		<string>0.34.0</string>
+		<string>0.33.0</string>
 		<key>OSXVersion</key>
 		<string>10.15.3</string>
 		<key>WebRTCCommit</key>
diff --git a/Signal/src/AppDelegate.m b/Signal/src/AppDelegate.m
index 3039232bf..c3e764c2a 100644
--- a/Signal/src/AppDelegate.m
+++ b/Signal/src/AppDelegate.m
@@ -770,6 +770,8 @@ static NSTimeInterval launchStartedAt;
             [self.socketManager requestSocketOpen];
             [Environment.shared.contactsManager fetchSystemContactsOnceIfAlreadyAuthorized];
 
+            NSString *userHexEncodedPublicKey = self.tsAccountManager.localNumber;
+
             // Loki: Tell our friends that we are online
             [LKP2PAPI broadcastOnlineStatus];
 
@@ -777,7 +779,22 @@ static NSTimeInterval launchStartedAt;
             [self startLongPollerIfNeeded];
 
             // Loki: Get device links
-            [LKFileServerAPI getDeviceLinksAssociatedWith:self.tsAccountManager.localNumber];
+            [[LKFileServerAPI getDeviceLinksAssociatedWith:userHexEncodedPublicKey] retainUntilComplete];
+
+            // Loki: Update profile picture if needed
+            NSUserDefaults *userDefaults = NSUserDefaults.standardUserDefaults;
+            NSDate *now = [NSDate new];
+            NSDate *lastProfilePictureUpload = (NSDate *)[userDefaults objectForKey:@"lastProfilePictureUpload"];
+            if ([now timeIntervalSinceDate:lastProfilePictureUpload] > 14 * 24 * 60 * 60) {
+                OWSProfileManager *profileManager = OWSProfileManager.sharedManager;
+                NSString *displayName = [profileManager profileNameForRecipientId:userHexEncodedPublicKey];
+                UIImage *profilePicture = [profileManager profileAvatarForRecipientId:userHexEncodedPublicKey];
+                [profileManager updateLocalProfileName:displayName avatarImage:profilePicture success:^{
+                    [userDefaults setObject:now forKey:@"lastProfilePictureUpload"];
+                } failure:^(NSError *error) {
+                    // Do nothing
+                } requiresSync:YES];
+            }
             
             if (![UIApplication sharedApplication].isRegisteredForRemoteNotifications) {
                 OWSLogInfo(@"Retrying to register for remote notifications since user hasn't registered yet.");
@@ -1448,7 +1465,7 @@ static NSTimeInterval launchStartedAt;
         [self startLongPollerIfNeeded];
 
         // Loki: Get device links
-        [LKFileServerAPI getDeviceLinksAssociatedWith:self.tsAccountManager.localNumber];
+        [[LKFileServerAPI getDeviceLinksAssociatedWith:self.tsAccountManager.localNumber] retainUntilComplete]; // TODO: Is this even needed?
     }
 }
 
diff --git a/SignalMessaging/profiles/OWSProfileManager.m b/SignalMessaging/profiles/OWSProfileManager.m
index bd2b6887e..1bbea7d6e 100644
--- a/SignalMessaging/profiles/OWSProfileManager.m
+++ b/SignalMessaging/profiles/OWSProfileManager.m
@@ -416,10 +416,10 @@ typedef void (^ProfileManagerFailureBlock)(NSError *error);
             NSData *encryptedAvatarData = [self encryptProfileData:avatarData profileKey:newProfileKey];
             OWSAssertDebug(encryptedAvatarData.length > 0);
             
-            [[LKFileServerAPI setProfilePicture:encryptedAvatarData]
-            .thenOn(dispatch_get_main_queue(), ^(NSString *url) {
+            [[LKFileServerAPI uploadProfilePicture:encryptedAvatarData]
+            .thenOn(dispatch_get_main_queue(), ^(NSString *downloadURL) {
                 [self.localUserProfile updateWithProfileKey:newProfileKey dbConnection:self.dbConnection completion:^{
-                   successBlock(url);
+                   successBlock(downloadURL);
                 }];
             })
             .catchOn(dispatch_get_main_queue(), ^(id result) {
diff --git a/SignalServiceKit/src/Loki/API/LokiFileServerAPI.swift b/SignalServiceKit/src/Loki/API/LokiFileServerAPI.swift
index 7dcac5e9f..9bde68e93 100644
--- a/SignalServiceKit/src/Loki/API/LokiFileServerAPI.swift
+++ b/SignalServiceKit/src/Loki/API/LokiFileServerAPI.swift
@@ -137,40 +137,33 @@ public final class LokiFileServerAPI : LokiDotNetAPI {
     }
     
     // MARK: Profile Pictures (Public API)
-    public static func setProfilePicture(_ profilePicture: Data) -> Promise<String> {
-        return Promise<String>() { seal in
-            guard profilePicture.count < maxFileSize else { return seal.reject(LokiDotNetAPIError.maxFileSizeExceeded) }
-            getAuthToken(for: server).done { token in
-                let url = "\(server)/users/me/avatar"
-                let parameters: JSON = [ "type" : attachmentType, "Content-Type" : "application/binary" ]
-                var error: NSError?
-                var request = AFHTTPRequestSerializer().multipartFormRequest(withMethod: "POST", urlString: url, parameters: parameters, constructingBodyWith: { formData in
-                    formData.appendPart(withFileData: profilePicture, name: "avatar", fileName: UUID().uuidString, mimeType: "application/binary")
-                }, error: &error)
-                request.addValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
-                if let error = error {
-                    print("[Loki] Couldn't upload profile picture due to error: \(error).")
-                    throw error
-                }
-                let _ = LokiFileServerProxy(for: server).performLokiFileServerNSURLRequest(request as NSURLRequest).done { responseObject in
-                    guard let json = responseObject as? JSON, let data = json["data"] as? JSON, let profilePicture = data["avatar_image"] as? JSON, let downloadURL = profilePicture["url"] as? String else {
-                        print("[Loki] Couldn't parse profile picture from: \(responseObject).")
-                        return seal.reject(LokiDotNetAPIError.parsingFailed)
-                    }
-                    return seal.fulfill(downloadURL)
-                }.catch { error in
-                    seal.reject(error)
-                }
-            }.catch { error in
-                print("[Loki] Couldn't upload profile picture due to error: \(error).")
-                seal.reject(error)
+    public static func uploadProfilePicture(_ profilePicture: Data) -> Promise<String> {
+        guard profilePicture.count < maxFileSize else { return Promise(error: LokiDotNetAPIError.maxFileSizeExceeded) }
+        let url = "\(server)/files"
+        let parameters: JSON = [ "type" : attachmentType, "Content-Type" : "application/binary" ]
+        var error: NSError?
+        var request = AFHTTPRequestSerializer().multipartFormRequest(withMethod: "POST", urlString: url, parameters: parameters, constructingBodyWith: { formData in
+            formData.appendPart(withFileData: profilePicture, name: "content", fileName: UUID().uuidString, mimeType: "application/binary")
+        }, error: &error)
+        // Uploads to the Loki File Server shouldn't include any personally identifiable information so use a dummy auth token
+        request.addValue("Bearer loki", forHTTPHeaderField: "Authorization")
+        if let error = error {
+            print("[Loki] Couldn't upload profile picture due to error: \(error).")
+            return Promise(error: error)
+        }
+        return LokiFileServerProxy(for: server).performLokiFileServerNSURLRequest(request as NSURLRequest).map { 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).")
+                throw LokiDotNetAPIError.parsingFailed
             }
+            UserDefaults.standard[.lastProfilePictureUpload] = Date().timeIntervalSince1970
+            return downloadURL
         }
     }
     
     // MARK: Profile Pictures (Public Obj-C API)
-    @objc(setProfilePicture:)
-    public static func objc_setProfilePicture(_ profilePicture: Data) -> AnyPromise {
-        return AnyPromise.from(setProfilePicture(profilePicture))
+    @objc(uploadProfilePicture:)
+    public static func objc_uploadProfilePicture(_ profilePicture: Data) -> AnyPromise {
+        return AnyPromise.from(uploadProfilePicture(profilePicture))
     }
 }
diff --git a/SignalServiceKit/src/Loki/Utilities/LKUserDefaults.swift b/SignalServiceKit/src/Loki/Utilities/LKUserDefaults.swift
index f1d426fd7..84921c642 100644
--- a/SignalServiceKit/src/Loki/Utilities/LKUserDefaults.swift
+++ b/SignalServiceKit/src/Loki/Utilities/LKUserDefaults.swift
@@ -9,7 +9,11 @@ public enum LKUserDefaults {
         /// Whether the device was unlinked as a slave device (used to notify the user on the landing screen).
         case wasUnlinked
     }
-    
+
+    public enum Date : Swift.String {
+        case lastProfilePictureUpload
+    }
+
     public enum Double : Swift.String {
         case lastDeviceTokenUpload = "lastDeviceTokenUploadTime"
     }
@@ -36,6 +40,11 @@ public extension UserDefaults {
         get { return self.bool(forKey: bool.rawValue) }
         set { set(newValue, forKey: bool.rawValue) }
     }
+
+    public subscript(date: LKUserDefaults.Date) -> Date? {
+        get { return self.object(forKey: date.rawValue) as? Date }
+        set { set(newValue, forKey: date.rawValue) }
+    }
     
     public subscript(double: LKUserDefaults.Double) -> Double {
         get { return self.double(forKey: double.rawValue) }