Skip to content

Commit

Permalink
Better Google Photos image/video upload.
Browse files Browse the repository at this point in the history
  • Loading branch information
Aditya Vaidyam committed Jun 19, 2017
1 parent b595fbe commit d63bf9d
Show file tree
Hide file tree
Showing 3 changed files with 184 additions and 95 deletions.
120 changes: 119 additions & 1 deletion Hangouts/API.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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": "<FILENAME>",
"put": {},
"size": <FILESIZE>
}
},
{
"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": "<TIMESTAMP (USEC)>",
"contentType": "text/plain"
}
},
{
"inlined": {
"name": "batchid",
"content": "<TIMESTAMP (USEC)>",
"contentType": "text/plain"
}
},
{
"inlined": {
"name": "album_name",
"content": "<MONTH DAY, YEAR>",
"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 {

Expand Down Expand Up @@ -354,7 +472,7 @@ public extension Client {
// 40 => DESKTOP_ACTIVE
// 30 => DESKTOP_IDLE
// 1 => nil
(online ? 1 : 40)
(online ? 40 : 30)
],
None,
None,
Expand Down
146 changes: 55 additions & 91 deletions Hangouts/Client.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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])")
}
}
}
}
13 changes: 10 additions & 3 deletions Mocha/FoundationSupport.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down

0 comments on commit d63bf9d

Please sign in to comment.