Pushr (master) :  summary log tree commit diff
path: root/Flickr.m blob: a95bdbd5a7e932abcb25c1f1a44305f7ae062f84
1/*
2 * Flickr.m
3 * --------
4 * Class containing methods to interact with the Flickr web services.
5 *
6 * Author: Chris Lee <clee@mg8.org>
7 * License: GPL v2 <http://www.opensource.org/licenses/gpl-license.php>
8 */
9
10#import <Foundation/Foundation.h>
11#import <UIKit/UIAlertSheet.h>
12#import "Flickr.h"
13#import "MobilePushr.h"
14#import "ExtendedAttributes.h"
15
16#include <unistd.h>
17
18@class NSXMLNode, NSXMLElement, NSXMLDocument;
19
20@implementation Flickr
21
22- (id)initWithPushr: (MobilePushr *)pushr
23{
24 if (![super init])
25 return nil;
26
27 _pushr = [pushr retain];
28 _settings = [[NSUserDefaults standardUserDefaults] retain];
29
30 return self;
31}
32
33- (void)dealloc
34{
35 [_pushr release];
36 [_settings release];
37 [super dealloc];
38}
39
40#pragma mark UIAlertSheet delegation
41- (void)alertSheet: (UIAlertSheet *)sheet buttonClicked: (int)button
42{
43 [sheet dismiss];
44
45 switch (button) {
46 case 1:
47 [_pushr openURL: [self authURL]];
48 break;
49 default:
50 [_pushr terminate];
51 break;
52 }
53}
54
55#pragma mark XML helper functions
56- (BOOL)sanityCheck: (id)responseDocument error: (NSError *)err
57{
58 NSXMLNode *rsp = [[responseDocument children] objectAtIndex: 0];
59 if (![[rsp name] isEqualToString: @"rsp"]) {
60 NSLog(@"This is not an <rsp> tag! Bailing out.");
61 return FALSE;
62 }
63
64 id element = [[NSClassFromString(@"NSXMLElement") alloc] initWithXMLString: [rsp XMLString] error: &err];
65 if (![[[element attributeForName: @"stat"] stringValue] isEqualToString: @"ok"]) {
66 NSLog(@"The status is not 'ok', and we have no error recovery.");
67 NSLog(@"XML: %@", [rsp XMLString]);
68 [element release];
69 return FALSE;
70 }
71
72 [element release];
73 return TRUE;
74}
75
76/*
77 * Returns an array of XMLNode objects with name matching nodeName. [ken] get is unusual
78 */
79- (NSArray *)getXMLNodesNamed: (NSString *)nodeName fromResponse: (NSData *)responseData
80{
81 NSError *err = nil;
82 id responseDoc = [[NSClassFromString(@"NSXMLDocument") alloc] initWithData: responseData options: 0 error: &err];
83 if (![self sanityCheck: responseDoc error: err]) {
84 NSLog(@"Flickr returned an error!");
85 [_pushr popupFailureAlertSheet];
86 [responseDoc release];
87 return nil;
88 }
89
90 NSMutableArray *matchingNodes = [NSMutableArray array];
91 NSArray *nodes = [responseDoc children];
92 NSEnumerator *chain = [nodes objectEnumerator];
93 NSXMLNode *node = nil;
94
95 while ((node = [chain nextObject])) {
96 if (![[node name] isEqualToString: nodeName]) {
97 nodes = [[nodes lastObject] children];
98 chain = [nodes objectEnumerator];
99 continue;
100 }
101
102 [matchingNodes addObject: node];
103 }
104
105 [responseDoc release];
106
107 return [NSArray arrayWithArray: matchingNodes];
108}
109
110/*
111 * Returns a dictionary filled with the node names, node values, node attribute names, and attribute values. [ken] get is unusual
112 */
113- (NSDictionary *)getXMLNodesAndAttributesFromResponse: (NSData *)responseData
114{
115 NSError *err = nil;
116 id responseDoc = [[NSClassFromString(@"NSXMLDocument") alloc] initWithData: responseData options: 0 error: &err];
117 if (![self sanityCheck: responseDoc error: err]) {
118 NSLog(@"Flickr returned an error!");
119 [_pushr popupFailureAlertSheet];
120 [responseDoc release];
121 return nil;
122 }
123
124 NSMutableDictionary *nodesWithAttributes = [NSMutableDictionary dictionary];
125 NSArray *nodes = [responseDoc children];
126 NSEnumerator *chain = [nodes objectEnumerator];
127 NSXMLNode *node = nil;
128
129 while ((node = [chain nextObject])) {
130 id element = [[NSClassFromString(@"NSXMLElement") alloc] initWithXMLString: [node XMLString] error: &err];
131 if ([[element attributes] count] > 0) {
132 NSEnumerator *attributeChain = [[element attributes] objectEnumerator];
133 id attribute = nil;
134 while ((attribute = [attributeChain nextObject]))
135 [nodesWithAttributes setObject: [attribute stringValue] forKey: [NSString stringWithFormat: @"%@%@", [node name], [attribute name]]];
136 }
137
138 [nodesWithAttributes setObject: [node stringValue] forKey: [node name]];
139
140 if ([[node children] count] > 0 && [[[[node children] objectAtIndex: 0] name] length] > 0) {
141 nodes = [node children];
142 chain = [nodes objectEnumerator];
143 }
144
145 [element release];
146 }
147
148 [responseDoc release];
149
150 return [NSDictionary dictionaryWithDictionary: nodesWithAttributes]; // [ken] we'd usually just return the mutable dict
151}
152
153#pragma mark internal functions
154/*
155 * Returns a URL with the parameters and values properly appended, including the call signing that Flickr requires from our app.
156 *
157 * This method made possible by extending system classes (without having to inherit from them.) Hooray!
158 */
159- (NSURL *)signedURL: (NSDictionary *)parameters withBase: (NSString *)base
160{
161 NSMutableString *url = [NSMutableString stringWithFormat: @"%@?", base];
162 NSMutableString *sig = [NSMutableString stringWithString: PUSHR_SHARED_SECRET];
163
164 [sig appendString: [[parameters pairsJoinedByString: @""] componentsJoinedByString: @""]];
165 [url appendString: [[parameters pairsJoinedByString: @"="] componentsJoinedByString: @"&"]];
166 [url appendString: [NSString stringWithFormat: @"&api_sig=%@", [sig md5HexHash]]];
167
168 return [NSURL URLWithString: url];
169}
170
171/*
172 * By default, we want the FLICKR_REST_URL as the base for our calls.
173 */
174- (NSURL *)signedURL: (NSDictionary *)parameters
175{
176 return [self signedURL: parameters withBase: FLICKR_REST_URL];
177}
178
179/*
180 * Returns a one-time-use authorization URL; this URL is a page where the user can tell Flickr to give us permission to upload pictures to their account.
181 */
182- (NSURL *)authURL
183{
184 NSArray *keys = [NSArray arrayWithObjects: @"api_key", @"perms", @"frob", nil];
185 NSArray *vals = [NSArray arrayWithObjects: PUSHR_API_KEY, FLICKR_WRITE_PERMS, [self frob], nil];
186 NSDictionary *params = [NSDictionary dictionaryWithObjects: vals forKeys: keys];
187
188 return [self signedURL: params withBase: FLICKR_AUTH_URL];
189}
190
191/*
192 * Get a frob from Flickr, to put in the URL that we send the user to to get their permission to upload pics.
193 */
194- (NSString *)frob
195{
196 NSArray *keys = [NSArray arrayWithObjects: @"api_key", @"method", nil];
197 NSArray *vals = [NSArray arrayWithObjects: PUSHR_API_KEY, FLICKR_GET_FROB, nil];
198 NSDictionary *params = [NSDictionary dictionaryWithObjects: vals forKeys: keys];
199
200 NSURL *url = [self signedURL: params];
201 NSData *responseData = [NSData dataWithContentsOfURL: url];
202
203 NSString *_frob = [[[self getXMLNodesNamed: @"frob" fromResponse: responseData] lastObject] stringValue];
204
205 [_settings setObject: _frob forKey: @"frob"];
206 [_settings synchronize];
207
208 return [NSString stringWithString: _frob];
209}
210
211#pragma mark externally-visible interface
212/*
213 * Get the tags the user has already set on their photos.
214 * TODO: At some point, we should offer a UI to let them tag their future photos with the same tags.
215 */
216- (NSArray *)tags
217{
218 NSArray *keys = [NSArray arrayWithObjects: @"api_key", @"method", @"user_id", nil];
219 NSArray *vals = [NSArray arrayWithObjects: PUSHR_API_KEY, FLICKR_GET_TAGS, [_settings stringForKey: @"nsid"], nil];
220 NSDictionary *params = [NSDictionary dictionaryWithObjects: vals forKeys: keys];
221
222 NSURL *url = [self signedURL: params];
223 NSData *responseData = [NSData dataWithContentsOfURL: url];
224 NSMutableArray *_tags = [NSMutableArray array];
225
226 NSEnumerator *iterator = [[self getXMLNodesNamed: @"tag" fromResponse: responseData] objectEnumerator];
227 id currentTagNode = nil;
228 while ((currentTagNode = [iterator nextObject]))
229 [_tags addObject: [currentTagNode stringValue]];
230
231 return [NSArray arrayWithArray: _tags];
232}
233
234/*
235 * Pop up a dialog so the user can tell Flickr it's cool for us to push pictures to their account.
236 */
237- (void)sendToGrantPermission
238{
239 UIAlertSheet *alertSheet = [[[UIAlertSheet alloc] initWithFrame: CGRectMake(0.0f, 0.0f, 320.0f, 240.0f)] autorelease];
240 [alertSheet setTitle: @"Can't upload to Flickr"];
241 [alertSheet setBodyText: @"Pushr needs your permission to upload pictures to Flickr."];
242 [alertSheet addButtonWithTitle: @"Proceed"];
243 [alertSheet addButtonWithTitle: @"Cancel"];
244 [alertSheet setDelegate: self];
245 [alertSheet setRunsModal: YES];
246 [alertSheet popupAlertAnimated: YES];
247 [_settings setBool: TRUE forKey: @"sentToGetToken"];
248}
249
250/*
251 * We have a frob that Flickr generated, and we used it in the URL we sent the user to (so that they could give us permission to upload pictures to their account). Now, we assume the user clicked on the 'Okay!' button the page we sent them to go click, and our frob can now be traded for a token.
252 */
253- (void)tradeFrobForToken
254{
255 NSArray *keys = [NSArray arrayWithObjects: @"api_key", @"method", @"frob", nil];
256 NSArray *vals = [NSArray arrayWithObjects: PUSHR_API_KEY, FLICKR_GET_TOKEN, [_settings stringForKey: @"frob"], nil];
257 NSDictionary *params = [NSDictionary dictionaryWithObjects: vals forKeys: keys];
258
259 NSData *responseData = [NSData dataWithContentsOfURL: [self signedURL: params]];
260
261 NSDictionary *tokenDictionary = [self getXMLNodesAndAttributesFromResponse: responseData];
262 NSArray *responseKeys = [tokenDictionary allKeys];
263 if (!([responseKeys containsObject: @"token"] && [responseKeys containsObject: @"usernsid"] && [responseKeys containsObject: @"userusername"])) {
264 NSLog(@"Flickr returned an error!");
265 [_settings removeObjectForKey: @"frob"];
266 [_settings synchronize];
267 [self sendToGrantPermission];
268 return;
269 }
270
271 [_settings setObject: [tokenDictionary objectForKey: @"token"] forKey: @"token"];
272 [_settings setObject: [tokenDictionary objectForKey: @"usernsid"] forKey: @"nsid"];
273 [_settings setObject: [tokenDictionary objectForKey: @"userusername"] forKey: @"username"];
274 [_settings removeObjectForKey: @"frob"];
275 [_settings synchronize];
276}
277
278/*
279 * We have a token, but is it valid? Maybe the user decided to de-authorize us and we can't push photos to their account anymore. This is how we make sure our token is valid.
280 */
281- (void)checkToken
282{
283 NSArray *keys = [NSArray arrayWithObjects: @"api_key", @"auth_token", @"method", nil];
284 NSArray *vals = [NSArray arrayWithObjects: PUSHR_API_KEY, [_settings stringForKey: @"token"], FLICKR_CHECK_TOKEN, nil];
285 NSDictionary *params = [NSDictionary dictionaryWithObjects: vals forKeys: keys];
286 NSData *responseData = [NSData dataWithContentsOfURL: [self signedURL: params]];
287 NSDictionary *tokenDictionary = [self getXMLNodesAndAttributesFromResponse: responseData];
288 NSArray *responseKeys = [tokenDictionary allKeys];
289 if (!([responseKeys containsObject: @"token"] && [responseKeys containsObject: @"usernsid"] && [responseKeys containsObject: @"userusername"])) {
290 NSLog(@"Failed the sanity check when verifying our token. Bailing!");
291 [_settings setBool: FALSE forKey: @"sentToGetToken"];
292 [self sendToGrantPermission];
293 return;
294 }
295
296 NSLog(@"Well, our token seems good.");
297}
298
299/*
300 * Takes a JPG file at the specified filesystem path, and uploads it to Flickr using CFNetwork, because there is no way of getting the number of bytes written from an HTTP POST request using the NSHTTP API.
301 *
302 * This is, without a doubt, the ugliest code in the entire application.
303 */
304- (NSString *)pushPhoto: (NSString *)pathToJPG
305{
306 NSString *token = [_settings stringForKey: @"token"];
307 NSMutableDictionary *params = [NSMutableDictionary dictionaryWithObjectsAndKeys: PUSHR_API_KEY, @"api_key", token, @"auth_token", nil];
308 if ([[ExtendedAttributes allKeysAtPath: pathToJPG] containsObject: NAME_ATTRIBUTE])
309 [params setObject: [ExtendedAttributes stringForKey: NAME_ATTRIBUTE atPath: pathToJPG] forKey: @"title"];
310 if ([[ExtendedAttributes allKeysAtPath: pathToJPG] containsObject: DESCRIPTION_ATTRIBUTE])
311 [params setObject: [ExtendedAttributes stringForKey: DESCRIPTION_ATTRIBUTE atPath: pathToJPG] forKey: @"description"];
312 if ([[_settings arrayForKey: @"defaultTags"] count] > 0)
313 [params setObject: [[_settings arrayForKey: @"defaultTags"] componentsJoinedByString: @" "] forKey: @"tags"];
314 if ([[ExtendedAttributes allKeysAtPath: pathToJPG] containsObject: TAGS_ATTRIBUTE])
315 [params setObject: [[ExtendedAttributes objectForKey: TAGS_ATTRIBUTE atPath: pathToJPG] componentsJoinedByString: @" "] forKey: @"tags"];
316 if ([[_settings arrayForKey: @"defaultPrivacy"] count] > 0 || [[ExtendedAttributes allKeysAtPath: pathToJPG] containsObject: PRIVACY_ATTRIBUTE]) {
317 NSArray *privacy = [_settings arrayForKey: @"defaultPrivacy"];
318 if ([[ExtendedAttributes allKeysAtPath: pathToJPG] containsObject: PRIVACY_ATTRIBUTE])
319 privacy = [ExtendedAttributes objectForKey: PRIVACY_ATTRIBUTE atPath: pathToJPG];
320
321 if ([privacy containsObject: @"Public"]) {
322 [params setObject: @"1" forKey: @"is_public"];
323 } else {
324 [params setObject: @"0" forKey: @"is_public"];
325 if ([privacy containsObject: @"Friends"])
326 [params setObject: @"1" forKey: @"is_friend"];
327 if ([privacy containsObject: @"Family"])
328 [params setObject: @"1" forKey: @"is_family"];
329 }
330 }
331 NSArray *pairs = [params pairsJoinedByString: @""];
332 NSString *api_sig = [NSString stringWithFormat: @"%@%@", PUSHR_SHARED_SECRET, [pairs componentsJoinedByString: @""]];
333 [params setObject: [api_sig md5HexHash] forKey: @"api_sig"];
334 NSData *jpgData = [NSData dataWithContentsOfFile: pathToJPG];
335 [params setObject: jpgData forKey: @"photo"];
336
337 NSMutableData *body = [[NSMutableData alloc] initWithLength: 0];
338 [body appendData: [[[[NSString alloc] initWithFormat: @"--%@\r\n", @MIME_BOUNDARY] autorelease] dataUsingEncoding: NSUTF8StringEncoding]];
339
340 NSEnumerator *enumerator = [params keyEnumerator];
341 id key = nil;
342 while ((key = [enumerator nextObject])) {
343 id val = [params objectForKey: key];
344 id keyHeader = nil;
345 if ([key isEqualToString: @"photo"]) {
346 // If this is the photo...
347 keyHeader = [[NSString stringWithFormat: @"Content-Disposition: form-data; name=\"photo\"; filename=\"%@\"\r\nContent-Type: image/jpeg\r\n\r\n", pathToJPG] dataUsingEncoding: NSUTF8StringEncoding];
348 [body appendData: keyHeader];
349 [body appendData: val];
350 } else {
351 // Treat all other values as strings.
352 keyHeader = [NSString stringWithFormat: @"Content-Disposition: form-data; name=\"%@\"\r\n\r\n", key];
353 [body appendData: [keyHeader dataUsingEncoding: NSUTF8StringEncoding]];
354 [body appendData: [val dataUsingEncoding: NSUTF8StringEncoding]];
355 }
356 [body appendData: [[NSString stringWithFormat: @"\r\n--%@\r\n", @MIME_BOUNDARY] dataUsingEncoding: NSUTF8StringEncoding]];
357 }
358
359 [body appendData: [[NSString stringWithString: @"--\r\n"] dataUsingEncoding: NSUTF8StringEncoding]];
360
361 CFURLRef _uploadURL = CFURLCreateWithString(kCFAllocatorDefault, (CFStringRef)FLICKR_UPLOAD_URL, NULL);
362 CFHTTPMessageRef _request = CFHTTPMessageCreateRequest(kCFAllocatorDefault, CFSTR("POST"), _uploadURL, kCFHTTPVersion1_1);
363 CFRelease(_uploadURL);
364 _uploadURL = NULL;
365
366 CFHTTPMessageSetHeaderFieldValue(_request, CFSTR("Content-Type"), CFSTR(CONTENT_TYPE));
367 CFHTTPMessageSetHeaderFieldValue(_request, CFSTR("Host"), CFSTR("api.flickr.com"));
368 CFHTTPMessageSetHeaderFieldValue(_request, CFSTR("Content-Length"), (CFStringRef)[NSString stringWithFormat: @"%d", [body length]]);
369 CFHTTPMessageSetBody(_request, (CFDataRef)body);
370
371 CFReadStreamRef _readStream = CFReadStreamCreateForHTTPRequest(kCFAllocatorDefault, _request);
372 CFReadStreamOpen(_readStream);
373
374 NSMutableString *responseString = [NSMutableString string];
375 CFIndex numBytesRead;
376 long bytesWritten, previousBytesWritten = 0;
377 UInt8 buf[1024];
378 BOOL doneUploading = NO;
379
380 while (!doneUploading) {
381 CFNumberRef cfSize = CFReadStreamCopyProperty(_readStream, kCFStreamPropertyHTTPRequestBytesWrittenCount);
382 CFNumberGetValue(cfSize, kCFNumberLongType, &bytesWritten);
383 CFRelease(cfSize);
384 cfSize = NULL;
385
386 if (bytesWritten > previousBytesWritten) {
387 previousBytesWritten = bytesWritten;
388 NSNumber *progress = [NSNumber numberWithFloat: ((float)bytesWritten / (float)[body length])];
389 [_pushr performSelectorOnMainThread: @selector(updateProgress:) withObject: progress waitUntilDone: YES];
390 }
391
392 if (!CFReadStreamHasBytesAvailable(_readStream)) {
393 usleep(3600);
394 continue;
395 }
396
397 numBytesRead = CFReadStreamRead(_readStream, buf, 1024);
398 if (numBytesRead < 1024)
399 buf[numBytesRead] = 0;
400 [responseString appendFormat: @"%s", buf];
401
402 if (CFReadStreamGetStatus(_readStream) == kCFStreamStatusAtEnd) doneUploading = YES;
403 }
404 [body release];
405
406 CFReadStreamClose(_readStream);
407 CFRelease(_request);
408 _request = NULL;
409 CFRelease(_readStream);
410 _readStream = NULL;
411
412 return [NSString stringWithString: responseString];
413}
414
415/*
416 * When the user clicks on the 'Push to Flickr' button, push the photos that haven't been pushed yet, and pass the XML for the responses back to the main class when finished.
417 */
418- (void)triggerUpload: (id)photos
419{
420 NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
421 NSMutableArray *responses = [NSMutableArray array];
422
423 NSEnumerator *enumerator = [photos objectEnumerator];
424 id photo = nil;
425 while ((photo = [enumerator nextObject])) {
426 [_pushr performSelectorOnMainThread: @selector(startingToPush:) withObject: photo waitUntilDone: NO];
427 [responses addObject: [self pushPhoto: photo]];
428 [ExtendedAttributes setString: @"true" forKey: PUSHED_ATTRIBUTE atPath: photo];
429 [_pushr performSelectorOnMainThread: @selector(donePushing:) withObject: photo waitUntilDone: NO];
430 }
431
432 [_pushr performSelectorOnMainThread: @selector(allDone:) withObject: responses waitUntilDone: YES];
433 [pool release];
434}
435
436@end
437