From d63bf9da73ae249ef37611068aad5702100b6320 Mon Sep 17 00:00:00 2001 From: Aditya Vaidyam Date: Mon, 19 Jun 2017 02:05:57 -0400 Subject: [PATCH] Better Google Photos image/video upload. --- Hangouts/API.swift | 120 +++++++++++++++++++++++++++- Hangouts/Client.swift | 146 +++++++++++++--------------------- Mocha/FoundationSupport.swift | 13 ++- 3 files changed, 184 insertions(+), 95 deletions(-) diff --git a/Hangouts/API.swift b/Hangouts/API.swift index 87724c4..5ab2258 100644 --- a/Hangouts/API.swift +++ b/Hangouts/API.swift @@ -34,6 +34,124 @@ fileprivate extension Client { } } +// Google Photos Resumable Uploads +public extension Client { + +// Google Photos Upload/Resumable FORMAT: +/* +{ + "protocolVersion": "0.8", + "createSessionRequest": { + "fields": [ + { + "external": { + "name": "file", + "filename": "", + "put": {}, + "size": + } + }, + { + "inlined": { + "name": "use_upload_size_pref", + "content": "true", + "contentType": "text/plain" + } + }, + { + "inlined": { + "name": "album_mode", + "content": "temporary", + "contentType": "text/plain" + } + }, + { + "inlined": { + "name": "title", + "content": "", + "contentType": "text/plain" + } + }, + { + "inlined": { + "name": "addtime", + "content": "", + "contentType": "text/plain" + } + }, + { + "inlined": { + "name": "batchid", + "content": "", + "contentType": "text/plain" + } + }, + { + "inlined": { + "name": "album_name", + "content": "", + "contentType": "text/plain" + } + }, + { + "inlined": { + "name": "album_abs_position", + "content": "0", + "contentType": "text/plain" + } + }, + { + "inlined": { + "name": "client", + "content": "hangouts", + "contentType": "text/plain" + } + } + ] + } +} +*/ + + // Upload an image that can be later attached to a chat message. + // The name of the uploaded file may be changed by specifying the filename argument. + public func uploadImage(data: Data, filename: String, cb: ((String) -> Void)? = nil) { + let now = Date(), msec = Int64(now.timeIntervalSince1970 * 1000) + let jst = +""" +{"protocolVersion":"0.8","createSessionRequest":{"fields":[{"external":{"name":"file","filename":"\(filename)","put":{},"size":\(data.count)}},{"inlined":{"name":"use_upload_size_pref","content":"true","contentType":"text/plain"}},{"inlined":{"name":"album_mode","content":"temporary","contentType":"text/plain"}},{"inlined":{"name":"title","content":"\(filename)","contentType":"text/plain"}},{"inlined":{"name":"addtime","content":"\(msec)","contentType":"text/plain"}},{"inlined":{"name":"batchid","content":"\(msec)","contentType":"text/plain"}},{"inlined":{"name":"album_name","content":"\(now.fullString(false))","contentType":"text/plain"}},{"inlined":{"name":"album_abs_position","content":"0","contentType":"text/plain"}},{"inlined":{"name":"client","content":"hangouts","contentType":"text/plain"}}]}} +""" + + self.channel?.base_request(path: Client.IMAGE_UPLOAD_URL, + content_type: "application/x-www-form-urlencoded;charset=UTF-8", + data: jst.data(using: .utf8)!) { response in + + // Sift through JSON for a response with the upload URL. + let _data: NSDictionary = try! JSONSerialization.jsonObject(with: response.data!, + options: .allowFragments) as! NSDictionary + let _a = _data["sessionStatus"] as! NSDictionary + let _b = _a["externalFieldTransfers"] as! NSArray + let _c = _b[0] as! NSDictionary + let _d = _c["putInfo"] as! NSDictionary + let upload = (_d["url"] as! NSString) as String + + self.channel?.base_request(path: upload, content_type: "application/octet-stream", data: data) { resp in + + // Sift through JSON for a response with the photo ID. + let _data2: NSDictionary = try! JSONSerialization.jsonObject(with: resp.data!, + options: .allowFragments) as! NSDictionary + let _a2 = _data2["sessionStatus"] as! NSDictionary + let _b2 = _a2["additionalInfo"] as! NSDictionary + let _c2 = _b2["uploader_service.GoogleRupioAdditionalInfo"] as! NSDictionary + let _d2 = _c2["completionInfo"] as! NSDictionary + let _e2 = _d2["customerSpecificInfo"] as! NSDictionary + let photoid = (_e2["photoid"] as! NSString) as String + + cb?(photoid) + } + } + } +} + /// Client API Operations public extension Client { @@ -354,7 +472,7 @@ public extension Client { // 40 => DESKTOP_ACTIVE // 30 => DESKTOP_IDLE // 1 => nil - (online ? 1 : 40) + (online ? 40 : 30) ], None, None, diff --git a/Hangouts/Client.swift b/Hangouts/Client.swift index cb6da54..8b0a5bf 100755 --- a/Hangouts/Client.swift +++ b/Hangouts/Client.swift @@ -157,8 +157,7 @@ public final class Client: Service { // before the API request finishes, we don't start extra requests. active_client_state = ActiveClientState.IsActive last_active_secs = Date().timeIntervalSince1970 as NSNumber? - - + // The first time this is called, we need to retrieve the user's email address. if self.email == nil { self.getSelfInfo { @@ -182,99 +181,64 @@ public final class Client: Service { } } } - - // Upload an image that can be later attached to a chat message. - // The name of the uploaded file may be changed by specifying the filename argument. - public func uploadImage(data: Data, filename: String, cb: ((String) -> Void)? = nil) { - let json = "{\"protocolVersion\":\"0.8\",\"createSessionRequest\":{\"fields\":[{\"external\":{\"name\":\"file\",\"filename\":\"\(filename)\",\"put\":{},\"size\":\(data.count)}}]}}" - - self.channel?.base_request(path: Client.IMAGE_UPLOAD_URL, - content_type: "application/x-www-form-urlencoded;charset=UTF-8", - data: json.data(using: String.Encoding.utf8)!) { response in - - // Sift through JSON for a response with the upload URL. - let _data: NSDictionary = try! JSONSerialization.jsonObject(with: response.data!, - options: .allowFragments) as! NSDictionary - let _a = _data["sessionStatus"] as! NSDictionary - let _b = _a["externalFieldTransfers"] as! NSArray - let _c = _b[0] as! NSDictionary - let _d = _c["putInfo"] as! NSDictionary - let upload = (_d["url"] as! NSString) as String - - self.channel?.base_request(path: upload, content_type: "application/octet-stream", data: data) { resp in - - // Sift through JSON for a response with the photo ID. - let _data2: NSDictionary = try! JSONSerialization.jsonObject(with: resp.data!, - options: .allowFragments) as! NSDictionary - let _a2 = _data2["sessionStatus"] as! NSDictionary - let _b2 = _a2["additionalInfo"] as! NSDictionary - let _c2 = _b2["uploader_service.GoogleRupioAdditionalInfo"] as! NSDictionary - let _d2 = _c2["completionInfo"] as! NSDictionary - let _e2 = _d2["customerSpecificInfo"] as! NSDictionary - let photoid = (_e2["photoid"] as! NSString) as String - - cb?(photoid) - } - } - } - - // Parse channel array and call the appropriate events. - public func channel(channel: Channel, didReceiveMessage message: [Any]) { - - // Add services to the channel. - // - // The services we add to the channel determine what kind of data we will - // receive on it. The "babel" service includes what we need for Hangouts. - // If this fails for some reason, hangups will never receive any events. - // This needs to be re-called whenever we open a new channel (when there's - // a new SID and client_id. - // - // Based on what Hangouts for Chrome does over 2 requests, this is - // trimmed down to 1 request that includes the bare minimum to make - // things work. - func addChannelServices(services: [String] = ["babel", "babel_presence_last_seen"]) { + + // Parse channel array and call the appropriate events. + public func channel(channel: Channel, didReceiveMessage message: [Any]) { + + // Add services to the channel. + // + // The services we add to the channel determine what kind of data we will + // receive on it. The "babel" service includes what we need for Hangouts. + // If this fails for some reason, hangups will never receive any events. + // This needs to be re-called whenever we open a new channel (when there's + // a new SID and client_id. + // + // Based on what Hangouts for Chrome does over 2 requests, this is + // trimmed down to 1 request that includes the bare minimum to make + // things work. + func addChannelServices(services: [String] = ["babel", "babel_presence_last_seen"]) { let mapped = services.map { ["3": ["1": ["1": $0]]] }.map { let dat = try! JSONSerialization.data(withJSONObject: $0, options: []) return NSString(data: dat, encoding: String.Encoding.utf8.rawValue)! as String - }.map { ["p": $0] } + }.map { ["p": $0] } self.channel?.sendMaps(mapped) - } - - guard message[0] as? String != "noop" else { - return - } - - // Wrapper appears to be a Protocol Buffer message, but encoded via - // field numbers as dictionary keys. Since we don't have a parser - // for that, parse it ad-hoc here. - let thr = (message[0] as! [String: String])["p"]! - let wrapper = try! thr.decodeJSON() - - // Once client_id is received, the channel is ready to have services added. - if let id = wrapper["3"] as? [String: Any] { - self.client_id = (id["2"] as! String) - addChannelServices() - } - if let cbu = wrapper["2"] as? [String: Any] { - let val2 = (cbu["2"]! as! String).data(using: String.Encoding.utf8) - let payload = try! JSONSerialization.jsonObject(with: val2!, options: .allowFragments) as! [AnyObject] - - // This is a (Client)BatchUpdate containing StateUpdate messages. - // payload[1] is a list of state updates. - if payload[0] as? String == "cbu" { - var b = BatchUpdate() as ProtoMessage - PBLiteSerialization.decode(message: &b, pblite: payload, ignoreFirstItem: true) - for state_update in (b as! BatchUpdate).stateUpdate { - self.active_client_state = state_update.stateUpdateHeader!.activeClientState! + } + + guard message[0] as? String != "noop" else { + return + } + + // Wrapper appears to be a Protocol Buffer message, but encoded via + // field numbers as dictionary keys. Since we don't have a parser + // for that, parse it ad-hoc here. + let thr = (message[0] as! [String: String])["p"]! + let wrapper = try! thr.decodeJSON() + + // Once client_id is received, the channel is ready to have services added. + if let id = wrapper["3"] as? [String: Any] { + self.client_id = (id["2"] as! String) + addChannelServices() + } + if let cbu = wrapper["2"] as? [String: Any] { + let val2 = (cbu["2"]! as! String).data(using: String.Encoding.utf8) + let payload = try! JSONSerialization.jsonObject(with: val2!, options: .allowFragments) as! [AnyObject] + + // This is a (Client)BatchUpdate containing StateUpdate messages. + // payload[1] is a list of state updates. + if payload[0] as? String == "cbu" { + var b = BatchUpdate() as ProtoMessage + PBLiteSerialization.decode(message: &b, pblite: payload, ignoreFirstItem: true) + for state_update in (b as! BatchUpdate).stateUpdate { + self.active_client_state = state_update.stateUpdateHeader!.activeClientState! self.lastUpdate = state_update.stateUpdateHeader!.currentServerTime! - hangoutsCenter.post( - name: Client.didUpdateStateNotification, object: self, - userInfo: [Client.didUpdateStateKey: state_update]) - } - } else { - log.warning("Ignoring message: \(payload[0])") - } - } - } + hangoutsCenter.post( + name: Client.didUpdateStateNotification, object: self, + userInfo: [Client.didUpdateStateKey: state_update]) + } + } else { + log.warning("Ignoring message: \(payload[0])") + } + } + } } diff --git a/Mocha/FoundationSupport.swift b/Mocha/FoundationSupport.swift index b2465a9..27a922e 100755 --- a/Mocha/FoundationSupport.swift +++ b/Mocha/FoundationSupport.swift @@ -226,15 +226,22 @@ public extension Date { } } - private static var formatter: DateFormatter = { + private static var fullFormatter: DateFormatter = { let formatter = DateFormatter() formatter.dateStyle = .full formatter.timeStyle = .long return formatter }() - public func fullString() -> String { - return Date.formatter.string(from: self) + private static var dateFormatter: DateFormatter = { + let formatter = DateFormatter() + formatter.dateStyle = .long + formatter.timeStyle = .none + return formatter + }() + + public func fullString(_ includeTime: Bool = true) -> String { + return (includeTime ? Date.fullFormatter : Date.dateFormatter).string(from: self) } public func nearestMinute() -> Date {