Plugins/Twitter Plugin/MGTwitterEngine/MGTwitterEngine.m
author Zachary West <zacw@adium.im>
Thu Nov 19 21:12:23 2009 -0500 (2009-11-19)
changeset 2768 85857106a45e
parent 2501 6a7ff9587023
child 2774 6bcb9802d1cf
permissions -rw-r--r--
Implement the Retweet API. This means checking home_timeline and sending proper retweet messages. Fixes #12556.

On an annoying note, home_timeline (despite saying "Returns the 20 most recent statuses, including retweets, posted by the authenticating user and that user's friends") does not include outgoing retweets. This will either be fixed by Twitter quickly, or not. I'm tired of Twitter's inconsistent and buggy API. So, as such, there's currently no way to remove a retweet done by yourself.
     1 //
     2 //  MGTwitterEngine.m
     3 //  MGTwitterEngine
     4 //
     5 //  Created by Matt Gemmell on 10/02/2008.
     6 //  Copyright 2008 Instinctive Code.
     7 //
     8 
     9 #import "MGTwitterEngine.h"
    10 #import "MGTwitterHTTPURLConnection.h"
    11 
    12 #import "NSData+Base64.h"
    13 
    14 #define USE_LIBXML 0
    15 
    16 #import "MGTwitterStatusesParser.h"
    17 #import "MGTwitterUsersParser.h"
    18 #import "MGTwitterMessagesParser.h"
    19 #import "MGTwitterMiscParser.h"
    20 
    21 #define TWITTER_DOMAIN          @"twitter.com"
    22 #define HTTP_POST_METHOD        @"POST"
    23 #define HTTP_MULTIPART_METHOD	@"MULTIPART" //adium
    24 #define MULTIPART_FORM_BOUNDARY	@"bf5faadd239c17e35f91e6dafe1d2f96" //adium
    25 #define MAX_MESSAGE_LENGTH      140 // Twitter recommends tweets of max 140 chars
    26 #define MAX_LOCATION_LENGTH		31
    27 
    28 #define DEFAULT_CLIENT_NAME     @"MGTwitterEngine"
    29 #define DEFAULT_CLIENT_VERSION  @"1.0"
    30 #define DEFAULT_CLIENT_URL      @"http://mattgemmell.com/source"
    31 #define DEFAULT_CLIENT_TOKEN	@"mgtwitterengine"
    32 
    33 #define URL_REQUEST_TIMEOUT     50.0 // Twitter usually fails quickly if it's going to fail at all.
    34 #define DEFAULT_TWEET_COUNT		20
    35 
    36 
    37 @interface MGTwitterEngine (PrivateMethods)
    38 
    39 // Utility methods
    40 - (NSDateFormatter *)_HTTPDateFormatter;
    41 - (NSString *)_queryStringWithBase:(NSString *)base parameters:(NSDictionary *)params prefixed:(BOOL)prefixed;
    42 - (NSDate *)_HTTPToDate:(NSString *)httpDate;
    43 - (NSString *)_dateToHTTP:(NSDate *)date;
    44 - (NSString *)_encodeString:(NSString *)string;
    45 
    46 // Connection/Request methods
    47 - (NSString *)_sendRequestWithMethod:(NSString *)method 
    48                                 path:(NSString *)path 
    49                      queryParameters:(NSDictionary *)params
    50                                 body:(id)body 
    51                          requestType:(MGTwitterRequestType)requestType 
    52                         responseType:(MGTwitterResponseType)responseType;
    53 
    54 // Parsing methods
    55 - (void)_parseXMLForConnection:(MGTwitterHTTPURLConnection *)connection;
    56 
    57 // Delegate methods
    58 - (BOOL) _isValidDelegateForSelector:(SEL)selector;
    59 
    60 @end
    61 
    62 
    63 @implementation MGTwitterEngine
    64 
    65 
    66 #pragma mark Constructors
    67 
    68 
    69 + (MGTwitterEngine *)twitterEngineWithDelegate:(NSObject *)theDelegate
    70 {
    71     return [[[MGTwitterEngine alloc] initWithDelegate:theDelegate] autorelease];
    72 }
    73 
    74 
    75 - (MGTwitterEngine *)initWithDelegate:(NSObject *)newDelegate
    76 {
    77     if ((self = [super init])) {
    78         _delegate = newDelegate; // deliberately weak reference
    79         _connections = [[NSMutableDictionary alloc] initWithCapacity:0];
    80         _clientName = [DEFAULT_CLIENT_NAME retain];
    81         _clientVersion = [DEFAULT_CLIENT_VERSION retain];
    82         _clientURL = [DEFAULT_CLIENT_URL retain];
    83 		_clientSourceToken = [DEFAULT_CLIENT_TOKEN retain];
    84 		_APIDomain = [TWITTER_DOMAIN retain];
    85         _secureConnection = YES;
    86 		_clearsCookies = NO;
    87     }
    88     
    89     return self;
    90 }
    91 
    92 
    93 - (void)dealloc
    94 {
    95     _delegate = nil;
    96     
    97     [[_connections allValues] makeObjectsPerformSelector:@selector(cancel)];
    98     [_connections release];
    99     
   100 	[_accessToken release];
   101 	[_consumer release];
   102 	
   103     [_username release];
   104     [_password release];
   105     [_clientName release];
   106     [_clientVersion release];
   107     [_clientURL release];
   108     [_clientSourceToken release];
   109 	[_APIDomain release];
   110     
   111     [super dealloc];
   112 }
   113 
   114 
   115 #pragma mark Configuration and Accessors
   116 
   117 
   118 + (NSString *)version
   119 {
   120     // 1.0.0 = 22 Feb 2008
   121     // 1.0.1 = 26 Feb 2008
   122     // 1.0.2 = 04 Mar 2008
   123     // 1.0.3 = 04 Mar 2008
   124 	// 1.0.4 = 11 Apr 2008
   125 	// 1.0.5 = 06 Jun 2008
   126 	// 1.0.6 = 05 Aug 2008
   127 	// 1.0.7 = 28 Sep 2008
   128 	// 1.0.8 = 01 Oct 2008
   129     return @"1.0.8";
   130 }
   131 
   132 
   133 - (NSString *)username
   134 {
   135     return [[_username retain] autorelease];
   136 }
   137 
   138 
   139 - (NSString *)password
   140 {
   141     return [[_password retain] autorelease];
   142 }
   143 
   144 
   145 - (void)setUsername:(NSString *)newUsername password:(NSString *)newPassword
   146 {
   147     // Set new credentials.
   148     [_username release];
   149     _username = [newUsername retain];
   150     [_password release];
   151     _password = [newPassword retain];
   152     
   153 	if ([self clearsCookies]) {
   154 		// Remove all cookies for twitter, to ensure next connection uses new credentials.
   155 		NSString *urlString = [NSString stringWithFormat:@"%@://%@", 
   156 							   (_secureConnection) ? @"https" : @"http", 
   157 							   _APIDomain];
   158 		NSURL *url = [NSURL URLWithString:urlString];
   159 		
   160 		NSHTTPCookieStorage *cookieStorage = [NSHTTPCookieStorage sharedHTTPCookieStorage];
   161 		NSEnumerator *enumerator = [[cookieStorage cookiesForURL:url] objectEnumerator];
   162 		NSHTTPCookie *cookie = nil;
   163 		while ((cookie = [enumerator nextObject])) {
   164 			[cookieStorage deleteCookie:cookie];
   165 		}
   166 	}
   167 }
   168 
   169 
   170 - (NSString *)clientName
   171 {
   172     return [[_clientName retain] autorelease];
   173 }
   174 
   175 
   176 - (NSString *)clientVersion
   177 {
   178     return [[_clientVersion retain] autorelease];
   179 }
   180 
   181 
   182 - (NSString *)clientURL
   183 {
   184     return [[_clientURL retain] autorelease];
   185 }
   186 
   187 
   188 - (NSString *)clientSourceToken
   189 {
   190     return [[_clientSourceToken retain] autorelease];
   191 }
   192 
   193 
   194 - (void)setClientName:(NSString *)name version:(NSString *)version URL:(NSString *)url token:(NSString *)token;
   195 {
   196     [_clientName release];
   197     _clientName = [name retain];
   198     [_clientVersion release];
   199     _clientVersion = [version retain];
   200     [_clientURL release];
   201     _clientURL = [url retain];
   202     [_clientSourceToken release];
   203     _clientSourceToken = [token retain];
   204 }
   205 
   206 
   207 - (NSString *)APIDomain
   208 {
   209 	return [[_APIDomain retain] autorelease];
   210 }
   211 
   212 
   213 - (void)setAPIDomain:(NSString *)domain
   214 {
   215 	[_APIDomain release];
   216 	if (!domain || [domain length] == 0) {
   217 		_APIDomain = [TWITTER_DOMAIN retain];
   218 	} else {
   219 		_APIDomain = [domain retain];
   220 	}
   221 }
   222 
   223 
   224 - (BOOL)usesSecureConnection
   225 {
   226     return _secureConnection;
   227 }
   228 
   229 
   230 - (void)setUsesSecureConnection:(BOOL)flag
   231 {
   232     _secureConnection = flag;
   233 }
   234 
   235 
   236 - (BOOL)clearsCookies
   237 {
   238 	return _clearsCookies;
   239 }
   240 
   241 
   242 - (void)setClearsCookies:(BOOL)flag
   243 {
   244 	_clearsCookies = flag;
   245 }
   246 
   247 
   248 #pragma mark Connection methods
   249 
   250 
   251 - (int)numberOfConnections
   252 {
   253     return [_connections count];
   254 }
   255 
   256 
   257 - (NSArray *)connectionIdentifiers
   258 {
   259     return [_connections allKeys];
   260 }
   261 
   262 
   263 - (void)closeConnection:(NSString *)identifier
   264 {
   265     MGTwitterHTTPURLConnection *connection = [_connections objectForKey:identifier];
   266     if (connection) {
   267         [connection cancel];
   268         [_connections removeObjectForKey:identifier];
   269     }
   270 }
   271 
   272 
   273 - (void)closeAllConnections
   274 {
   275     [[_connections allValues] makeObjectsPerformSelector:@selector(cancel)];
   276     [_connections removeAllObjects];
   277 }
   278 
   279 
   280 #pragma mark Utility methods
   281 
   282 
   283 - (NSDateFormatter *)_HTTPDateFormatter
   284 {
   285     // Returns a formatter for dates in HTTP format (i.e. RFC 822, updated by RFC 1123).
   286     // e.g. "Sun, 06 Nov 1994 08:49:37 GMT"
   287 	NSDateFormatter *dateFormatter = [[[NSDateFormatter alloc] init] autorelease];
   288 	//[dateFormatter setDateFormat:@"%a, %d %b %Y %H:%M:%S GMT"]; // won't work with -init, which uses new (unicode) format behaviour.
   289 	[dateFormatter setFormatterBehavior:NSDateFormatterBehavior10_4];
   290 	[dateFormatter setDateFormat:@"EEE, dd MMM yyyy HH:mm:ss GMT"];
   291 	return dateFormatter;
   292 }
   293 
   294 
   295 - (NSString *)_queryStringWithBase:(NSString *)base parameters:(NSDictionary *)params prefixed:(BOOL)prefixed
   296 {
   297     // Append base if specified.
   298     NSMutableString *str = [NSMutableString stringWithCapacity:0];
   299     if (base) {
   300         [str appendString:base];
   301     }
   302     
   303     // Append each name-value pair.
   304     if (params) {
   305         int i;
   306         NSArray *names = [params allKeys];
   307         for (i = 0; i < [names count]; i++) {
   308             if (i == 0 && prefixed) {
   309                 [str appendString:@"?"];
   310             } else if (i > 0) {
   311                 [str appendString:@"&"];
   312             }
   313             NSString *name = [names objectAtIndex:i];
   314             [str appendString:[NSString stringWithFormat:@"%@=%@", 
   315              name, [self _encodeString:[params objectForKey:name]]]];
   316         }
   317     }
   318     
   319     return str;
   320 }
   321 
   322 
   323 - (NSDate *)_HTTPToDate:(NSString *)httpDate
   324 {
   325     NSDateFormatter *dateFormatter = [self _HTTPDateFormatter];
   326     return [dateFormatter dateFromString:httpDate];
   327 }
   328 
   329 
   330 - (NSString *)_dateToHTTP:(NSDate *)date
   331 {
   332     NSDateFormatter *dateFormatter = [self _HTTPDateFormatter];
   333     return [dateFormatter stringFromDate:date];
   334 }
   335 
   336 
   337 - (NSString *)_encodeString:(NSString *)string
   338 {
   339     NSString *result = (NSString *)CFURLCreateStringByAddingPercentEscapes(NULL, 
   340                                                                  (CFStringRef)string, 
   341                                                                  NULL, 
   342                                                                  (CFStringRef)@";/?:@&=$+{}<>,",
   343                                                                  kCFStringEncodingUTF8);
   344     return [result autorelease];
   345 }
   346 
   347 
   348 - (NSString *)getImageAtURL:(NSString *)urlString
   349 {
   350     // This is a method implemented for the convenience of the client, 
   351     // allowing asynchronous downloading of users' Twitter profile images.
   352 	NSString *encodedUrlString = [urlString stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
   353     NSURL *url = [NSURL URLWithString:encodedUrlString];
   354     if (!url) {
   355         return nil;
   356     }
   357     
   358     // Construct an NSMutableURLRequest for the URL and set appropriate request method.
   359     NSMutableURLRequest *theRequest = [NSMutableURLRequest requestWithURL:url 
   360                                                               cachePolicy:NSURLRequestReloadIgnoringCacheData 
   361                                                           timeoutInterval:URL_REQUEST_TIMEOUT];
   362     
   363     // Create a connection using this request, with the default timeout and caching policy, 
   364     // and appropriate Twitter request and response types for parsing and error reporting.
   365     MGTwitterHTTPURLConnection *connection;
   366     connection = [[MGTwitterHTTPURLConnection alloc] initWithRequest:theRequest 
   367                                                             delegate:self 
   368                                                          requestType:MGTwitterImageRequest 
   369                                                         responseType:MGTwitterImage];
   370     
   371     if (!connection) {
   372         return nil;
   373     } else {
   374         [_connections setObject:connection forKey:[connection identifier]];
   375         [connection release];
   376     }
   377     
   378     return [connection identifier];
   379 }
   380 
   381 
   382 #pragma mark Request sending methods
   383 
   384 #define SET_AUTHORIZATION_IN_HEADER 1
   385 
   386 /* See Adium Additions/Changes below—oauth support */
   387 #if 0
   388 - (NSString *)_sendRequestWithMethod:(NSString *)method 
   389                                 path:(NSString *)path 
   390                      queryParameters:(NSDictionary *)params 
   391                                 body:(id)body 
   392                          requestType:(MGTwitterRequestType)requestType 
   393                         responseType:(MGTwitterResponseType)responseType
   394 {
   395     // Construct appropriate URL string.
   396     NSString *fullPath = path;
   397     if (params) {
   398         fullPath = [self _queryStringWithBase:fullPath parameters:params prefixed:YES];
   399     }
   400 	
   401 #if SET_AUTHORIZATION_IN_HEADER
   402     NSString *urlString = [NSString stringWithFormat:@"%@://%@/%@", 
   403                            (_secureConnection) ? @"https" : @"http",
   404                            _APIDomain, fullPath];
   405 #else    
   406     NSString *urlString = [NSString stringWithFormat:@"%@://%@:%@@%@/%@", 
   407                            (_secureConnection) ? @"https" : @"http", 
   408                            [self _encodeString:_username], [self _encodeString:_password], 
   409                            _APIDomain, fullPath];
   410 #endif
   411     
   412     NSURL *finalURL = [NSURL URLWithString:urlString];
   413     if (!finalURL) {
   414         return nil;
   415     }
   416     
   417     // Construct an NSMutableURLRequest for the URL and set appropriate request method.
   418     NSMutableURLRequest *theRequest = [NSMutableURLRequest requestWithURL:finalURL 
   419                                                               cachePolicy:NSURLRequestReloadIgnoringCacheData 
   420                                                           timeoutInterval:URL_REQUEST_TIMEOUT];
   421 	if(method && [method isEqualToString:HTTP_MULTIPART_METHOD]) {
   422 		method = HTTP_POST_METHOD;
   423 		[theRequest setValue:[NSString stringWithFormat:@"multipart/form-data; boundary=%@", MULTIPART_FORM_BOUNDARY] forHTTPHeaderField:@"Content-type"];
   424 	}
   425 	
   426     if (method) {
   427         [theRequest setHTTPMethod:method];
   428     }
   429 
   430     [theRequest setHTTPShouldHandleCookies:NO];
   431 	
   432     // Set headers for client information, for tracking purposes at Twitter.
   433     [theRequest setValue:_clientName    forHTTPHeaderField:@"X-Twitter-Client"];
   434     [theRequest setValue:_clientVersion forHTTPHeaderField:@"X-Twitter-Client-Version"];
   435     [theRequest setValue:_clientURL     forHTTPHeaderField:@"X-Twitter-Client-URL"];
   436     
   437 #if SET_AUTHORIZATION_IN_HEADER
   438 	if ([self username] && [self password]) {
   439 		// Set header for HTTP Basic authentication explicitly, to avoid problems with proxies and other intermediaries
   440 		NSString *authStr = [NSString stringWithFormat:@"%@:%@", [self username], [self password]];
   441 		NSData *authData = [authStr dataUsingEncoding:NSASCIIStringEncoding];
   442 		NSString *authValue = [NSString stringWithFormat:@"Basic %@", [authData base64EncodingWithLineLength:80]];
   443 		[theRequest setValue:authValue forHTTPHeaderField:@"Authorization"];
   444 	}
   445 #endif
   446 
   447     // Set the request body if this is a POST request.
   448     BOOL isPOST = (method && [method isEqualToString:HTTP_POST_METHOD]);
   449 	
   450     if (isPOST) {
   451         // Set request body, if specified (hopefully so), with 'source' parameter if appropriate.
   452 		if([body isKindOfClass:[NSString class]]) {
   453 			NSString *finalBody = @"";
   454 			if (body) {
   455 				finalBody = [finalBody stringByAppendingString:body];
   456 			}
   457 			if (_clientSourceToken) {
   458 				finalBody = [finalBody stringByAppendingString:[NSString stringWithFormat:@"%@source=%@", 
   459 																(body) ? @"&" : @"?" , 
   460 																_clientSourceToken]];
   461 			}
   462 			
   463 			if (finalBody) {
   464 				[theRequest setHTTPBody:[finalBody dataUsingEncoding:NSUTF8StringEncoding]];
   465 			}
   466 		} else if ([body isKindOfClass:[NSData class]]) {
   467 			[theRequest setHTTPBody:body];
   468 		}
   469     }
   470     
   471     
   472     // Create a connection using this request, with the default timeout and caching policy, 
   473     // and appropriate Twitter request and response types for parsing and error reporting.
   474     MGTwitterHTTPURLConnection *connection;
   475     connection = [[MGTwitterHTTPURLConnection alloc] initWithRequest:theRequest 
   476                                                             delegate:self 
   477                                                          requestType:requestType 
   478                                                         responseType:responseType];
   479     
   480     if (!connection) {
   481         return nil;
   482     } else {
   483         [_connections setObject:connection forKey:[connection identifier]];
   484         [connection release];
   485     }
   486     
   487     return [connection identifier];
   488 }
   489 #endif
   490 
   491 #pragma mark Parsing methods
   492 
   493 
   494 - (void)_parseXMLForConnection:(MGTwitterHTTPURLConnection *)connection
   495 {
   496     NSString *identifier = [[[connection identifier] copy] autorelease];
   497     NSData *xmlData = [[[connection data] copy] autorelease];
   498     MGTwitterRequestType requestType = [connection requestType];
   499     MGTwitterResponseType responseType = [connection responseType];
   500     
   501 #if USE_LIBXML
   502 	NSURL *URL = [connection URL];
   503 
   504     switch (responseType) {
   505         case MGTwitterStatuses:
   506         case MGTwitterStatus:
   507             [MGTwitterStatusesLibXMLParser parserWithXML:xmlData delegate:self 
   508                               connectionIdentifier:identifier requestType:requestType 
   509                                       responseType:responseType URL:URL];
   510             break;
   511         case MGTwitterUsers:
   512         case MGTwitterUser:
   513             [MGTwitterUsersLibXMLParser parserWithXML:xmlData delegate:self 
   514                            connectionIdentifier:identifier requestType:requestType 
   515                                    responseType:responseType URL:URL];
   516             break;
   517         case MGTwitterDirectMessages:
   518         case MGTwitterDirectMessage:
   519             [MGTwitterMessagesLibXMLParser parserWithXML:xmlData delegate:self 
   520                               connectionIdentifier:identifier requestType:requestType 
   521                                       responseType:responseType URL:URL];
   522             break;
   523 		case MGTwitterMiscellaneous:
   524 			[MGTwitterMiscLibXMLParser parserWithXML:xmlData delegate:self 
   525 						  connectionIdentifier:identifier requestType:requestType 
   526 								  responseType:responseType URL:URL];
   527 			break;
   528         default:
   529             break;
   530     }
   531 #else
   532     // Determine which type of parser to use.
   533     switch (responseType) {
   534         case MGTwitterStatuses:
   535         case MGTwitterStatus:
   536             [MGTwitterStatusesParser parserWithXML:xmlData delegate:self 
   537                               connectionIdentifier:identifier requestType:requestType 
   538                                       responseType:responseType];
   539             break;
   540         case MGTwitterUsers:
   541         case MGTwitterUser:
   542             [MGTwitterUsersParser parserWithXML:xmlData delegate:self 
   543                            connectionIdentifier:identifier requestType:requestType 
   544                                    responseType:responseType];
   545             break;
   546         case MGTwitterDirectMessages:
   547         case MGTwitterDirectMessage:
   548             [MGTwitterMessagesParser parserWithXML:xmlData delegate:self 
   549                               connectionIdentifier:identifier requestType:requestType 
   550                                       responseType:responseType];
   551             break;
   552 		case MGTwitterMiscellaneous:
   553 			[MGTwitterMiscParser parserWithXML:xmlData delegate:self 
   554 						  connectionIdentifier:identifier requestType:requestType 
   555 								  responseType:responseType];
   556 			break;
   557         default:
   558             break;
   559     }
   560 #endif
   561 }
   562 
   563 #pragma mark Delegate methods
   564 
   565 - (BOOL) _isValidDelegateForSelector:(SEL)selector
   566 {
   567 	return ((_delegate != nil) && [_delegate respondsToSelector:selector]);
   568 }
   569 
   570 #pragma mark MGTwitterParserDelegate methods
   571 
   572 
   573 - (void)parsingSucceededForRequest:(NSString *)identifier 
   574                     ofResponseType:(MGTwitterResponseType)responseType 
   575                  withParsedObjects:(NSArray *)parsedObjects
   576 {
   577     // Forward appropriate message to _delegate, depending on responseType.
   578     switch (responseType) {
   579         case MGTwitterStatuses:
   580         case MGTwitterStatus:
   581 			if ([self _isValidDelegateForSelector:@selector(statusesReceived:forRequest:)])
   582 				[_delegate statusesReceived:parsedObjects forRequest:identifier];
   583             break;
   584         case MGTwitterUsers:
   585         case MGTwitterUser:
   586 			if ([self _isValidDelegateForSelector:@selector(userInfoReceived:forRequest:)])
   587 				[_delegate userInfoReceived:parsedObjects forRequest:identifier];
   588             break;
   589         case MGTwitterDirectMessages:
   590         case MGTwitterDirectMessage:
   591 			if ([self _isValidDelegateForSelector:@selector(directMessagesReceived:forRequest:)])
   592 				[_delegate directMessagesReceived:parsedObjects forRequest:identifier];
   593             break;
   594 		case MGTwitterMiscellaneous:
   595 			if ([self _isValidDelegateForSelector:@selector(miscInfoReceived:forRequest:)])
   596 				[_delegate miscInfoReceived:parsedObjects forRequest:identifier];
   597 			break;
   598         default:
   599             break;
   600     }
   601 }
   602 
   603 
   604 - (void)parsingFailedForRequest:(NSString *)requestIdentifier 
   605                  ofResponseType:(MGTwitterResponseType)responseType 
   606                       withError:(NSError *)error
   607 {
   608 	if ([self _isValidDelegateForSelector:@selector(requestFailed:withError:)])
   609 		[_delegate requestFailed:requestIdentifier withError:error];
   610 }
   611 
   612 
   613 #pragma mark NSURLConnection delegate methods
   614 
   615 
   616 - (void)connection:(NSURLConnection *)connection didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge
   617 {
   618 	if ([challenge previousFailureCount] == 0 && ![challenge proposedCredential] && !_useOAuth && _password && _username) {
   619 		NSURLCredential *credential = [NSURLCredential credentialWithUser:_username password:_password 
   620 															  persistence:NSURLCredentialPersistenceForSession];
   621 		[[challenge sender] useCredential:credential forAuthenticationChallenge:challenge];
   622 	} else {
   623 		[[challenge sender] continueWithoutCredentialForAuthenticationChallenge:challenge];
   624 	}
   625 }
   626 
   627 
   628 - (void)connection:(MGTwitterHTTPURLConnection *)connection didReceiveResponse:(NSURLResponse *)response
   629 {
   630     // This method is called when the server has determined that it has enough information to create the NSURLResponse.
   631     // it can be called multiple times, for example in the case of a redirect, so each time we reset the data.
   632     [connection resetDataLength];
   633     
   634     // Get response code.
   635     NSHTTPURLResponse *resp = (NSHTTPURLResponse *)response;
   636     int statusCode = [resp statusCode];
   637     
   638     if (statusCode >= 400) {
   639         // Assume failure, and report to delegate.
   640         NSError *error = [NSError errorWithDomain:@"HTTP" code:statusCode userInfo:nil];
   641 		if ([self _isValidDelegateForSelector:@selector(requestFailed:withError:)])
   642 			[_delegate requestFailed:[connection identifier] withError:error];
   643         
   644         // Destroy the connection.
   645         [connection cancel];
   646         [_connections removeObjectForKey:[connection identifier]];
   647         
   648     } else if (statusCode == 304 || [connection responseType] == MGTwitterGeneric) {
   649         // Not modified, or generic success.
   650 		if ([self _isValidDelegateForSelector:@selector(requestSucceeded:)])
   651 			[_delegate requestSucceeded:[connection identifier]];
   652         if (statusCode == 304) {
   653             [self parsingSucceededForRequest:[connection identifier] 
   654                               ofResponseType:[connection responseType] 
   655                            withParsedObjects:[NSArray array]];
   656         }
   657         
   658         // Destroy the connection.
   659         [connection cancel];
   660         [_connections removeObjectForKey:[connection identifier]];
   661     }
   662     
   663     if (NO) {
   664         // Display headers for debugging.
   665         NSHTTPURLResponse *resp = (NSHTTPURLResponse *)response;
   666         NSLog(@"(%d) [%@]:\r%@", 
   667               [resp statusCode], 
   668               [NSHTTPURLResponse localizedStringForStatusCode:[resp statusCode]], 
   669               [resp allHeaderFields]);
   670     }
   671 }
   672 
   673 
   674 - (void)connection:(MGTwitterHTTPURLConnection *)connection didReceiveData:(NSData *)data
   675 {
   676     // Append the new data to the receivedData.
   677     [connection appendData:data];
   678 }
   679 
   680 
   681 - (void)connection:(MGTwitterHTTPURLConnection *)connection didFailWithError:(NSError *)error
   682 {
   683     // Inform delegate.
   684 	if ([self _isValidDelegateForSelector:@selector(requestFailed:withError:)])
   685 		[_delegate requestFailed:[connection identifier] withError:error];
   686     
   687     // Release the connection.
   688     [_connections removeObjectForKey:[connection identifier]];
   689 }
   690 
   691 
   692 - (void)connectionDidFinishLoading:(MGTwitterHTTPURLConnection *)connection
   693 {
   694     // Inform delegate.
   695 	if ([self _isValidDelegateForSelector:@selector(requestSucceeded:)])
   696 		[_delegate requestSucceeded:[connection identifier]];
   697     
   698     NSData *receivedData = [connection data];
   699     if (receivedData) {
   700         if (NO) {
   701             // Dump data as string for debugging.
   702             NSString *dataString = [NSString stringWithUTF8String:[receivedData bytes]];
   703             NSLog(@"Succeeded! Received %d bytes of data:\r\r%@", [receivedData length], dataString);
   704         }
   705         
   706         if (NO) {
   707             // Dump XML to file for debugging.
   708             NSString *dataString = [NSString stringWithUTF8String:[receivedData bytes]];
   709             [dataString writeToFile:[@"~/Desktop/twitter_messages.xml" stringByExpandingTildeInPath] 
   710                          atomically:NO encoding:NSUnicodeStringEncoding error:NULL];
   711         }
   712         
   713         if ([connection responseType] == MGTwitterImage) {
   714 			// Create image from data.
   715 #if TARGET_OS_IPHONE
   716             UIImage *image = [[[UIImage alloc] initWithData:[connection data]] autorelease];
   717 #else
   718             NSImage *image = [[[NSImage alloc] initWithData:[connection data]] autorelease];
   719 #endif
   720             
   721             // Inform delegate.
   722 			if ([self _isValidDelegateForSelector:@selector(imageReceived:forRequest:)])
   723 				[_delegate imageReceived:image forRequest:[connection identifier]];
   724         } else {
   725             // Parse XML appropriately.
   726             [self _parseXMLForConnection:connection];
   727         }
   728     }
   729     
   730     // Release the connection.
   731     [_connections removeObjectForKey:[connection identifier]];
   732 }
   733 
   734 
   735 #pragma mark -
   736 #pragma mark Twitter API methods
   737 #pragma mark -
   738 
   739 
   740 #pragma mark Account methods
   741 
   742 - (NSString *)endUserSession
   743 {
   744     NSString *path = @"account/end_session"; // deliberately no format specified
   745     
   746     return [self _sendRequestWithMethod:nil path:path queryParameters:nil body:nil 
   747                             requestType:MGTwitterAccountRequest 
   748                            responseType:MGTwitterGeneric];
   749 }
   750 
   751 
   752 - (NSString *)enableUpdatesFor:(NSString *)username
   753 {
   754     // i.e. follow
   755     if (!username) {
   756         return nil;
   757     }
   758     NSString *path = [NSString stringWithFormat:@"friendships/create/%@.xml", username];
   759     
   760     return [self _sendRequestWithMethod:HTTP_POST_METHOD path:path queryParameters:nil body:nil 
   761                             requestType:MGTwitterAccountRequest 
   762                            responseType:MGTwitterUser];
   763 }
   764 
   765 
   766 - (NSString *)disableUpdatesFor:(NSString *)username
   767 {
   768     // i.e. no longer follow
   769     if (!username) {
   770         return nil;
   771     }
   772     NSString *path = [NSString stringWithFormat:@"friendships/destroy/%@.xml", username];
   773     
   774     return [self _sendRequestWithMethod:HTTP_POST_METHOD path:path queryParameters:nil body:nil 
   775                             requestType:MGTwitterAccountRequest 
   776                            responseType:MGTwitterUser];
   777 }
   778 
   779 
   780 - (NSString *)isUser:(NSString *)username1 receivingUpdatesFor:(NSString *)username2
   781 {
   782 	if (!username1 || !username2) {
   783         return nil;
   784     }
   785 	NSMutableDictionary *params = [NSMutableDictionary dictionaryWithCapacity:0];
   786     [params setObject:username1 forKey:@"user_a"];
   787 	[params setObject:username2 forKey:@"user_b"];
   788 	
   789     NSString *path = @"friendships/exists.xml";
   790     
   791     return [self _sendRequestWithMethod:nil path:path queryParameters:params body:nil 
   792                             requestType:MGTwitterAccountRequest 
   793                            responseType:MGTwitterMiscellaneous];
   794 }
   795 
   796 
   797 - (NSString *)enableNotificationsFor:(NSString *)username
   798 {
   799     if (!username) {
   800         return nil;
   801     }
   802     NSString *path = [NSString stringWithFormat:@"notifications/follow/%@.xml", username];
   803     
   804     return [self _sendRequestWithMethod:HTTP_POST_METHOD path:path queryParameters:nil body:nil 
   805                             requestType:MGTwitterAccountRequest 
   806                            responseType:MGTwitterUser];
   807 }
   808 
   809 
   810 - (NSString *)disableNotificationsFor:(NSString *)username
   811 {
   812     if (!username) {
   813         return nil;
   814     }
   815     NSString *path = [NSString stringWithFormat:@"notifications/leave/%@.xml", username];
   816     
   817     return [self _sendRequestWithMethod:HTTP_POST_METHOD path:path queryParameters:nil body:nil 
   818                             requestType:MGTwitterAccountRequest 
   819                            responseType:MGTwitterUser];
   820 }
   821 
   822 
   823 - (NSString *)getRateLimitStatus
   824 {
   825 	NSString *path = @"account/rate_limit_status.xml";
   826 	
   827 	return [self _sendRequestWithMethod:nil path:path queryParameters:nil body:nil 
   828                             requestType:MGTwitterAccountRequest 
   829                            responseType:MGTwitterMiscellaneous];
   830 }
   831 
   832 
   833 - (NSString *)setLocation:(NSString *)location
   834 {
   835 	if (!location) {
   836         return nil;
   837     }
   838     
   839     NSString *path = @"account/update_location.xml";
   840     
   841     NSString *trimmedText = location;
   842     if ([trimmedText length] > MAX_LOCATION_LENGTH) {
   843         trimmedText = [trimmedText substringToIndex:MAX_LOCATION_LENGTH];
   844     }
   845     
   846     NSMutableDictionary *params = [NSMutableDictionary dictionaryWithCapacity:0];
   847     [params setObject:trimmedText forKey:@"location"];
   848     NSString *body = [self _queryStringWithBase:nil parameters:params prefixed:NO];
   849     
   850     return [self _sendRequestWithMethod:HTTP_POST_METHOD path:path 
   851                         queryParameters:nil body:body 
   852                             requestType:MGTwitterAccountRequest 
   853                            responseType:MGTwitterGeneric];
   854 }
   855 
   856 
   857 - (NSString *)setNotificationsDeliveryMethod:(NSString *)method
   858 {
   859 	NSString *deliveryMethod = method;
   860 	if (!method || [method length] == 0) {
   861 		deliveryMethod = @"none";
   862 	}
   863 	
   864 	NSString *path = @"account/update_delivery_device.xml";
   865     
   866     NSMutableDictionary *params = [NSMutableDictionary dictionaryWithCapacity:0];
   867     if (deliveryMethod) {
   868         [params setObject:deliveryMethod forKey:@"device"];
   869     }
   870     
   871     return [self _sendRequestWithMethod:HTTP_POST_METHOD path:path queryParameters:nil body:nil 
   872                             requestType:MGTwitterAccountRequest
   873                            responseType:MGTwitterGeneric];
   874 }
   875 
   876 
   877 - (NSString *)block:(NSString *)username
   878 {
   879 	if (!username) {
   880 		return nil;
   881 	}
   882 	
   883 	NSString *path = [NSString stringWithFormat:@"blocks/create/%@.xml", username];
   884     
   885     return [self _sendRequestWithMethod:HTTP_POST_METHOD path:path queryParameters:nil body:nil 
   886                             requestType:MGTwitterAccountRequest
   887                            responseType:MGTwitterUser];
   888 }
   889 
   890 
   891 - (NSString *)unblock:(NSString *)username
   892 {
   893 	if (!username) {
   894 		return nil;
   895 	}
   896 	
   897 	NSString *path = [NSString stringWithFormat:@"blocks/destroy/%@.xml", username];
   898     
   899     return [self _sendRequestWithMethod:HTTP_POST_METHOD path:path queryParameters:nil body:nil 
   900                             requestType:MGTwitterAccountRequest
   901                            responseType:MGTwitterUser];
   902 }
   903 
   904 
   905 - (NSString *)testService
   906 {
   907 	NSString *path = @"help/test.xml";
   908 	
   909 	return [self _sendRequestWithMethod:nil path:path queryParameters:nil body:nil 
   910                             requestType:MGTwitterAccountRequest
   911                            responseType:MGTwitterGeneric];
   912 }
   913 
   914 
   915 - (NSString *)getDowntimeSchedule
   916 {
   917 	NSString *path = @"help/downtime_schedule.xml";
   918 	
   919 	return [self _sendRequestWithMethod:nil path:path queryParameters:nil body:nil 
   920                             requestType:MGTwitterAccountRequest
   921                            responseType:MGTwitterMiscellaneous];
   922 }
   923 
   924 
   925 #pragma mark Retrieving updates
   926 
   927 
   928 - (NSString *)getFollowedTimelineFor:(NSString *)username since:(NSDate *)date startingAtPage:(int)pageNum
   929 {
   930 	// Included for backwards-compatibility.
   931     return [self getFollowedTimelineFor:username since:date startingAtPage:pageNum count:0]; // zero means default
   932 }
   933 
   934 
   935 - (NSString *)getFollowedTimelineFor:(NSString *)username since:(NSDate *)date startingAtPage:(int)pageNum count:(int)count
   936 {
   937 	NSString *path = @"statuses/home_timeline.xml";
   938     
   939     NSMutableDictionary *params = [NSMutableDictionary dictionaryWithCapacity:0];
   940     if (date) {
   941         [params setObject:[self _dateToHTTP:date] forKey:@"since"];
   942     }
   943     if (pageNum > 0) {
   944         [params setObject:[NSString stringWithFormat:@"%d", pageNum] forKey:@"page"];
   945     }
   946     if (username) {
   947         path = [NSString stringWithFormat:@"statuses/home_timeline/%@.xml", username];
   948     }
   949 	int tweetCount = DEFAULT_TWEET_COUNT;
   950 	if (count > 0) {
   951 		tweetCount = count;
   952 	}
   953 	[params setObject:[NSString stringWithFormat:@"%d", tweetCount] forKey:@"count"];
   954     
   955     return [self _sendRequestWithMethod:nil path:path queryParameters:params body:nil 
   956                             requestType:MGTwitterStatusesRequest 
   957                            responseType:MGTwitterStatuses];
   958 }
   959 
   960 
   961 - (NSString *)getFollowedTimelineFor:(NSString *)username sinceID:(NSString *)updateID startingAtPage:(int)pageNum count:(int)count
   962 {
   963 	NSString *path = @"statuses/home_timeline.xml";
   964     
   965     NSMutableDictionary *params = [NSMutableDictionary dictionaryWithCapacity:0];
   966     if (updateID > 0) {
   967         [params setObject:updateID forKey:@"since_id"];
   968     }
   969     if (pageNum > 0) {
   970         [params setObject:[NSString stringWithFormat:@"%d", pageNum] forKey:@"page"];
   971     }
   972     if (username) {
   973         path = [NSString stringWithFormat:@"statuses/home_timeline/%@.xml", username];
   974     }
   975 	int tweetCount = DEFAULT_TWEET_COUNT;
   976 	if (count > 0) {
   977 		tweetCount = count;
   978 	}
   979 	[params setObject:[NSString stringWithFormat:@"%d", tweetCount] forKey:@"count"];
   980 	
   981     return [self _sendRequestWithMethod:nil path:path queryParameters:params body:nil 
   982                             requestType:MGTwitterStatusesRequest 
   983                            responseType:MGTwitterStatuses];
   984 }
   985 
   986 
   987 - (NSString *)getUserTimelineFor:(NSString *)username since:(NSDate *)date count:(int)numUpdates
   988 {
   989 	// Included for backwards-compatibility.
   990     return [self getUserTimelineFor:username since:date startingAtPage:0 count:numUpdates];
   991 }
   992 
   993 
   994 - (NSString *)getUserTimelineFor:(NSString *)username since:(NSDate *)date startingAtPage:(int)pageNum count:(int)numUpdates
   995 {
   996 	NSString *path = @"statuses/user_timeline.xml";
   997     
   998     NSMutableDictionary *params = [NSMutableDictionary dictionaryWithCapacity:0];
   999     if (date) {
  1000         [params setObject:[self _dateToHTTP:date] forKey:@"since"];
  1001     }
  1002 	if (pageNum > 0) {
  1003         [params setObject:[NSString stringWithFormat:@"%d", pageNum] forKey:@"page"];
  1004     }
  1005     if (numUpdates > 0) {
  1006         [params setObject:[NSString stringWithFormat:@"%d", numUpdates] forKey:@"count"];
  1007     }
  1008     if (username) {
  1009         path = [NSString stringWithFormat:@"statuses/user_timeline/%@.xml", username];
  1010     }
  1011     
  1012     return [self _sendRequestWithMethod:nil path:path queryParameters:params body:nil 
  1013                             requestType:MGTwitterStatusesRequest 
  1014                            responseType:MGTwitterStatuses];
  1015 }
  1016 
  1017 
  1018 - (NSString *)getUserTimelineFor:(NSString *)username sinceID:(NSString *)updateID startingAtPage:(int)pageNum count:(int)numUpdates
  1019 {
  1020 	NSString *path = @"statuses/user_timeline.xml";
  1021     
  1022     NSMutableDictionary *params = [NSMutableDictionary dictionaryWithCapacity:0];
  1023     if (updateID > 0) {
  1024         [params setObject:updateID forKey:@"since_id"];
  1025     }
  1026 	if (pageNum > 0) {
  1027         [params setObject:[NSString stringWithFormat:@"%d", pageNum] forKey:@"page"];
  1028     }
  1029     if (numUpdates > 0) {
  1030         [params setObject:[NSString stringWithFormat:@"%d", numUpdates] forKey:@"count"];
  1031     }
  1032     if (username) {
  1033         path = [NSString stringWithFormat:@"statuses/user_timeline/%@.xml", username];
  1034     }
  1035     
  1036     return [self _sendRequestWithMethod:nil path:path queryParameters:params body:nil 
  1037                             requestType:MGTwitterStatusesRequest 
  1038                            responseType:MGTwitterStatuses];
  1039 }
  1040 
  1041 
  1042 - (NSString *)getUserUpdatesArchiveStartingAtPage:(int)pageNum
  1043 {
  1044     NSString *path = @"account/archive.xml";
  1045     
  1046     NSMutableDictionary *params = [NSMutableDictionary dictionaryWithCapacity:0];
  1047     if (pageNum > 0) {
  1048         [params setObject:[NSString stringWithFormat:@"%d", pageNum] forKey:@"page"];
  1049     }
  1050     
  1051     return [self _sendRequestWithMethod:nil path:path queryParameters:params body:nil 
  1052                             requestType:MGTwitterStatusesRequest 
  1053                            responseType:MGTwitterStatuses];
  1054 }
  1055 
  1056 
  1057 - (NSString *)getPublicTimelineSinceID:(NSString *)updateID
  1058 {
  1059     NSString *path = @"statuses/public_timeline.xml";
  1060     
  1061     NSMutableDictionary *params = [NSMutableDictionary dictionaryWithCapacity:0];
  1062     if (updateID > 0) {
  1063         [params setObject:updateID forKey:@"since_id"];
  1064     }
  1065     
  1066     return [self _sendRequestWithMethod:nil path:path queryParameters:params body:nil 
  1067                             requestType:MGTwitterStatusesRequest 
  1068                            responseType:MGTwitterStatuses];
  1069 }
  1070 
  1071 - (NSString *)getRepliesStartingAtPage:(int)pageNum
  1072 {
  1073     NSString *path = @"statuses/replies.xml";
  1074     
  1075     NSMutableDictionary *params = [NSMutableDictionary dictionaryWithCapacity:0];
  1076     if (pageNum > 0) {
  1077         [params setObject:[NSString stringWithFormat:@"%d", pageNum] forKey:@"page"];
  1078     }
  1079     
  1080     return [self _sendRequestWithMethod:nil path:path queryParameters:params body:nil 
  1081                             requestType:MGTwitterRepliesRequest 
  1082                            responseType:MGTwitterStatuses];
  1083 }
  1084 
  1085 
  1086 - (NSString *)getFavoriteUpdatesFor:(NSString *)username startingAtPage:(int)pageNum
  1087 {
  1088     NSString *path = @"favorites.xml";
  1089     
  1090     NSMutableDictionary *params = [NSMutableDictionary dictionaryWithCapacity:0];
  1091     if (pageNum > 0) {
  1092         [params setObject:[NSString stringWithFormat:@"%d", pageNum] forKey:@"page"];
  1093     }
  1094     if (username) {
  1095         path = [NSString stringWithFormat:@"favorites/%@.xml", username];
  1096     }
  1097     
  1098     return [self _sendRequestWithMethod:nil path:path queryParameters:params body:nil 
  1099                             requestType:MGTwitterStatusesRequest 
  1100                            responseType:MGTwitterStatuses];
  1101 }
  1102 
  1103 
  1104 - (NSString *)getUpdate:(NSString *)updateID
  1105 {
  1106     NSString *path = [NSString stringWithFormat:@"statuses/show/%@.xml", updateID];
  1107     
  1108     return [self _sendRequestWithMethod:nil path:path queryParameters:nil body:nil 
  1109                             requestType:MGTwitterStatusesRequest 
  1110                            responseType:MGTwitterStatus];
  1111 }
  1112 
  1113 
  1114 #pragma mark Retrieving direct messages
  1115 
  1116 
  1117 - (NSString *)getDirectMessagesSince:(NSDate *)date startingAtPage:(int)pageNum
  1118 {
  1119     NSString *path = @"direct_messages.xml";
  1120     
  1121     NSMutableDictionary *params = [NSMutableDictionary dictionaryWithCapacity:0];
  1122     if (date) {
  1123         [params setObject:[self _dateToHTTP:date] forKey:@"since"];
  1124     }
  1125     if (pageNum > 0) {
  1126         [params setObject:[NSString stringWithFormat:@"%d", pageNum] forKey:@"page"];
  1127     }
  1128     
  1129     return [self _sendRequestWithMethod:nil path:path queryParameters:params body:nil 
  1130                             requestType:MGTwitterDirectMessagesRequest 
  1131                            responseType:MGTwitterDirectMessages];
  1132 }
  1133 
  1134 
  1135 - (NSString *)getDirectMessagesSinceID:(NSString *)updateID startingAtPage:(int)pageNum
  1136 {
  1137     NSString *path = @"direct_messages.xml";
  1138     
  1139     NSMutableDictionary *params = [NSMutableDictionary dictionaryWithCapacity:0];
  1140     if (updateID > 0) {
  1141         [params setObject:updateID forKey:@"since_id"];
  1142     }
  1143     if (pageNum > 0) {
  1144         [params setObject:[NSString stringWithFormat:@"%d", pageNum] forKey:@"page"];
  1145     }
  1146     
  1147     return [self _sendRequestWithMethod:nil path:path queryParameters:params body:nil 
  1148                             requestType:MGTwitterDirectMessagesRequest 
  1149                            responseType:MGTwitterDirectMessages];
  1150 }
  1151 
  1152 
  1153 - (NSString *)getSentDirectMessagesSince:(NSDate *)date startingAtPage:(int)pageNum
  1154 {
  1155     NSString *path = @"direct_messages/sent.xml";
  1156     
  1157     NSMutableDictionary *params = [NSMutableDictionary dictionaryWithCapacity:0];
  1158     if (date) {
  1159         [params setObject:[self _dateToHTTP:date] forKey:@"since"];
  1160     }
  1161     if (pageNum > 0) {
  1162         [params setObject:[NSString stringWithFormat:@"%d", pageNum] forKey:@"page"];
  1163     }
  1164     
  1165     return [self _sendRequestWithMethod:nil path:path queryParameters:params body:nil 
  1166                             requestType:MGTwitterDirectMessagesRequest 
  1167                            responseType:MGTwitterDirectMessages];
  1168 }
  1169 
  1170 
  1171 - (NSString *)getSentDirectMessagesSinceID:(NSString *)updateID startingAtPage:(int)pageNum
  1172 {
  1173     NSString *path = @"direct_messages/sent.xml";
  1174     
  1175     NSMutableDictionary *params = [NSMutableDictionary dictionaryWithCapacity:0];
  1176     if (updateID > 0) {
  1177         [params setObject:updateID forKey:@"since_id"];
  1178     }
  1179     if (pageNum > 0) {
  1180         [params setObject:[NSString stringWithFormat:@"%d", pageNum] forKey:@"page"];
  1181     }
  1182     
  1183     return [self _sendRequestWithMethod:nil path:path queryParameters:params body:nil 
  1184                             requestType:MGTwitterDirectMessagesRequest 
  1185                            responseType:MGTwitterDirectMessages];
  1186 }
  1187 
  1188 
  1189 #pragma mark Retrieving user information
  1190 
  1191 
  1192 - (NSString *)getUserInformationFor:(NSString *)username
  1193 {
  1194     if (!username) {
  1195         return nil;
  1196     }
  1197     NSString *path = [NSString stringWithFormat:@"users/show/%@.xml", username];
  1198     
  1199     return [self _sendRequestWithMethod:nil path:path queryParameters:nil body:nil 
  1200                             requestType:MGTwitterUserInfoRequest 
  1201                            responseType:MGTwitterUser];
  1202 }
  1203 
  1204 
  1205 - (NSString *)getUserInformationForEmail:(NSString *)email
  1206 {
  1207     NSString *path = @"users/show.xml";
  1208     NSMutableDictionary *params = [NSMutableDictionary dictionaryWithCapacity:0];
  1209     if (email) {
  1210         [params setObject:email forKey:@"email"];
  1211     } else {
  1212         return nil;
  1213     }
  1214     
  1215     return [self _sendRequestWithMethod:nil path:path queryParameters:params body:nil 
  1216                             requestType:MGTwitterUserInfoRequest 
  1217                            responseType:MGTwitterUser];
  1218 }
  1219 
  1220 
  1221 - (NSString *)getRecentlyUpdatedFriendsFor:(NSString *)username startingAtPage:(int)pageNum
  1222 {
  1223     NSString *path = @"statuses/friends.xml";
  1224     
  1225     NSMutableDictionary *params = [NSMutableDictionary dictionaryWithCapacity:0];
  1226     if (username) {
  1227         path = [NSString stringWithFormat:@"statuses/friends/%@.xml", username];
  1228     }
  1229     if (pageNum > 0) {
  1230         [params setObject:[NSString stringWithFormat:@"%d", pageNum] forKey:@"page"];
  1231     }
  1232     
  1233     return [self _sendRequestWithMethod:nil path:path queryParameters:params body:nil 
  1234                             requestType:MGTwitterUserInfoRequest 
  1235                            responseType:MGTwitterUsers];
  1236 }
  1237 
  1238 
  1239 - (NSString *)getFollowersIncludingCurrentStatus:(BOOL)flag
  1240 {
  1241     NSString *path = @"statuses/followers.xml";
  1242     
  1243     NSMutableDictionary *params = [NSMutableDictionary dictionaryWithCapacity:0];
  1244     if (!flag) {
  1245         [params setObject:@"true" forKey:@"lite"]; // slightly bizarre, but correct.
  1246     }
  1247     
  1248     return [self _sendRequestWithMethod:nil path:path queryParameters:params body:nil 
  1249                             requestType:MGTwitterUserInfoRequest 
  1250                            responseType:MGTwitterUsers];
  1251 }
  1252 
  1253 
  1254 - (NSString *)getFeaturedUsers
  1255 {
  1256     NSString *path = @"statuses/featured.xml";
  1257     
  1258     return [self _sendRequestWithMethod:nil path:path queryParameters:nil body:nil 
  1259                             requestType:MGTwitterUserInfoRequest 
  1260                            responseType:MGTwitterUsers];
  1261 }
  1262 
  1263 
  1264 #pragma mark Sending and editing updates
  1265 
  1266 
  1267 - (NSString *)sendUpdate:(NSString *)status
  1268 {
  1269     return [self sendUpdate:status inReplyTo:0];
  1270 }
  1271 
  1272 
  1273 - (NSString *)sendUpdate:(NSString *)status inReplyTo:(NSString *)updateID
  1274 {
  1275     if (!status) {
  1276         return nil;
  1277     }
  1278     
  1279     NSString *path = @"statuses/update.xml";
  1280     
  1281     NSString *trimmedText = status;
  1282     if ([trimmedText length] > MAX_MESSAGE_LENGTH) {
  1283         trimmedText = [trimmedText substringToIndex:MAX_MESSAGE_LENGTH];
  1284     }
  1285     
  1286     NSMutableDictionary *params = [NSMutableDictionary dictionaryWithCapacity:0];
  1287     [params setObject:trimmedText forKey:@"status"];
  1288     if (updateID > 0) {
  1289         [params setObject:updateID forKey:@"in_reply_to_status_id"];
  1290     }
  1291     NSString *body = [self _queryStringWithBase:nil parameters:params prefixed:NO];
  1292     
  1293     return [self _sendRequestWithMethod:HTTP_POST_METHOD path:path 
  1294                         queryParameters:nil body:body 
  1295                             requestType:MGTwitterStatusSend 
  1296                            responseType:MGTwitterStatus];
  1297 }
  1298 
  1299 
  1300 - (NSString *)deleteUpdate:(NSString *)updateID
  1301 {
  1302     NSString *path = [NSString stringWithFormat:@"statuses/destroy/%@.xml", updateID];
  1303     
  1304     return [self _sendRequestWithMethod:HTTP_POST_METHOD path:path queryParameters:nil body:nil 
  1305                             requestType:MGTwitterAccountRequest 
  1306                            responseType:MGTwitterGeneric];
  1307 }
  1308 
  1309 
  1310 - (NSString *)markUpdate:(NSString *)updateID asFavorite:(BOOL)flag
  1311 {
  1312     NSString *path = [NSString stringWithFormat:@"favorites/%@/%@.xml", 
  1313                       (flag) ? @"create" : @"destroy" ,
  1314                       updateID];
  1315     
  1316     return [self _sendRequestWithMethod:HTTP_POST_METHOD path:path queryParameters:nil body:nil 
  1317                             requestType:MGTwitterAccountRequest 
  1318                            responseType:MGTwitterStatus];
  1319 }
  1320 
  1321 - (NSString *)retweetUpdate:(NSString *)updateID
  1322 {
  1323     NSString *path = [NSString stringWithFormat:@"statuses/retweet/%@.xml", updateID];
  1324     
  1325     return [self _sendRequestWithMethod:HTTP_POST_METHOD path:path queryParameters:nil body:nil 
  1326                             requestType:MGTwitterAccountRequest 
  1327                            responseType:MGTwitterStatus];	
  1328 }
  1329 
  1330 #pragma mark Sending and editing direct messages
  1331 
  1332 
  1333 - (NSString *)sendDirectMessage:(NSString *)message to:(NSString *)username
  1334 {
  1335     if (!message || !username) {
  1336         return nil;
  1337     }
  1338     
  1339     NSString *path = @"direct_messages/new.xml";
  1340     
  1341     NSString *trimmedText = message;
  1342     if ([trimmedText length] > MAX_MESSAGE_LENGTH) {
  1343         trimmedText = [trimmedText substringToIndex:MAX_MESSAGE_LENGTH];
  1344     }
  1345     
  1346     NSMutableDictionary *params = [NSMutableDictionary dictionaryWithCapacity:0];
  1347     [params setObject:trimmedText forKey:@"text"];
  1348     [params setObject:username forKey:@"user"];
  1349     NSString *body = [self _queryStringWithBase:nil parameters:params prefixed:NO];
  1350     
  1351     return [self _sendRequestWithMethod:HTTP_POST_METHOD path:path 
  1352                         queryParameters:nil body:body 
  1353                             requestType:MGTwitterDirectMessageSend 
  1354                            responseType:MGTwitterDirectMessage];
  1355 }
  1356 
  1357 
  1358 - (NSString *)deleteDirectMessage:(NSString *)updateID
  1359 {
  1360     NSString *path = [NSString stringWithFormat:@"direct_messages/destroy/%@.xml", updateID];
  1361     
  1362     return [self _sendRequestWithMethod:HTTP_POST_METHOD path:path queryParameters:nil body:nil 
  1363                             requestType:MGTwitterAccountRequest 
  1364                            responseType:MGTwitterGeneric];
  1365 }
  1366 
  1367 #pragma mark Adium Additions/Changes
  1368 
  1369 #define MAX_NAME_LENGTH			20
  1370 #define MAX_EMAIL_LENGTH		40
  1371 #define MAX_URL_LENGTH			100
  1372 #define MAX_DESCRIPTION_LENGTH	160
  1373 
  1374 - (NSString *)checkUserCredentials
  1375 {
  1376     NSString *path = @"account/verify_credentials.xml";
  1377     
  1378     return [self _sendRequestWithMethod:nil path:path queryParameters:nil body:nil 
  1379                             requestType:MGTwitterAccountRequest 
  1380                            responseType:MGTwitterUser];
  1381 }
  1382 
  1383 
  1384 - (NSString *)getRepliesSinceID:(NSString *)updateID startingAtPage:(int)pageNum
  1385 {
  1386 	NSString *path = @"statuses/replies.xml";
  1387     
  1388     NSMutableDictionary *params = [NSMutableDictionary dictionaryWithCapacity:0];
  1389     if (updateID > 0) {
  1390         [params setObject:updateID forKey:@"since_id"];
  1391     }
  1392     if (pageNum > 0) {
  1393         [params setObject:[NSString stringWithFormat:@"%d", pageNum] forKey:@"page"];
  1394     }
  1395     
  1396     return [self _sendRequestWithMethod:nil path:path queryParameters:params body:nil 
  1397                             requestType:MGTwitterRepliesRequest 
  1398                            responseType:MGTwitterStatuses];
  1399 }
  1400 
  1401 - (NSString *)updateProfileName:(NSString *)name
  1402 						  email:(NSString *)email
  1403 							url:(NSString *)url
  1404 					   location:(NSString *)location
  1405 					description:(NSString *)description
  1406 {
  1407 	if (!name && !email && !url && !location && !description) {
  1408         return nil;
  1409     }
  1410     
  1411     NSString *path = @"account/update_profile.xml";
  1412     
  1413     NSMutableDictionary *params = [NSMutableDictionary dictionaryWithCapacity:0];
  1414 	
  1415 	if (name) {
  1416 		if(name.length > MAX_NAME_LENGTH) {
  1417 			name = [name substringToIndex:MAX_NAME_LENGTH];
  1418 		}
  1419 		
  1420 		[params setObject:name forKey:@"name"];
  1421 	}
  1422 
  1423 	if (email) {
  1424 		if(email.length > MAX_EMAIL_LENGTH) {
  1425 			email = [email substringToIndex:MAX_EMAIL_LENGTH];
  1426 		}
  1427 		
  1428 		[params setObject:email forKey:@"email"];
  1429 	}
  1430 	
  1431 	if (url) {
  1432 		if(url.length > MAX_URL_LENGTH) {
  1433 			url = [url substringToIndex:MAX_URL_LENGTH];
  1434 		}
  1435 		
  1436 		[params setObject:url forKey:@"url"];
  1437 	}
  1438 	
  1439 	if (location) {
  1440 		if(location.length > MAX_LOCATION_LENGTH) {
  1441 			location = [location substringToIndex:MAX_LOCATION_LENGTH];
  1442 		}
  1443 		
  1444 		[params setObject:location forKey:@"location"];
  1445 	}
  1446 	
  1447 	if (description) {
  1448 		if(description.length > MAX_DESCRIPTION_LENGTH) {
  1449 			description = [description substringToIndex:MAX_DESCRIPTION_LENGTH];
  1450 		}
  1451 		
  1452 		[params setObject:description forKey:@"description"];
  1453 	}
  1454 
  1455     NSString *body = [self _queryStringWithBase:nil parameters:params prefixed:NO];
  1456     
  1457     return [self _sendRequestWithMethod:HTTP_POST_METHOD path:path 
  1458                         queryParameters:nil body:body 
  1459                             requestType:MGTwitterAccountRequest 
  1460                            responseType:MGTwitterUser];
  1461 }
  1462 
  1463 - (NSString *)updateProfileImage:(NSData *)profileImage
  1464 {
  1465 	if (!profileImage || _useOAuth) {
  1466         return nil;
  1467     }
  1468     
  1469     NSString *path = @"account/update_profile_image.xml";
  1470 	
  1471 	NSMutableData *body = [NSMutableData data];
  1472 	NSMutableDictionary *params = [NSMutableDictionary dictionaryWithCapacity:0];
  1473 	
  1474 	NSImage *image = [[[NSImage alloc] initWithData:profileImage] autorelease];
  1475 	
  1476 	NSBitmapImageRep *bitmapImageRep = nil;
  1477 	for(NSImageRep *imageRep in image.representations) {
  1478 		if([imageRep isKindOfClass:[NSBitmapImageRep class]]) {
  1479 			bitmapImageRep = (NSBitmapImageRep *)imageRep;
  1480 		}
  1481 	}
  1482 	
  1483 	if(!bitmapImageRep) {
  1484 		return nil;
  1485 	}
  1486 	
  1487 	[body appendData:[[NSString stringWithFormat:@"--%@\r\n", MULTIPART_FORM_BOUNDARY] dataUsingEncoding:NSUTF8StringEncoding]];
  1488 	[body appendData:[@"Content-Disposition: form-data; name=\"image\"; filename=\"adium_icon.png\"\r\n" dataUsingEncoding:NSUTF8StringEncoding]];
  1489 	[body appendData:[@"Content-Type: image/png\r\n\r\n" dataUsingEncoding:NSUTF8StringEncoding]];
  1490 	[body appendData:[bitmapImageRep representationUsingType:NSPNGFileType properties:nil]];
  1491 	[body appendData:[[NSString stringWithFormat:@"\r\n--%@--\r\n", MULTIPART_FORM_BOUNDARY] dataUsingEncoding:NSUTF8StringEncoding]];
  1492 
  1493     return [self _sendRequestWithMethod:HTTP_MULTIPART_METHOD path:path 
  1494                         queryParameters:params body:body 
  1495                             requestType:MGTwitterAccountRequest 
  1496                            responseType:MGTwitterUser];
  1497 }
  1498 
  1499 #pragma mark Adium OAuth Changes
  1500 
  1501 - (NSString *)_sendRequestWithMethod:(NSString *)method 
  1502                                 path:(NSString *)path 
  1503                      queryParameters:(NSDictionary *)params 
  1504                                 body:(id)body 
  1505                          requestType:(MGTwitterRequestType)requestType 
  1506                         responseType:(MGTwitterResponseType)responseType
  1507 {
  1508     // Construct appropriate URL string.
  1509     NSString *fullPath = path;
  1510     if (params) {
  1511         fullPath = [self _queryStringWithBase:fullPath parameters:params prefixed:YES];
  1512     }
  1513 
  1514 	NSString *urlString = nil;
  1515 	
  1516 	if (!_useOAuth) {
  1517 		#if SET_AUTHORIZATION_IN_HEADER
  1518 			urlString = [NSString stringWithFormat:@"%@://%@/%@", 
  1519 						 (_secureConnection) ? @"https" : @"http",
  1520 						 _APIDomain, fullPath];
  1521 		#else 
  1522 			urlString = [NSString stringWithFormat:@"%@://%@:%@@%@/%@", 
  1523 						 (_secureConnection) ? @"https" : @"http", 
  1524 						 [self _encodeString:_username], [self _encodeString:_password], 
  1525 						 _APIDomain, fullPath];
  1526 		#endif
  1527 	} else {
  1528 		urlString = [NSString stringWithFormat:@"%@://%@/%@", 
  1529 					 (_secureConnection) ? @"https" : @"http",
  1530 					 _APIDomain, fullPath];		
  1531 	}
  1532     
  1533     NSURL *finalURL = [NSURL URLWithString:urlString];
  1534     if (!finalURL) {
  1535         return nil;
  1536     }
  1537 	
  1538 	NSMutableURLRequest *theRequest = nil;
  1539 	
  1540 	if (_useOAuth) {
  1541 		if (!_consumer || !_accessToken) {
  1542 			NSLog(@"No consumer or access token, fail.");
  1543 			return nil;
  1544 		}
  1545 		
  1546 		theRequest = [[[OAMutableURLRequest alloc] initWithURL:finalURL
  1547 													  consumer:_consumer
  1548 														 token:_accessToken
  1549 														 realm:nil
  1550 											 signatureProvider:nil] autorelease];
  1551 	} else {
  1552 		// Construct an NSMutableURLRequest for the URL and set appropriate request method.
  1553 		theRequest = [NSMutableURLRequest requestWithURL:finalURL 
  1554 											 cachePolicy:NSURLRequestReloadIgnoringCacheData 
  1555 										 timeoutInterval:URL_REQUEST_TIMEOUT];
  1556 	}
  1557 		
  1558 	if(method && [method isEqualToString:HTTP_MULTIPART_METHOD]) {
  1559 		method = HTTP_POST_METHOD;
  1560 		[theRequest setValue:[NSString stringWithFormat:@"multipart/form-data; boundary=%@", MULTIPART_FORM_BOUNDARY] forHTTPHeaderField:@"Content-type"];
  1561 	}
  1562 	
  1563     if (method) {
  1564         [theRequest setHTTPMethod:method];
  1565     }
  1566 	
  1567     [theRequest setHTTPShouldHandleCookies:NO];
  1568 	
  1569     // Set headers for client information, for tracking purposes at Twitter.
  1570     [theRequest setValue:_clientName    forHTTPHeaderField:@"X-Twitter-Client"];
  1571     [theRequest setValue:_clientVersion forHTTPHeaderField:@"X-Twitter-Client-Version"];
  1572     [theRequest setValue:_clientURL     forHTTPHeaderField:@"X-Twitter-Client-URL"];
  1573     
  1574 #if SET_AUTHORIZATION_IN_HEADER
  1575 	if (_useOAuth && [self username] && [self password]) {
  1576 		// Set header for HTTP Basic authentication explicitly, to avoid problems with proxies and other intermediaries
  1577 		NSString *authStr = [NSString stringWithFormat:@"%@:%@", [self username], [self password]];
  1578 		NSData *authData = [authStr dataUsingEncoding:NSASCIIStringEncoding];
  1579 		NSString *authValue = [NSString stringWithFormat:@"Basic %@", [authData base64EncodingWithLineLength:80]];
  1580 		[theRequest setValue:authValue forHTTPHeaderField:@"Authorization"];
  1581 	}
  1582 #endif
  1583 	
  1584     // Set the request body if this is a POST request.
  1585     BOOL isPOST = (method && [method isEqualToString:HTTP_POST_METHOD]);
  1586 	
  1587     if (isPOST) {
  1588         // Set request body, if specified (hopefully so), with 'source' parameter if appropriate.
  1589 		if([body isKindOfClass:[NSString class]]) {
  1590 			NSString *finalBody = @"";
  1591 			if (body) {
  1592 				finalBody = [finalBody stringByAppendingString:body];
  1593 			}
  1594 			if (_clientSourceToken) {
  1595 				finalBody = [finalBody stringByAppendingString:[NSString stringWithFormat:@"%@source=%@", 
  1596 																(body) ? @"&" : @"?" , 
  1597 																_clientSourceToken]];
  1598 			}
  1599 			
  1600 			if (finalBody) {
  1601 				[theRequest setHTTPBody:[finalBody dataUsingEncoding:NSUTF8StringEncoding]];
  1602 			}
  1603 		} else if ([body isKindOfClass:[NSData class]]) {
  1604 			[theRequest setHTTPBody:body];
  1605 		}
  1606     }
  1607 	
  1608 	if (_useOAuth) {
  1609 		[(OAMutableURLRequest *)theRequest prepare];
  1610     }
  1611     
  1612     // Create a connection using this request, with the default timeout and caching policy, 
  1613     // and appropriate Twitter request and response types for parsing and error reporting.
  1614     MGTwitterHTTPURLConnection *connection;
  1615     connection = [[MGTwitterHTTPURLConnection alloc] initWithRequest:theRequest 
  1616                                                             delegate:self 
  1617                                                          requestType:requestType 
  1618                                                         responseType:responseType];
  1619     
  1620     if (!connection) {
  1621         return nil;
  1622     } else {
  1623         [_connections setObject:connection forKey:[connection identifier]];
  1624         [connection release];
  1625     }
  1626     
  1627     return [connection identifier];
  1628 }
  1629 
  1630 @synthesize consumer = _consumer, accessToken = _accessToken, useOAuth = _useOAuth;
  1631 
  1632 @end