Plugins/Twitter Plugin/AITwitterAccount.m
author Zachary West <zacw@adium.im>
Fri Oct 16 10:12:34 2009 -0400 (2009-10-16)
changeset 2612 cb1fd4d17218
parent 2520 f9e4063ed226
child 2768 85857106a45e
permissions -rw-r--r--
Patch from brion to add an SSL option for StatusNet accounts. Fixes #13077.
     1 /* 
     2  * Adium is the legal property of its developers, whose names are listed in the copyright file included
     3  * with this source distribution.
     4  * 
     5  * This program is free software; you can redistribute it and/or modify it under the terms of the GNU
     6  * General Public License as published by the Free Software Foundation; either version 2 of the License,
     7  * or (at your option) any later version.
     8  * 
     9  * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
    10  * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General
    11  * Public License for more details.
    12  * 
    13  * You should have received a copy of the GNU General Public License along with this program; if not,
    14  * write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
    15  */
    16 
    17 #import "AITwitterAccount.h"
    18 #import "AITwitterURLParser.h"
    19 #import "AITwitterReplyWindowController.h"
    20 #import "MGTwitterEngine/MGTwitterEngine.h"
    21 #import <AIUtilities/AIAttributedStringAdditions.h>
    22 #import <AIUtilities/AIStringAdditions.h>
    23 #import <AIUtilities/AIMenuAdditions.h>
    24 #import <AIUtilities/AIApplicationAdditions.h>
    25 #import <AIUtilities/AIDateFormatterAdditions.h>
    26 #import <Adium/AIChatControllerProtocol.h>
    27 #import <Adium/AIContentControllerProtocol.h>
    28 #import <Adium/AIContactControllerProtocol.h>
    29 #import <Adium/AIStatusControllerProtocol.h>
    30 #import <Adium/AIInterfaceControllerProtocol.h>
    31 #import <Adium/AIAccountControllerProtocol.h>
    32 #import <Adium/AIContactObserverManager.h>
    33 #import <Adium/AIListContact.h>
    34 #import <Adium/AIContentMessage.h>
    35 #import <Adium/AIListBookmark.h>
    36 #import <Adium/AIChat.h>
    37 #import <Adium/AIUserIcons.h>
    38 #import <Adium/AIService.h>
    39 #import <Adium/AIStatus.h>
    40 #import <Adium/AIHTMLDecoder.h>
    41 #import <Adium/AIContentEvent.h>
    42 
    43 @interface AITwitterAccount()
    44 - (void)updateUserIcon:(NSString *)url forContact:(AIListContact *)listContact;
    45 
    46 - (void)updateTimelineChat:(AIChat *)timelineChat;
    47 
    48 - (NSAttributedString *)parseMessage:(NSString *)inMessage
    49 							 tweetID:(NSString *)tweetID
    50 							  userID:(NSString *)userID
    51 					   inReplyToUser:(NSString *)replyUserID
    52 					inReplyToTweetID:(NSString *)replyTweetID;
    53 - (NSAttributedString *)parseDirectMessage:(NSString *)inMessage
    54 									withID:(NSString *)dmID
    55 								  fromUser:(NSString *)sourceUID;
    56 - (NSAttributedString *)attributedStringWithLinkLabel:(NSString *)label
    57 									  linkDestination:(NSString *)destination
    58 											linkClass:(NSString *)attributeName;
    59 
    60 - (void)setRequestType:(AITwitterRequestType)type forRequestID:(NSString *)requestID withDictionary:(NSDictionary *)info;
    61 - (AITwitterRequestType)requestTypeForRequestID:(NSString *)requestID;
    62 - (NSDictionary *)dictionaryForRequestID:(NSString *)requestID;
    63 - (void)clearRequestTypeForRequestID:(NSString *)requestID;
    64 
    65 - (void)periodicUpdate;
    66 - (void)displayQueuedUpdatesForRequestType:(AITwitterRequestType)requestType;
    67 
    68 - (void)getRateLimitAmount;
    69 @end
    70 
    71 @implementation AITwitterAccount
    72 - (void)initAccount
    73 {
    74 	[super initAccount];
    75 	
    76 	pendingRequests = [[NSMutableDictionary alloc] init];
    77 	queuedUpdates = [[NSMutableArray alloc] init];
    78 	queuedDM = [[NSMutableArray alloc] init];
    79 	queuedOutgoingDM = [[NSMutableArray alloc] init];
    80 
    81 	[[NSNotificationCenter defaultCenter] addObserver:self
    82 							     selector:@selector(chatDidOpen:) 
    83 									 name:Chat_DidOpen
    84 								   object:nil];
    85 	
    86 	[adium.preferenceController registerDefaults:[NSDictionary dictionaryWithObjectsAndKeys:
    87 												  [NSNumber numberWithInt:TWITTER_UPDATE_INTERVAL_MINUTES], TWITTER_PREFERENCE_UPDATE_INTERVAL,
    88 												  [NSNumber numberWithBool:YES], TWITTER_PREFERENCE_UPDATE_AFTER_SEND,
    89 												  [NSNumber numberWithBool:YES], TWITTER_PREFERENCE_LOAD_CONTACTS, nil]
    90 										forGroup:TWITTER_PREFERENCE_GROUP_UPDATES
    91 										  object:self];
    92 
    93 	// If we don't have a server set, set our default (if we have one)
    94 	if (!self.host && self.defaultServer) {
    95 		[self setPreference:self.defaultServer forKey:KEY_CONNECT_HOST group:GROUP_ACCOUNT_STATUS];
    96 	}
    97 
    98 	[adium.preferenceController registerPreferenceObserver:self forGroup:TWITTER_PREFERENCE_GROUP_UPDATES];
    99 	[adium.preferenceController informObserversOfChangedKey:nil inGroup:TWITTER_PREFERENCE_GROUP_UPDATES object:self];
   100 }
   101 
   102 - (void)dealloc
   103 {
   104 	[[NSNotificationCenter defaultCenter] removeObserver:self];
   105 	[adium.preferenceController unregisterPreferenceObserver:self];
   106 	
   107 	[twitterEngine release];
   108 	[pendingRequests release];
   109 	[queuedUpdates release];
   110 	[queuedDM release];
   111 	[queuedOutgoingDM release];
   112 	
   113 	[super dealloc];
   114 }
   115 
   116 /*!
   117  * @brief Our default server if none is provided.
   118  */
   119 - (NSString *)defaultServer
   120 {
   121 	return @"twitter.com";
   122 }
   123 
   124 #pragma mark AIAccount methods
   125 /*!
   126  * @brief We've been asked to connect.
   127  *
   128  * Sets our username and password for MGTwitterEngine, and validates credentials.
   129  *
   130  * Our connection procedure:
   131  * 1. Validate credentials
   132  * 2. Retrieve friends
   133  * 3. Trigger "periodic" update - DM, replies, timeline
   134  */
   135 - (void)connect
   136 {
   137 	[super connect];
   138 	
   139 	[twitterEngine release];
   140 	
   141 	twitterEngine = [[MGTwitterEngine alloc] initWithDelegate:self];
   142 	
   143 	[twitterEngine setClientName:@"Adium"
   144 						 version:[NSApp applicationVersion]
   145 							 URL:@"http://www.adiumx.com"
   146 						   token:self.sourceToken];	
   147 	
   148 	[twitterEngine setAPIDomain:[self.host stringByAppendingPathComponent:self.apiPath]];
   149 	
   150 	[twitterEngine setUsesSecureConnection:self.useSSL];
   151 	
   152 	if (self.useOAuth) {
   153 		if (!self.passwordWhileConnected.length) {
   154 			[self setLastDisconnectionError:TWITTER_OAUTH_NOT_AUTHORIZED];
   155 			
   156 			[[NSNotificationCenter defaultCenter] postNotificationName:@"AIEditAccount"
   157 																object:self];
   158 			
   159 			[self didDisconnect];
   160 			
   161 			// Don't try and connect.
   162 			return;
   163 			
   164 		} else {
   165 			twitterEngine.useOAuth = YES;
   166 			
   167 			OAToken *token = [[[OAToken alloc] initWithHTTPResponseBody:self.passwordWhileConnected] autorelease];
   168 			OAConsumer *consumer = [[[OAConsumer alloc] initWithKey:self.consumerKey secret:self.secretKey] autorelease];
   169 			
   170 			twitterEngine.accessToken = token;
   171 			twitterEngine.consumer = consumer;
   172 		}
   173 	} else {
   174 		[twitterEngine setUsername:self.UID password:self.passwordWhileConnected];
   175 	}
   176 	
   177 	AILogWithSignature(@"%@ connecting to %@", self, twitterEngine.APIDomain);
   178 	
   179 	NSString *requestID = [twitterEngine checkUserCredentials];
   180 	
   181 	if (requestID) {
   182 		[self setRequestType:AITwitterValidateCredentials forRequestID:requestID withDictionary:nil];
   183 	} else {
   184 		[self setLastDisconnectionError:AILocalizedString(@"Unable to connect to server", nil)];
   185 		[self didDisconnect];
   186 	}
   187 }
   188 
   189 /*!
   190  * @brief Connection successful
   191  *
   192  * Our credentials were validated correctly. Set up the timeline chat, and request our friends from the server.
   193  */
   194 - (void)didConnect
   195 {
   196 	[super didConnect];
   197 	
   198 	//Clear any previous disconnection error
   199 	[self setLastDisconnectionError:nil];
   200 	
   201 	// Creating the fake timeline account.
   202 	AIListBookmark *timelineBookmark = nil;
   203 	
   204 	if(!(timelineBookmark = [adium.contactController existingBookmarkForChatName:self.timelineChatName
   205 																	   onAccount:self
   206 																chatCreationInfo:nil])) {
   207 		AIChat *newTimelineChat = [adium.chatController chatWithName:self.timelineChatName
   208 														  identifier:nil
   209 														   onAccount:self 
   210 													chatCreationInfo:nil];
   211 		
   212 		[newTimelineChat setDisplayName:self.timelineChatName];
   213 		
   214 		timelineBookmark = [adium.contactController bookmarkForChat:newTimelineChat inGroup:[adium.contactController groupWithUID:TWITTER_REMOTE_GROUP_NAME]];
   215 
   216 	}
   217 	
   218 	NSTimeInterval updateInterval = [[self preferenceForKey:TWITTER_PREFERENCE_UPDATE_INTERVAL group:TWITTER_PREFERENCE_GROUP_UPDATES] integerValue] * 60;
   219 	
   220 	if(updateInterval > 0) {
   221 		[updateTimer invalidate];
   222 		updateTimer = [NSTimer scheduledTimerWithTimeInterval:updateInterval
   223 													   target:self
   224 													 selector:@selector(periodicUpdate)
   225 													 userInfo:nil
   226 													  repeats:YES];
   227 		
   228 		[self periodicUpdate];
   229 	}
   230 }
   231 
   232 /*!
   233  * @brief We've been asked to disconnect.
   234  *
   235  * End the session.
   236  */
   237 - (void)disconnect
   238 {
   239 	[super disconnect];
   240 	
   241 	[twitterEngine release]; twitterEngine = nil;
   242 	[updateTimer invalidate]; updateTimer = nil;
   243 	
   244 	[self didDisconnect];
   245 }
   246 
   247 /*!
   248  * @brief Account will be deleted
   249  */
   250 - (void)willBeDeleted
   251 {
   252 	[updateTimer invalidate]; updateTimer = nil;
   253 	
   254 	[super willBeDeleted];
   255 }
   256 
   257 /*!
   258  * @brief Session ended
   259  *
   260  * Remove all state information.
   261  */
   262 - (void)didDisconnect
   263 {
   264 	[updateTimer invalidate]; updateTimer = nil;
   265 	[pendingRequests removeAllObjects];
   266 	[queuedDM removeAllObjects];
   267 	[queuedOutgoingDM removeAllObjects];
   268 	[queuedUpdates removeAllObjects];
   269 	
   270 	[super didDisconnect];
   271 }
   272 
   273 /*!
   274  * @brief API path
   275  *
   276  * The API path extension for the given host.
   277  */
   278 - (NSString *)apiPath
   279 {
   280 	return nil;
   281 }
   282 
   283 /*!
   284  * @brief Our source token
   285  *
   286  * On Twitter, our given source token is "adiumofficial".
   287  */
   288 - (NSString *)sourceToken
   289 {
   290 	return @"adiumofficial";
   291 }
   292 
   293 /*!
   294  * @brief Returns whether or not to connect to Twitter API over HTTPS.
   295  */
   296 - (BOOL)useSSL
   297 {
   298 	return YES;
   299 }
   300 
   301 /*!
   302  * @brief Returns whether or not this account is connected via an encrypted connection.
   303  */
   304 - (BOOL)encrypted
   305 {
   306 	return (self.online && [twitterEngine usesSecureConnection]);
   307 }
   308 
   309 /*!
   310  * @brief Affirm we can open chats.
   311  */
   312 - (BOOL)openChat:(AIChat *)chat
   313 {	
   314 	[chat setValue:[NSNumber numberWithBool:YES] forProperty:@"Account Joined" notify:NotifyNow];
   315 	
   316 	return YES;
   317 }
   318 
   319 /*!
   320  * @brief Allow all chats to close.
   321  */
   322 - (BOOL)closeChat:(AIChat *)inChat
   323 {	
   324 	return YES;
   325 }
   326 
   327 /*!
   328  * @brief Rejoin the requested chat.
   329  */
   330 - (BOOL)rejoinChat:(AIChat *)inChat
   331 {	
   332 	[self displayYouHaveConnectedInChat:inChat];
   333 	
   334 	return YES;
   335 }
   336 
   337 /*!
   338  * @brief We always want to autocomplete the UID.
   339  */
   340 - (BOOL)chatShouldAutocompleteUID:(AIChat *)inChat
   341 {
   342 	return YES;
   343 }
   344 
   345 /*!
   346  * @brief A chat opened.
   347  *
   348  * If this is a group chat which belongs to us, aka a timeline chat, set it up how we want it.
   349  */
   350 - (void)chatDidOpen:(NSNotification *)notification
   351 {
   352 	AIChat *chat = [notification object];
   353 	
   354 	if(chat.isGroupChat && chat.account == self) {
   355 		[self updateTimelineChat:chat];
   356 	}	
   357 }
   358 
   359 /*!
   360  * @brief We support adding and removing follows.
   361  */
   362 - (BOOL)contactListEditable
   363 {
   364     return self.online;
   365 }
   366 
   367 /*!
   368  * @brief Move contacts
   369  *
   370  * Move existing contacts to a specific group on this account.  The passed contacts should already exist somewhere on
   371  * this account.
   372  * @param objects NSArray of AIListContact objects to remove
   373  * @param group AIListGroup destination for contacts
   374  */
   375 - (void)moveListObjects:(NSArray *)objects oldGroups:(NSSet *)oldGroups toGroups:(NSSet *)groups
   376 {
   377 	// XXX do twitter grouping
   378 }
   379 
   380 /*!
   381  * @brief Rename a group
   382  *
   383  * Rename a group on this account.
   384  * @param group AIListGroup to rename
   385  * @param newName NSString name for the group
   386  */
   387 - (void)renameGroup:(AIListGroup *)group to:(NSString *)newName
   388 {
   389 	// XXX do twitter grouping
   390 }
   391 
   392 /*!
   393  * @brief For an invalid password, fail but don't try and reconnect or report it. We do it ourself.
   394  */
   395 - (AIReconnectDelayType)shouldAttemptReconnectAfterDisconnectionError:(NSString **)disconnectionError
   396 {
   397 	AIReconnectDelayType reconnectDelayType = [super shouldAttemptReconnectAfterDisconnectionError:disconnectionError];
   398 	
   399 	if ([*disconnectionError isEqualToString:TWITTER_INCORRECT_PASSWORD_MESSAGE]) {
   400 		reconnectDelayType = AIReconnectImmediately;
   401 	} else if ([*disconnectionError isEqualToString:TWITTER_OAUTH_NOT_AUTHORIZED]) {
   402 		reconnectDelayType = AIReconnectNeverNoMessage;
   403 	}
   404 	
   405 	return reconnectDelayType;
   406 }
   407 
   408 /*!
   409  * @brief Don't allow OTR encryption.
   410  */
   411 - (BOOL)allowSecureMessagingTogglingForChat:(AIChat *)inChat
   412 {
   413 	return NO;
   414 }
   415 
   416 /*!
   417  * @brief Update our status
   418  */
   419 - (void)setSocialNetworkingStatusMessage:(NSAttributedString *)statusMessage
   420 {
   421 	NSString *requestID = [twitterEngine sendUpdate:[statusMessage string]];
   422 
   423 	if(requestID) {
   424 		[self setRequestType:AITwitterSendUpdate
   425 				forRequestID:requestID
   426 			  withDictionary:nil];
   427 	}
   428 }
   429 
   430 - (NSString *)encodedAttributedString:(NSAttributedString *)inAttributedString forListObject:(AIListObject *)inListObject
   431 {
   432 	return [[inAttributedString attributedStringByConvertingLinksToURLStrings] string];
   433 }
   434 
   435 /*!
   436  * @brief Send a message
   437  *
   438  * Sends a direct message to the user requested.
   439  * If it fails to send, i.e. the request fails, an unknown error will occur.
   440  * This is usually caused by the target not following the user.
   441  */
   442 - (BOOL)sendMessageObject:(AIContentMessage *)inContentMessage
   443 {
   444 	NSString *requestID;
   445 	
   446 	if(inContentMessage.chat.isGroupChat) {
   447 		requestID = [twitterEngine sendUpdate:inContentMessage.encodedMessage
   448 									inReplyTo:[inContentMessage.chat valueForProperty:@"TweetInReplyToStatusID"]];
   449 		
   450 		if(requestID) {
   451 			[self setRequestType:AITwitterSendUpdate
   452 					forRequestID:requestID
   453 				  withDictionary:[NSDictionary dictionaryWithObject:inContentMessage.chat
   454 															 forKey:@"Chat"]];
   455 			
   456 			inContentMessage.displayContent = NO;
   457 			
   458 			AILogWithSignature(@"%@ Sending update [in reply to %@]: %@", self, [inContentMessage.chat valueForProperty:@"TweetInReplyToStatusID"], inContentMessage.encodedMessage);
   459 		}
   460 
   461 	} else {		
   462 		requestID = [twitterEngine sendDirectMessage:inContentMessage.encodedMessage
   463 												  to:inContentMessage.destination.UID];
   464 		
   465 		if(requestID) {
   466 			[self setRequestType:AITwitterDirectMessageSend
   467 					forRequestID:requestID
   468 				  withDictionary:[NSDictionary dictionaryWithObject:inContentMessage.chat
   469 															 forKey:@"Chat"]];
   470 			
   471 			inContentMessage.displayContent = NO;
   472 			
   473 			AILogWithSignature(@"%@ Sending DM to %@: %@", self, inContentMessage.destination.UID, inContentMessage.encodedMessage);
   474 		}
   475 	}
   476 	
   477 	if (!requestID) {
   478 		AILogWithSignature(@"%@ Message immediate fail.", self);
   479 	}
   480 	
   481 	return (requestID != nil);
   482 }
   483 
   484 /*!
   485  * @brief Trigger an info update
   486  *
   487  * This is called when the info inspector wants more information on a contact.
   488  * Grab the user's profile information, set everything up accordingly in the user info method.
   489  */
   490 - (void)delayedUpdateContactStatus:(AIListContact *)inContact
   491 {
   492 	if(!self.online) {
   493 		return;
   494 	}
   495 	
   496 	NSString *requestID = [twitterEngine getUserInformationFor:inContact.UID];
   497 	
   498 	if(requestID) {
   499 		[self setRequestType:AITwitterProfileUserInfo
   500 				forRequestID:requestID
   501 			  withDictionary:[NSDictionary dictionaryWithObject:inContact forKey:@"ListContact"]];
   502 	}
   503 }
   504 
   505 /*!
   506  * @brief Should an autoreply be sent to this message?
   507  */
   508 - (BOOL)shouldSendAutoreplyToMessage:(AIContentMessage *)message
   509 {
   510 	return NO;
   511 }
   512 
   513 /*!
   514  * @brief Update the Twitter profile
   515  */
   516 - (void)setProfileName:(NSString *)name
   517 				   url:(NSString*)url
   518 			  location:(NSString *)location
   519 		   description:(NSString *)description
   520 {
   521 	NSString *requestID = [twitterEngine updateProfileName:name
   522 													 email:nil
   523 													   url:url
   524 												  location:location
   525 											   description:description];
   526 	
   527 	if (requestID) {
   528 		[self setRequestType:AITwitterProfileSelf
   529 				forRequestID:requestID
   530 			  withDictionary:nil];
   531 	}
   532 }
   533 
   534 #pragma mark OAuth
   535 /*!
   536  * @brief Should we store our password based on internal object ID?
   537  *
   538  * We only need to if we're using OAuth.
   539  */
   540 - (BOOL)useInternalObjectIDForPasswordName
   541 {
   542 	return self.useOAuth;
   543 }
   544 
   545 /*!
   546  * @brief Should we connect using OAuth?
   547  *
   548  * If enabled, the account view will display the OAuth setup. Basic authentication will not be used.
   549  */
   550 - (BOOL)useOAuth
   551 {
   552 	return YES;
   553 }
   554 
   555 /*!
   556  * @brief OAuth consumer key
   557  */
   558 - (NSString *)consumerKey
   559 {
   560 	return @"amjYVOrzKpKkkHAsdEaClA";
   561 }
   562 
   563 /*!
   564  * @brief OAuth secret key
   565  */
   566 - (NSString *)secretKey
   567 {
   568 	return @"kvqM2CQsUO3J6NHctJVhTOzlKZ0k7FsTaR5NwakYU";
   569 }
   570 
   571 /*!
   572  * @brief Token request URL
   573  */
   574 - (NSString *)tokenRequestURL
   575 {
   576 	return @"https://twitter.com/oauth/request_token";
   577 }
   578 
   579 /*!
   580  * @brief Token access URL
   581  */
   582 - (NSString *)tokenAccessURL
   583 {
   584 	return @"https://twitter.com/oauth/access_token";	
   585 }
   586 
   587 /*!
   588  * @brief Token authorize URL
   589  */
   590 - (NSString *)tokenAuthorizeURL
   591 {
   592 	return @"https://twitter.com/oauth/authorize";
   593 }
   594 
   595 #pragma mark Menu Items
   596 /*!
   597  * @brief Menu items for contact
   598  *
   599  * Returns an array of menu items for a contact on this account.  This is the best place to add protocol-specific
   600  * actions that aren't otherwise supported by Adium.
   601  * @param inContact AIListContact for menu items
   602  * @return NSArray of NSMenuItem instances for the passed contact
   603  */
   604 - (NSArray *)menuItemsForContact:(AIListContact *)inContact
   605 {
   606 	NSMutableArray *menuItemArray = [NSMutableArray array];
   607 	
   608 	NSMenuItem *menuItem;
   609 	
   610 	NSImage	*serviceIcon = [AIServiceIcons serviceIconForService:self.service
   611 															type:AIServiceIconSmall
   612 													   direction:AIIconNormal];
   613 	
   614 	menuItem = [[[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:[NSString stringWithFormat:AILocalizedString(@"Open %@'s user page",nil), inContact.UID]
   615 																	 target:self
   616 																	 action:@selector(openUserPage:)
   617 															  keyEquivalent:@""] autorelease];
   618 	[menuItem setImage:serviceIcon];
   619 	[menuItem setRepresentedObject:inContact];
   620 	[menuItemArray addObject:menuItem];	
   621 
   622 	menuItem = [[[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:[NSString stringWithFormat:AILocalizedString(@"Enable device notifications for %@",nil), inContact.UID]
   623 																	 target:self
   624 																	 action:@selector(enableOrDisableNotifications:)
   625 															  keyEquivalent:@""] autorelease];
   626 	[menuItem setTag:YES];
   627 	[menuItem setImage:serviceIcon];
   628 	[menuItem setRepresentedObject:inContact];
   629 	[menuItemArray addObject:menuItem];
   630 
   631 	menuItem = [[[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:[NSString stringWithFormat:AILocalizedString(@"Disable device notifications for %@",nil), inContact.UID]
   632 																	 target:self
   633 																	 action:@selector(enableOrDisableNotifications:)
   634 															  keyEquivalent:@""] autorelease];
   635 	[menuItem setTag:NO];
   636 	[menuItem setImage:serviceIcon];
   637 	[menuItem setRepresentedObject:inContact];
   638 	[menuItemArray addObject:menuItem];
   639 	
   640 	return menuItemArray;	
   641 }
   642 
   643 /*!
   644  * @brief Open the represented objec'ts user page
   645  */
   646 - (void)openUserPage:(NSMenuItem *)menuItem
   647 {
   648 	[[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:[self addressForLinkType:AITwitterLinkUserPage
   649 																				  userID:((AIListContact *)menuItem.representedObject).UID
   650 																				statusID:nil
   651 																				 context:nil]]];
   652 }
   653 
   654 /*!
   655  * @brief Enable or disable notifications for a contact.
   656  *
   657  * If the menuItem's tag is YES, we're adding. Otherwise we're removing.
   658  */
   659 - (void)enableOrDisableNotifications:(NSMenuItem *)menuItem
   660 {
   661 	if(![menuItem.representedObject isKindOfClass:[AIListContact class]]) {
   662 		return;
   663 	}
   664 
   665 	BOOL enableNotification = menuItem.tag;
   666 	AIListContact *contact = menuItem.representedObject;
   667 	
   668 	NSString *requestID = nil;
   669 	
   670 	BOOL initialFailure = NO;
   671 	
   672 	if (enableNotification) {
   673 		requestID = [twitterEngine enableNotificationsFor:contact.UID];
   674 
   675 		if (requestID) {
   676 			[self setRequestType:AITwitterNotificationEnable
   677 					forRequestID:requestID
   678 				  withDictionary:[NSDictionary dictionaryWithObjectsAndKeys:contact, @"ListContact", nil]];
   679 		} else {
   680 			initialFailure = YES;
   681 		}
   682 	
   683 	} else {
   684 		requestID = [twitterEngine disableNotificationsFor:contact.UID];
   685 		
   686 		if (requestID) {
   687 			[self setRequestType:AITwitterNotificationDisable
   688 					forRequestID:requestID
   689 				  withDictionary:[NSDictionary dictionaryWithObjectsAndKeys:contact, @"ListContact", nil]];
   690 		} else {
   691 			initialFailure = YES;
   692 		}
   693 	}
   694 	
   695 	if (initialFailure) {
   696 		[adium.interfaceController handleErrorMessage:(enableNotification ?
   697 														AILocalizedString(@"Unable to Enable Notifications", nil) :
   698 														AILocalizedString(@"Unable to Disable Notifications", nil))
   699 									  withDescription:AILocalizedString(@"Unable to connect to the Twitter server.", nil)];
   700 	}
   701 }
   702 
   703 /*!
   704  * @brief Menu items for chat
   705  *
   706  * Returns an array of menu items for a chat on this account.  This is the best place to add protocol-specific
   707  * actions that aren't otherwise supported by Adium.
   708  * @param inChat AIChat for menu items
   709  * @return NSArray of NSMenuItem instances for the passed contact
   710  */
   711 - (NSArray *)menuItemsForChat:(AIChat *)inChat
   712 {
   713 	NSMutableArray *menuItemArray = [NSMutableArray array];
   714 	
   715 	NSMenuItem *menuItem;
   716 	
   717 	NSImage	*serviceIcon = [AIServiceIcons serviceIconForService:self.service
   718 															type:AIServiceIconSmall
   719 													   direction:AIIconNormal];
   720 	
   721 	menuItem = [[[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:AILocalizedString(@"Update Tweets",nil)
   722 																	 target:self
   723 																	 action:@selector(periodicUpdate)
   724 															  keyEquivalent:@""] autorelease];
   725 	[menuItem setImage:serviceIcon];
   726 	[menuItemArray addObject:menuItem];
   727 	
   728 	menuItem = [[[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:AILocalizedString(@"Reply to a Tweet",nil)
   729 																	 target:self
   730 																	 action:@selector(replyToTweet)
   731 															  keyEquivalent:@""] autorelease];
   732 	[menuItem setImage:serviceIcon];
   733 	[menuItemArray addObject:menuItem];
   734 	
   735 	return menuItemArray;	
   736 }
   737 
   738 /*!
   739  * @brief Menu items for the account's actions
   740  *
   741  * Returns an array of menu items for account-specific actions.  This is the best place to add protocol-specific
   742  * actions that aren't otherwise supported by Adium.  It will only be queried if the account is online.
   743  * @return NSArray of NSMenuItem instances for this account
   744  */
   745 - (NSArray *)accountActionMenuItems
   746 {
   747 	NSMutableArray *menuItemArray = [NSMutableArray array];
   748 	
   749 	NSMenuItem *menuItem;
   750 	
   751 	menuItem = [[[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:AILocalizedString(@"Update Tweets",nil)
   752 																	target:self
   753 																	action:@selector(periodicUpdate)
   754 															 keyEquivalent:@""] autorelease];
   755 	[menuItemArray addObject:menuItem];
   756 
   757 	menuItem = [[[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:AILocalizedString(@"Reply to a Tweet",nil)
   758 																	 target:self
   759 																	 action:@selector(replyToTweet)
   760 															  keyEquivalent:@""] autorelease];
   761 	[menuItemArray addObject:menuItem];
   762 
   763 	menuItem = [[[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:AILocalizedString(@"Get Rate Limit Amount",nil)
   764 																	 target:self
   765 																	 action:@selector(getRateLimitAmount)
   766 															  keyEquivalent:@""] autorelease];
   767 	[menuItemArray addObject:menuItem];
   768 	
   769 	return menuItemArray;	
   770 }
   771 
   772 /*!
   773  * @brief Open the reply to tweet window
   774  *
   775  * Opens a window in which the user can create a reply featuring in_reply_to_status_id being set.
   776  */
   777 - (void)replyToTweet
   778 {
   779 	[AITwitterReplyWindowController showReplyWindowForAccount:self];
   780 }
   781 
   782 /*!
   783  * @brief Gets the current rate limit amount.
   784  */
   785 - (void)getRateLimitAmount
   786 {
   787 	NSString *requestID = [twitterEngine getRateLimitStatus];
   788 	
   789 	if (requestID) {
   790 		[self setRequestType:AITwitterRateLimitStatus
   791 				forRequestID:requestID
   792 			  withDictionary:nil];
   793 	}
   794 	
   795 }
   796 
   797 #pragma mark Contact handling
   798 /*!
   799  * @brief The name of our timeline chat
   800  */
   801 - (NSString *)timelineChatName
   802 {
   803 	return [NSString stringWithFormat:TWITTER_TIMELINE_NAME, self.UID];
   804 }
   805 
   806 /*!
   807  * @brief Our timeline chat
   808  *
   809  * If the timeline chat is not already active, it is created.
   810  */
   811 - (AIChat *)timelineChat
   812 {
   813 	AIChat *timelineChat = [adium.chatController existingChatWithName:self.timelineChatName
   814 							onAccount:self];
   815 	
   816 	if (!timelineChat) {
   817 		timelineChat = [adium.chatController chatWithName:self.timelineChatName
   818 						identifier:nil
   819 						onAccount:self
   820 						chatCreationInfo:nil];
   821 	}
   822 
   823 	return timelineChat;	
   824 }
   825 
   826 /*!
   827  * @brief Update the timeline chat
   828  * 
   829  * Remove the userlist
   830  */
   831 - (void)updateTimelineChat:(AIChat *)timelineChat
   832 {
   833 	// Disable the user list on the chat.
   834 	if (timelineChat.chatContainer.chatViewController.userListVisible) {
   835 		[timelineChat.chatContainer.chatViewController toggleUserList]; 
   836 	}	
   837 	
   838 	// Update the participant list.
   839 	[timelineChat addParticipatingListObjects:self.contacts notify:NotifyNow];
   840 	
   841 	[timelineChat setValue:[NSNumber numberWithInt:140] forProperty:@"Character Counter Max" notify:NotifyNow];
   842 }
   843 
   844 /*!
   845  * @brief Update serverside icon
   846  *
   847  * This is called by AIUserIcons when it needs an icon update for a contact.
   848  * If we already have an icon set (even a cached icon), ignore it.
   849  * Otherwise return the Twitter service icon.
   850  *
   851  * This is so that when an unknown contact appears, it has an actual image
   852  * to replace in the WKMV when an actual icon update is returned.
   853  *
   854  * This service icon will not remain saved very long, I see no harm in using it.
   855  * This only occurs for "strangers".
   856  */
   857 - (NSData *)serversideIconDataForContact:(AIListContact *)listContact
   858 {
   859 	if (![AIUserIcons userIconSourceForObject:listContact] &&
   860 		![AIUserIcons cachedUserIconDataForObject:listContact]) {
   861 		return [[self.service defaultServiceIconOfType:AIServiceIconLarge] TIFFRepresentation];
   862 	} else {
   863 		return nil;
   864 	}
   865 }
   866 
   867 /*!
   868  * @brief Update a user icon from a URL if necessary
   869  */
   870 - (void)updateUserIcon:(NSString *)url forContact:(AIListContact *)listContact;
   871 {
   872 	// If we don't already have an icon for the user...
   873 	if(![[listContact valueForProperty:TWITTER_PROPERTY_REQUESTED_USER_ICON] boolValue]) {
   874 		NSString *fileName = [[url lastPathComponent] stringByReplacingOccurrencesOfString:@"_normal." withString:@"_bigger."];
   875 		
   876 		url = [[url stringByDeletingLastPathComponent] stringByAppendingPathComponent:fileName];
   877 		
   878 		// Grab the user icon and set it as their serverside icon.
   879 		NSString *requestID = [twitterEngine getImageAtURL:url];
   880 		
   881 		if(requestID) {
   882 			[self setRequestType:AITwitterUserIconPull
   883 					forRequestID:requestID
   884 				  withDictionary:[NSDictionary dictionaryWithObject:listContact forKey:@"ListContact"]];
   885 		}
   886 		
   887 		[listContact setValue:[NSNumber numberWithBool:YES] forProperty:TWITTER_PROPERTY_REQUESTED_USER_ICON notify:NotifyNever];
   888 	}
   889 }
   890 
   891 /*!
   892  * @brief Unfollow the requested contacts.
   893  */
   894 - (void)removeContacts:(NSArray *)objects fromGroups:(NSArray *)groups
   895 {	
   896 	for (AIListContact *object in objects) {
   897 		NSString *requestID = [twitterEngine disableUpdatesFor:object.UID];
   898 		
   899 		AILogWithSignature(@"%@ Requesting unfollow for: %@", self, object.UID);
   900 		
   901 		if(requestID) {
   902 			[self setRequestType:AITwitterRemoveFollow
   903 					forRequestID:requestID
   904 				  withDictionary:[NSDictionary dictionaryWithObject:object forKey:@"ListContact"]];
   905 		}	
   906 	}
   907 }
   908 
   909 /*!
   910  * @brief Follow the requested contact, trigger an information pull for them.
   911  */
   912 - (void)addContact:(AIListContact *)contact toGroup:(AIListGroup *)group
   913 {
   914 	NSString	*requestID = [twitterEngine enableUpdatesFor:contact.UID];
   915 	
   916 	AILogWithSignature(@"%@ Requesting follow for: %@", self, contact.UID);
   917 	
   918 	if(requestID) {	
   919 		NSString	*updateRequestID = [twitterEngine getUserInformationFor:contact.UID];
   920 		
   921 		if (updateRequestID) {
   922 			[self setRequestType:AITwitterAddFollow
   923 					forRequestID:updateRequestID
   924 				  withDictionary:[NSDictionary dictionaryWithObjectsAndKeys:contact.UID, @"UID", nil]];
   925 		}
   926 	}
   927 }
   928 
   929 #pragma mark Request cataloguing
   930 /*!
   931  * @brief Set the type and optional dictionary for a request ID
   932  *
   933  * Sets the AITwitterRequestType for a particular request ID, so when the request finishes we can identify what it is for.
   934  * Optionally sets a dictionary which can be retrieved in association with the request type.
   935  */
   936 - (void)setRequestType:(AITwitterRequestType)type forRequestID:(NSString *)requestID withDictionary:(NSDictionary *)info
   937 {
   938 	[pendingRequests setObject:[NSDictionary dictionaryWithObjectsAndKeys:[NSNumber numberWithInt:type], @"Type",
   939 								info, @"Info", nil]
   940 						forKey:requestID];
   941 }
   942 
   943 /*!
   944  * @brief Get the request type for a request ID
   945  */
   946 - (AITwitterRequestType)requestTypeForRequestID:(NSString *)requestID
   947 {
   948 	return [(NSNumber *)[[pendingRequests objectForKey:requestID] objectForKey:@"Type"] intValue];
   949 }
   950 
   951 /*!
   952  * @brief Get the dictionary associated with a request ID
   953  */
   954 - (NSDictionary *)dictionaryForRequestID:(NSString *)requestID
   955 {
   956 	return (NSDictionary *)[[pendingRequests objectForKey:requestID] objectForKey:@"Info"];
   957 }
   958 
   959 /*!
   960  * @brief Remove a request ID's saved information.
   961  */
   962 - (void)clearRequestTypeForRequestID:(NSString *)requestID
   963 {
   964 	[pendingRequests removeObjectForKey:requestID];
   965 }
   966 
   967 #pragma mark Preference updating
   968 - (void)preferencesChangedForGroup:(NSString *)group key:(NSString *)key object:(AIListObject *)object
   969 					preferenceDict:(NSDictionary *)prefDict firstTime:(BOOL)firstTime
   970 {
   971 	[super preferencesChangedForGroup:group key:key object:object preferenceDict:prefDict firstTime:firstTime];
   972 	
   973 	// We only care about our changes.
   974 	if (object != self) {
   975 		return;
   976 	}
   977 	
   978 	if([group isEqualToString:GROUP_ACCOUNT_STATUS]) {
   979 		if([key isEqualToString:KEY_USER_ICON]) {
   980 			// Avoid pushing an icon update which we just downloaded.
   981 			if(![self boolValueForProperty:TWITTER_PROPERTY_REQUESTED_USER_ICON]) {
   982 				NSString *requestID = [twitterEngine updateProfileImage:[prefDict objectForKey:KEY_USER_ICON]];
   983 			
   984 				if(requestID) {
   985 					AILogWithSignature(@"%@ Pushing self icon update", self);
   986 					
   987 					[self setRequestType:AITwitterProfileSelf
   988 							forRequestID:requestID
   989 						  withDictionary:nil];
   990 				}
   991 			}
   992 			
   993 			[self setValue:nil forProperty:TWITTER_PROPERTY_REQUESTED_USER_ICON notify:NotifyNever];
   994 		}
   995 	}
   996 	
   997 	if([group isEqualToString:TWITTER_PREFERENCE_GROUP_UPDATES]) {
   998 		if(!firstTime && [key isEqualToString:TWITTER_PREFERENCE_UPDATE_INTERVAL]) {
   999 			NSTimeInterval timeInterval = [updateTimer timeInterval];
  1000 			NSTimeInterval newTimeInterval = [[prefDict objectForKey:TWITTER_PREFERENCE_UPDATE_INTERVAL] intValue] * 60;
  1001 			
  1002 			if (timeInterval != newTimeInterval && self.online) {
  1003 				[updateTimer invalidate]; updateTimer = nil;
  1004 				
  1005 				if(newTimeInterval > 0) {
  1006 					updateTimer = [NSTimer scheduledTimerWithTimeInterval:newTimeInterval
  1007 																   target:self
  1008 																 selector:@selector(periodicUpdate)
  1009 																 userInfo:nil
  1010 																  repeats:YES];
  1011 				}
  1012 			}
  1013 		}
  1014 		
  1015 		updateAfterSend = [[prefDict objectForKey:TWITTER_PREFERENCE_UPDATE_AFTER_SEND] boolValue];
  1016 		retweetLink = [[prefDict objectForKey:TWITTER_PREFERENCE_RETWEET_SPAM] boolValue];
  1017 		
  1018 		if ([key isEqualToString:TWITTER_PREFERENCE_LOAD_CONTACTS] && self.online) {
  1019 			if ([[prefDict objectForKey:TWITTER_PREFERENCE_LOAD_CONTACTS] boolValue]) {
  1020 				// Delay updates when loading our contacts list.
  1021 				[self silenceAllContactUpdatesForInterval:18.0];
  1022 				// Grab our user list.
  1023 				NSString	*requestID = [twitterEngine getRecentlyUpdatedFriendsFor:self.UID startingAtPage:1];
  1024 				
  1025 				if (requestID) {
  1026 					[self setRequestType:AITwitterInitialUserInfo
  1027 							forRequestID:requestID
  1028 						  withDictionary:[NSDictionary dictionaryWithObject:[NSNumber numberWithInt:1] forKey:@"Page"]];
  1029 				}
  1030 			} else {
  1031 				[self removeAllContacts];
  1032 			}
  1033 		}
  1034 	}	
  1035 }
  1036 
  1037 #pragma mark Periodic update scheduler
  1038 /*!
  1039  * @brief Trigger our periodic updates
  1040  */
  1041 - (void)periodicUpdate
  1042 {
  1043 	if (pendingUpdateCount) {
  1044 		AILogWithSignature(@"%@ Update already in progress. Count = %d", self, pendingUpdateCount);
  1045 		return;
  1046 	}
  1047 	
  1048 	NSString	*requestID;
  1049 	NSString	*lastID;
  1050 	
  1051 	// We haven't completed the timeline nor replies. This lets us know if we should display statuses.
  1052 	followedTimelineCompleted = repliesCompleted = NO;
  1053 	futureTimelineLastID = futureRepliesLastID = nil;
  1054 	
  1055 	// Prevent triggering this update routine multiple times.
  1056 	pendingUpdateCount = 3;
  1057 	
  1058 	// We haven't printed error messages for this set.
  1059 	timelineErrorMessagePrinted = NO;
  1060 	
  1061 	[queuedUpdates removeAllObjects];
  1062 	[queuedDM removeAllObjects];
  1063 	
  1064 	AILogWithSignature(@"%@ Periodic update fire", self);
  1065 	
  1066 	// Pull direct messages	
  1067 	lastID = [self preferenceForKey:TWITTER_PREFERENCE_DM_LAST_ID
  1068 							  group:TWITTER_PREFERENCE_GROUP_UPDATES];
  1069 	
  1070 	requestID = [twitterEngine getDirectMessagesSinceID:lastID startingAtPage:1];
  1071 	
  1072 	if (requestID) {
  1073 		[self setRequestType:AITwitterUpdateDirectMessage
  1074 				forRequestID:requestID
  1075 			  withDictionary:[NSDictionary dictionaryWithObjectsAndKeys:[NSNumber numberWithInt:1], @"Page", nil]];
  1076 	} else {
  1077 		--pendingUpdateCount;
  1078 	}
  1079 
  1080 	// Pull followed timeline
  1081 	lastID = [self preferenceForKey:TWITTER_PREFERENCE_TIMELINE_LAST_ID
  1082 							  group:TWITTER_PREFERENCE_GROUP_UPDATES];
  1083 
  1084 	requestID = [twitterEngine getFollowedTimelineFor:nil
  1085 											  sinceID:lastID
  1086 									   startingAtPage:1
  1087 												count:(lastID ? TWITTER_UPDATE_TIMELINE_COUNT : TWITTER_UPDATE_TIMELINE_COUNT_FIRST_RUN)];
  1088 	
  1089 	if (requestID) {
  1090 		[self setRequestType:AITwitterUpdateFollowedTimeline
  1091 				forRequestID:requestID
  1092 			  withDictionary:[NSDictionary dictionaryWithObjectsAndKeys:[NSNumber numberWithInt:1], @"Page", nil]];
  1093 	} else {
  1094 		--pendingUpdateCount;
  1095 	}
  1096 	
  1097 	// Pull the replies feed	
  1098 	lastID = [self preferenceForKey:TWITTER_PREFERENCE_REPLIES_LAST_ID
  1099 							  group:TWITTER_PREFERENCE_GROUP_UPDATES];
  1100 	
  1101 	requestID = [twitterEngine getRepliesSinceID:lastID startingAtPage:1];
  1102 	
  1103 	if (requestID) {
  1104 		[self setRequestType:AITwitterUpdateReplies
  1105 				forRequestID:requestID
  1106 			  withDictionary:[NSDictionary dictionaryWithObjectsAndKeys:[NSNumber numberWithInt:1], @"Page", nil]];
  1107 	} else {
  1108 		--pendingUpdateCount;
  1109 	}
  1110 }
  1111 
  1112 #pragma mark Message Display
  1113 /*!
  1114  * @brief Returns a user-readable message for an error code.
  1115  */
  1116 - (NSString *)errorMessageForError:(NSError *)error
  1117 {
  1118 	switch (error.code) {
  1119 		case 400:
  1120 			// Bad Request: your request is invalid, and we'll return an error message that tells you why.
  1121 			// This is the status code returned if you've exceeded the rate limit. 
  1122 			return AILocalizedString(@"You've exceeded the rate limit.", nil);
  1123 			break;
  1124 			
  1125 		case 401:
  1126 			// Not Authorized: either you need to provide authentication credentials, or the credentials provided aren't valid.
  1127 			return AILocalizedString(@"Your credentials do not allow you access.", nil);
  1128 			break;
  1129 			
  1130 		case 403:
  1131 			// Forbidden: we understand your request, but are refusing to fulfill it.  An accompanying error message should explain why.
  1132 			return AILocalizedString(@"Request refused by the server.", nil);
  1133 			break;
  1134 			
  1135 		case 404:
  1136 			// Not Found: either you're requesting an invalid URI or the resource in question doesn't exist (ex: no such user). 
  1137 			return AILocalizedString(@"Requested resource not found.", nil);
  1138 			break;
  1139 			
  1140 		case 500:
  1141 			// Internal Server Error: we did something wrong.  Please post to the group about it and the Twitter team will investigate.
  1142 			return AILocalizedString(@"The server reported an internal error.", nil);
  1143 			break;
  1144 			
  1145 		case 502:
  1146 			// Bad Gateway: returned if Twitter is down or being upgraded.
  1147 			return AILocalizedString(@"The server is currently down.", nil);
  1148 			break;
  1149 			
  1150 		case -1001:
  1151 			// Timeout
  1152 		case 503:
  1153 			// Service Unavailable: the Twitter servers are up, but are overloaded with requests.  Try again later.
  1154 			return AILocalizedString(@"The server is overloaded with requests.", nil);
  1155 			break;
  1156 			
  1157 	}
  1158 	
  1159 	return [NSString stringWithFormat:AILocalizedString(@"Unknown error: code %d, %@", nil), error.code, error.localizedDescription];
  1160 }
  1161 
  1162 /*!
  1163  * @brief Returns the link URL for a specific type of link
  1164  */
  1165 - (NSString *)addressForLinkType:(AITwitterLinkType)linkType
  1166 						  userID:(NSString *)userID
  1167 						statusID:(NSString *)statusID
  1168 						 context:(NSString *)context
  1169 {
  1170 	NSString *address = nil;
  1171 	
  1172 	if (linkType == AITwitterLinkStatus) {
  1173 		address = [NSString stringWithFormat:@"https://twitter.com/%@/status/%@", userID, statusID];
  1174 	} else if (linkType == AITwitterLinkFriends) {
  1175 		address = [NSString stringWithFormat:@"https://twitter.com/%@/friends", userID];
  1176 	} else if (linkType == AITwitterLinkFollowers) {
  1177 		address = [NSString stringWithFormat:@"https://twitter.com/%@/followers", userID]; 
  1178 	} else if (linkType == AITwitterLinkUserPage) {
  1179 		address = [NSString stringWithFormat:@"https://twitter.com/%@", userID]; 
  1180 	} else if (linkType == AITwitterLinkSearchHash) {
  1181 		address = [NSString stringWithFormat:@"http://search.twitter.com/search?q=%%23%@", context];
  1182 	} else if (linkType == AITwitterLinkReply) {
  1183 		address = [NSString stringWithFormat:@"twitterreply://%@@%@?action=reply&status=%@", self.internalObjectID, userID, statusID];
  1184 	} else if (linkType == AITwitterLinkRetweet) {
  1185 		address = [NSString stringWithFormat:@"twitterreply://%@@%@?action=retweet&status=%@&message=%@", self.internalObjectID, userID, statusID, context];
  1186 	} else if (linkType == AITwitterLinkFavorite) {
  1187 		address = [NSString stringWithFormat:@"twitterreply://%@@%@?action=favorite&status=%@", self.internalObjectID, userID, statusID];
  1188 	} else if (linkType == AITwitterLinkDestroyStatus) {
  1189 		address = [NSString stringWithFormat:@"twitterreply://%@@%@?action=destroy&status=%@&message=%@", self.internalObjectID, userID, statusID, context];
  1190 	} else if (linkType == AITwitterLinkDestroyDM) {
  1191 		address = [NSString stringWithFormat:@"twitterreply://%@@%@?action=destroy&dm=%@&message=%@", self.internalObjectID, userID, statusID, context];		
  1192 	}
  1193 	
  1194 	return address;
  1195 }
  1196 
  1197 /*!
  1198  * @brief Toggle the favorite status for a tweet.
  1199  *
  1200  * Attempts to favorite a tweet. If that fails, it removes favorite status.
  1201  * Prints a status message in the chat on success/failure, since it's otherwise not obvious.
  1202  */
  1203 - (void)toggleFavoriteTweet:(NSString *)tweetID
  1204 {
  1205 	NSString *requestID = [twitterEngine markUpdate:tweetID asFavorite:YES];
  1206 	
  1207 	if (requestID) {
  1208 		[self setRequestType:AITwitterFavoriteYes
  1209 				forRequestID:requestID
  1210 			  withDictionary:[NSDictionary dictionaryWithObjectsAndKeys:tweetID, @"tweetID", nil]];
  1211 	} else {
  1212 		AIChat *timelineChat = self.timelineChat;
  1213 		
  1214 		[adium.contentController displayEvent:AILocalizedString(@"Attempt to favorite tweet failed to connect.", nil)
  1215 									   ofType:@"favorite"
  1216 									   inChat:timelineChat];
  1217 	}
  1218 }
  1219 
  1220 /*!
  1221  * @brief Destroy the tweet.
  1222  *
  1223  * The user has already confirmed they want to destroy it; send the message.
  1224  */
  1225 - (void)destroyTweet:(NSString *)tweetID
  1226 {
  1227 	NSString *requestID = [twitterEngine deleteUpdate:tweetID];
  1228 	
  1229 	if(requestID) {
  1230 		[self setRequestType:AITwitterDestroyStatus
  1231 				forRequestID:requestID
  1232 			  withDictionary:nil];
  1233 	} else {
  1234 		AIChat *timelineChat = self.timelineChat;
  1235 		
  1236 		[adium.contentController displayEvent:AILocalizedString(@"Attempt to delete tweet failed to connect.", nil)
  1237 									   ofType:@"delete"
  1238 									   inChat:timelineChat];
  1239 	}
  1240 }
  1241 
  1242 /*!
  1243  * @brief Destroy the DM.
  1244  *
  1245  * The user has already confirmed they want to destroy it; send the message.
  1246  */
  1247 - (void)destroyDirectMessage:(NSString *)messageID
  1248 					 forUser:(NSString *)userID
  1249 {
  1250 	NSString *requestID = [twitterEngine deleteDirectMessage:messageID];
  1251 	AIListContact *contact = [self contactWithUID:userID];
  1252 	
  1253 	if(requestID) {
  1254 		[self setRequestType:AITwitterDestroyDM
  1255 				forRequestID:requestID
  1256 			  withDictionary:[NSDictionary dictionaryWithObject:contact forKey:@"ListContact"]];
  1257 	} else {
  1258 		AIChat *chat = [adium.chatController chatWithContact:contact];
  1259 		
  1260 		[adium.contentController displayEvent:AILocalizedString(@"Attempt to delete tweet failed to connect.", nil)
  1261 									   ofType:@"delete"
  1262 									   inChat:chat];
  1263 	}	
  1264 }
  1265 
  1266 /*!
  1267  * @brief Convert a link URL and name into an attributed link
  1268  *
  1269  * @param label The text to display for the link.
  1270  * @param destination The destination address for the link.
  1271  * @param attributeName The name of the twitter link attribute for HTML processing.
  1272  */
  1273 - (NSAttributedString *)attributedStringWithLinkLabel:(NSString *)label
  1274 									  linkDestination:(NSString *)destination
  1275 											linkClass:(NSString *)className
  1276 {
  1277 	NSURL *url = [NSURL URLWithString:destination];
  1278 	NSDictionary *attributes = [NSDictionary dictionaryWithObjectsAndKeys:
  1279 								url, NSLinkAttributeName,
  1280 								className, AIElementClassAttributeName, nil];
  1281 	
  1282 	return [[[NSAttributedString alloc] initWithString:label attributes:attributes] autorelease];
  1283 }
  1284 
  1285 /*!
  1286  * @brief Parse an attributed string into a linkified version.
  1287  */
  1288 - (NSAttributedString *)linkifiedAttributedStringFromString:(NSAttributedString *)inString
  1289 {	
  1290 	NSAttributedString *attributedString;
  1291 	
  1292 	static NSCharacterSet *usernameCharacters = nil;
  1293 	static NSCharacterSet *hashCharacters = nil;
  1294 	
  1295 	if (!usernameCharacters) {
  1296 		usernameCharacters = [[NSCharacterSet characterSetWithCharactersInString:@"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_"] retain];
  1297 	}
  1298 	
  1299 	if (!hashCharacters) {
  1300 		NSMutableCharacterSet	*disallowedCharacters = [[NSCharacterSet punctuationCharacterSet] mutableCopy];
  1301 		[disallowedCharacters formUnionWithCharacterSet:[NSCharacterSet whitespaceCharacterSet]];
  1302 		
  1303 		hashCharacters = [[disallowedCharacters invertedSet] retain];
  1304 		
  1305 		[disallowedCharacters release];
  1306 	}
  1307 	
  1308 	attributedString = [AITwitterURLParser linkifiedStringFromAttributedString:inString
  1309 															forPrefixCharacter:@"@"
  1310 																   forLinkType:AITwitterLinkUserPage
  1311 																	forAccount:self
  1312 															 validCharacterSet:usernameCharacters];
  1313 	
  1314 	attributedString = [AITwitterURLParser linkifiedStringFromAttributedString:attributedString
  1315 															forPrefixCharacter:@"#"
  1316 																   forLinkType:AITwitterLinkSearchHash
  1317 																	forAccount:self
  1318 															 validCharacterSet:hashCharacters];
  1319 	
  1320 	return attributedString;
  1321 }
  1322 
  1323 /*!
  1324  * @brief Parses a Twitter message into an attributed string
  1325  */
  1326 - (NSAttributedString *)parseMessage:(NSString *)inMessage
  1327 							 tweetID:(NSString *)tweetID
  1328 							  userID:(NSString *)userID
  1329 					   inReplyToUser:(NSString *)replyUserID
  1330 					inReplyToTweetID:(NSString *)replyTweetID
  1331 {
  1332 	NSAttributedString *message;
  1333 	
  1334 	message = [NSAttributedString stringWithString:[inMessage stringByUnescapingFromXMLWithEntities:nil]];
  1335 	
  1336 	message = [self linkifiedAttributedStringFromString:message];
  1337 	
  1338 	BOOL replyTweet = (replyTweetID.length);
  1339 	BOOL tweetLink = (tweetID.length && userID.length);
  1340 	
  1341 	if (replyTweet || tweetLink) {
  1342 		NSMutableAttributedString *mutableMessage = [[message mutableCopy] autorelease];
  1343 		
  1344 		NSUInteger startIndex = message.length;
  1345 		
  1346 		[mutableMessage appendString:@"  (" withAttributes:nil];
  1347 	
  1348 		BOOL commaNeeded = NO;
  1349 		
  1350 		// Append a link to the tweet this is in reply to
  1351 		if (replyTweet) {
  1352 			NSString *linkAddress = [self addressForLinkType:AITwitterLinkStatus
  1353 													  userID:replyUserID
  1354 													statusID:replyTweetID
  1355 													 context:nil];
  1356 
  1357 			if([inMessage hasPrefix:@"@"] &&
  1358 			   inMessage.length >= replyUserID.length + 1 &&
  1359 			   [replyUserID isCaseInsensitivelyEqualToString:[inMessage substringWithRange:NSMakeRange(1, replyUserID.length)]]) {
  1360 				// If the message has a "@" prefix, it's a proper in_reply_to_status_id if the usernames match. Set a link appropriately.
  1361 				[mutableMessage setAttributes:[NSDictionary dictionaryWithObjectsAndKeys:linkAddress, NSLinkAttributeName, nil]
  1362 										range:NSMakeRange(0, replyUserID.length + 1)];
  1363 			} else {
  1364 				// This happens for mentions which are in_reply_to_status_id but the @target isn't the first part of the message.
  1365 				
  1366 				[mutableMessage appendAttributedString:[self attributedStringWithLinkLabel:AILocalizedString(@"IRT", "An abbreviation for 'in reply to' - placed at the beginning of the tweet tools for those which are directly in reply to another")
  1367 																		   linkDestination:linkAddress
  1368 																				 linkClass:AITwitterInReplyToClassName]];
  1369 				
  1370 				commaNeeded = YES;	
  1371 			}
  1372 		}
  1373 		
  1374 		// Append a link to reply to this tweet
  1375 		if (tweetLink) {
  1376 			NSString *linkAddress;
  1377 			
  1378 			if(![self.UID isCaseInsensitivelyEqualToString:userID]) {
  1379 				// A message from someone other than ourselves. RT and @ is permissible.
  1380 				if (retweetLink) {				
  1381 					if(commaNeeded) {
  1382 						[mutableMessage appendString:@", " withAttributes:nil];
  1383 					}
  1384 					
  1385 					linkAddress = [self addressForLinkType:AITwitterLinkRetweet
  1386 													userID:userID
  1387 												  statusID:tweetID
  1388 												   context:[inMessage stringByAddingPercentEscapesForAllCharacters]];
  1389 					
  1390 					[mutableMessage appendAttributedString:[self attributedStringWithLinkLabel:@"RT"
  1391 																			   linkDestination:linkAddress
  1392 																					 linkClass:AITwitterRetweetClassName]];
  1393 					commaNeeded = YES;
  1394 				}
  1395 				
  1396 				if (commaNeeded) {
  1397 					[mutableMessage appendString:@", " withAttributes:nil];
  1398 				}			
  1399 				
  1400 				linkAddress = [self addressForLinkType:AITwitterLinkReply
  1401 												userID:userID
  1402 											  statusID:tweetID
  1403 											   context:nil];
  1404 				
  1405 				[mutableMessage appendAttributedString:[self attributedStringWithLinkLabel:@"@"
  1406 																		   linkDestination:linkAddress
  1407 																				 linkClass:AITwitterReplyClassName]];
  1408 			} else {
  1409 				if(commaNeeded) {
  1410 					[mutableMessage appendString:@", " withAttributes:nil];
  1411 				}
  1412 				
  1413 				// Our own message. Display a destroy link.
  1414 				linkAddress = [self addressForLinkType:AITwitterLinkDestroyStatus
  1415 												userID:userID
  1416 											  statusID:tweetID
  1417 											   context:[inMessage stringByAddingPercentEscapesForAllCharacters]];
  1418 				
  1419 				[mutableMessage appendAttributedString:[self attributedStringWithLinkLabel:@"\u232B"
  1420 																		   linkDestination:linkAddress
  1421 																				 linkClass:AITwitterDeleteClassName]];
  1422 			}
  1423 			
  1424 			[mutableMessage appendString:@", " withAttributes:nil];
  1425 
  1426 			linkAddress = [self addressForLinkType:AITwitterLinkFavorite
  1427 											userID:userID
  1428 										  statusID:tweetID
  1429 										   context:nil];
  1430 
  1431 			[mutableMessage appendAttributedString:[self attributedStringWithLinkLabel:@"\u2606"
  1432 																	   linkDestination:linkAddress
  1433 																			 linkClass:AITwitterFavoriteClassName]];
  1434 
  1435 			[mutableMessage appendString:@", " withAttributes:nil];
  1436 			
  1437 			linkAddress = [self addressForLinkType:AITwitterLinkStatus
  1438 											userID:userID
  1439 										  statusID:tweetID
  1440 										   context:nil];
  1441 			
  1442 			[mutableMessage appendAttributedString:[self attributedStringWithLinkLabel:@"#"
  1443 																	   linkDestination:linkAddress
  1444 																			 linkClass:AITwitterStatusLinkClassName]];
  1445 
  1446 		}
  1447 	
  1448 		[mutableMessage appendString:@")" withAttributes:nil];
  1449 		
  1450 		[mutableMessage addAttributes:[NSDictionary dictionaryWithObjectsAndKeys:
  1451 									   [NSNumber numberWithBool:YES], AITwitterActionLinksAttributeName,
  1452 									   [NSNumber numberWithBool:YES], AIHiddenMessagePartAttributeName, nil]
  1453 								range:NSMakeRange(startIndex, mutableMessage.length - startIndex)];
  1454 	
  1455 		return mutableMessage;
  1456 	} else {
  1457 		return message;
  1458 	}
  1459 }
  1460 
  1461 /*!
  1462  * @brief Parse a direct message
  1463  */
  1464 - (NSAttributedString *)parseDirectMessage:(NSString *)inMessage
  1465 									withID:(NSString *)dmID
  1466 								  fromUser:(NSString *)sourceUID
  1467 {
  1468 	NSAttributedString *message;
  1469 	
  1470 	message = [NSAttributedString stringWithString:[inMessage stringByUnescapingFromXMLWithEntities:nil]];
  1471 	
  1472 	message = [self linkifiedAttributedStringFromString:message];
  1473 	
  1474 	NSMutableAttributedString *mutableMessage = [[message mutableCopy] autorelease];
  1475 	
  1476 	NSUInteger startIndex = message.length;
  1477 	
  1478 	[mutableMessage appendString:@"  (" withAttributes:nil];
  1479 	
  1480 	NSString *linkAddress = [self addressForLinkType:AITwitterLinkDestroyDM
  1481 											  userID:sourceUID
  1482 											statusID:dmID
  1483 											 context:[inMessage stringByAddingPercentEscapesForAllCharacters]];
  1484 	
  1485 	[mutableMessage appendAttributedString:[self attributedStringWithLinkLabel:@"\u232B"
  1486 															   linkDestination:linkAddress
  1487 																	 linkClass:AITwitterDeleteClassName]];
  1488 	
  1489 	[mutableMessage appendString:@")" withAttributes:nil];
  1490 	
  1491 	[mutableMessage addAttributes:[NSDictionary dictionaryWithObjectsAndKeys:
  1492 								   [NSNumber numberWithBool:YES], AITwitterActionLinksAttributeName,
  1493 								   [NSNumber numberWithBool:YES], AIHiddenMessagePartAttributeName, nil]
  1494 							range:NSMakeRange(startIndex, mutableMessage.length - startIndex)];
  1495 	
  1496 	return mutableMessage;
  1497 }
  1498 
  1499 
  1500 /*!
  1501  * @brief Sort status updates
  1502  */
  1503 NSInteger queuedUpdatesSort(id update1, id update2, void *context)
  1504 {
  1505 	return [[update1 objectForKey:TWITTER_STATUS_CREATED] compare:[update2 objectForKey:TWITTER_STATUS_CREATED]];
  1506 }
  1507 
  1508 /*!
  1509  * @brief Sort direct messages
  1510  */
  1511 NSInteger queuedDMSort(id dm1, id dm2, void *context)
  1512 {
  1513 	return [[dm1 objectForKey:TWITTER_DM_CREATED] compare:[dm2 objectForKey:TWITTER_DM_CREATED]];	
  1514 }
  1515 
  1516 /*!
  1517  * @brief Remove duplicate status updates.
  1518  *
  1519  * If we're following someone who replies to us, we'll receive a status update in both the
  1520  * timeline and the reply feed.
  1521  *
  1522  * @param inArray The sorted array of Tweets
  1523  */
  1524 - (NSArray *)arrayWithDuplicateTweetsRemoved:(NSArray *)inArray
  1525 {
  1526 	NSMutableArray *mutableArray = [inArray mutableCopy];
  1527 	
  1528 	NSDictionary *status = nil, *previousStatus = nil;
  1529 	
  1530 	// Starting at index 1, checking backwards. We'll never exceed bounds this way.
  1531 	for(NSUInteger index = 1; index < inArray.count; index++)
  1532 	{
  1533 		status = [inArray objectAtIndex:index];
  1534 		previousStatus = [inArray objectAtIndex:index-1];
  1535 		
  1536 		if([[status objectForKey:TWITTER_STATUS_ID] isEqualToString:[previousStatus objectForKey:TWITTER_STATUS_ID]]) {
  1537 			[mutableArray removeObject:status];
  1538 		}
  1539 	}
  1540 	
  1541 	return [mutableArray autorelease];
  1542 }
  1543 
  1544 /*!
  1545  * @brief Display queued updates or direct messages
  1546  *
  1547  * This could potentially be simplified since both DMs and updates have the same format.
  1548  */
  1549 - (void)displayQueuedUpdatesForRequestType:(AITwitterRequestType)requestType
  1550 {
  1551 	if(requestType == AITwitterUpdateReplies || requestType == AITwitterUpdateFollowedTimeline) {
  1552 		if(!queuedUpdates.count) {
  1553 			return;
  1554 		}
  1555 		
  1556 		AILogWithSignature(@"%@ Displaying %d updates", self, queuedUpdates.count);
  1557 		
  1558 		// Sort the queued updates (since we're intermingling pages of data from different souces)
  1559 		NSArray *sortedQueuedUpdates = [queuedUpdates sortedArrayUsingFunction:queuedUpdatesSort context:nil];
  1560 		
  1561 		sortedQueuedUpdates = [self arrayWithDuplicateTweetsRemoved:sortedQueuedUpdates];
  1562 		
  1563 		BOOL trackContent = [[self preferenceForKey:TWITTER_PREFERENCE_EVER_LOADED_TIMELINE group:TWITTER_PREFERENCE_GROUP_UPDATES] boolValue];
  1564 		
  1565 		AIChat *timelineChat = self.timelineChat;
  1566 		
  1567 		[[AIContactObserverManager sharedManager] delayListObjectNotifications];
  1568 		
  1569 		for (NSDictionary *status in sortedQueuedUpdates) {
  1570 			NSDate			*date = [status objectForKey:TWITTER_STATUS_CREATED];
  1571 			NSString		*text = [status objectForKey:TWITTER_STATUS_TEXT];
  1572 			
  1573 			NSString *contactUID = [[status objectForKey:TWITTER_STATUS_USER] objectForKey:TWITTER_STATUS_UID];
  1574 			
  1575 			id fromObject = nil;
  1576 			
  1577 			if(![self.UID isCaseInsensitivelyEqualToString:contactUID]) {
  1578 				AIListContact *listContact = [self contactWithUID:contactUID];
  1579 				
  1580 				// Update the user's status message
  1581 				[listContact setStatusMessage:[NSAttributedString stringWithString:[text stringByUnescapingFromXMLWithEntities:nil]]
  1582 									   notify:NotifyNow];
  1583 				
  1584 				[self updateUserIcon:[[status objectForKey:TWITTER_STATUS_USER] objectForKey:TWITTER_INFO_ICON] forContact:listContact];
  1585 				
  1586 				[timelineChat addParticipatingListObject:listContact notify:NotifyNow];
  1587 				
  1588 				fromObject = (id)listContact;
  1589 			} else {
  1590 				fromObject = (id)self;
  1591 			}
  1592 
  1593 			NSAttributedString *message = [self parseMessage:text
  1594 													 tweetID:[status objectForKey:TWITTER_STATUS_ID]
  1595 													  userID:contactUID
  1596 											   inReplyToUser:[status objectForKey:TWITTER_STATUS_REPLY_UID]
  1597 											inReplyToTweetID:[status objectForKey:TWITTER_STATUS_REPLY_ID]];
  1598 			
  1599 			AIContentMessage *contentMessage = [AIContentMessage messageInChat:timelineChat
  1600 																	withSource:fromObject
  1601 																   destination:self
  1602 																		  date:date
  1603 																	   message:message
  1604 																	 autoreply:NO];
  1605 			
  1606 			contentMessage.trackContent = trackContent;
  1607 			
  1608 			[adium.contentController receiveContentObject:contentMessage];
  1609 		}
  1610 		
  1611 		[[AIContactObserverManager sharedManager] endListObjectNotificationsDelay];
  1612 		
  1613 		[queuedUpdates removeAllObjects];
  1614 	} else if (requestType == AITwitterUpdateDirectMessage || requestType == AITwitterDirectMessageSend) {
  1615 		NSMutableArray **unsortedArray = (requestType == AITwitterUpdateDirectMessage) ? &queuedDM : &queuedOutgoingDM;
  1616 		
  1617 		if (!(*unsortedArray).count) {
  1618 			return;
  1619 		}
  1620 		
  1621 		AILogWithSignature(@"%@ Displaying %d DMs", self, queuedDM.count);
  1622 		
  1623 		NSArray *sortedQueuedDM = [*unsortedArray sortedArrayUsingFunction:queuedDMSort context:nil];
  1624 		
  1625 		for (NSDictionary *message in sortedQueuedDM) {
  1626 			NSDate			*date = [message objectForKey:TWITTER_DM_CREATED];
  1627 			NSString		*text = [message objectForKey:TWITTER_DM_TEXT];
  1628 			NSString		*fromUID = [message objectForKey:TWITTER_DM_SENDER_UID];
  1629 			NSString		*toUID = [message objectForKey:TWITTER_DM_RECIPIENT_UID];
  1630 			
  1631 			AIListObject *source = nil, *destination = nil;
  1632 			AIChat *chat = nil;
  1633 			
  1634 			if([self.UID isCaseInsensitivelyEqualToString:fromUID]) {
  1635 				// This is a message we sent; display as coming from us.
  1636 				source = self;
  1637 				destination = [self contactWithUID:toUID];
  1638 				chat = [adium.chatController chatWithContact:(AIListContact *)destination];
  1639 			} else {
  1640 				source = [self contactWithUID:fromUID];
  1641 				destination = self;
  1642 				chat = [adium.chatController chatWithContact:(AIListContact *)source];
  1643 			}
  1644 			
  1645 			if(chat && source && destination) {
  1646 				AIContentMessage *contentMessage = [AIContentMessage messageInChat:chat
  1647 																		withSource:source
  1648 																	   destination:destination
  1649 																			  date:date
  1650 																		   message:[self parseDirectMessage:text
  1651 																									 withID:[message objectForKey:TWITTER_DM_ID]
  1652 																								   fromUser:chat.listObject.UID]
  1653 																		 autoreply:NO];
  1654 				
  1655 				[adium.contentController receiveContentObject:contentMessage];
  1656 			}
  1657 		}
  1658 		
  1659 		[*unsortedArray removeAllObjects];
  1660 	}
  1661 }
  1662 
  1663 #pragma mark MGTwitterEngine Delegate Methods
  1664 /*!
  1665  * @brief A request was successful
  1666  *
  1667  * We only care about requests succeeding if they aren't specifically handled in another location.
  1668  */
  1669 - (void)requestSucceeded:(NSString *)identifier
  1670 {
  1671 	// If a request succeeds and we think we're offline, call ourselves online.
  1672 	if ([self requestTypeForRequestID:identifier] == AITwitterDisconnect) {
  1673 		[self didDisconnect];
  1674 	} else if ([self requestTypeForRequestID:identifier] == AITwitterRemoveFollow) {
  1675 		AIListContact *listContact = [[self dictionaryForRequestID:identifier] objectForKey:@"ListContact"];
  1676 		
  1677 		for (NSString *groupName in listContact.remoteGroupNames) {
  1678 			[listContact removeRemoteGroupName:groupName];
  1679 		}
  1680 	} else if ([self requestTypeForRequestID:identifier] == AITwitterDestroyStatus) {
  1681 		AIChat *timelineChat = self.timelineChat;
  1682 		
  1683 		[adium.contentController displayEvent:AILocalizedString(@"Your tweet has been successfully deleted.", nil)
  1684 									  ofType:@"delete"
  1685 									  inChat:timelineChat];
  1686 	} else if ([self requestTypeForRequestID:identifier] == AITwitterDestroyDM) {
  1687 		AIListContact *contact = [[self dictionaryForRequestID:identifier] objectForKey:@"ListContact"];
  1688 		AIChat *chat = [adium.chatController chatWithContact:contact];
  1689 		
  1690 		[adium.contentController displayEvent:AILocalizedString(@"The direct message has been successfully deleted.", nil)
  1691 									   ofType:@"delete"
  1692 									   inChat:chat];		
  1693 	}
  1694 }
  1695 
  1696 /*!
  1697  * @brief A request failed
  1698  *
  1699  * If it's a fatal error, we need to kill the session and retry. Otherwise, twitter's reliability is
  1700  * pretty terrible, so let's ignore errors for the most part.
  1701  */
  1702 - (void)requestFailed:(NSString *)identifier withError:(NSError *)error
  1703 {	
  1704 	switch ([self requestTypeForRequestID:identifier]) {
  1705 		case AITwitterDirectMessageSend:
  1706 		case AITwitterSendUpdate:
  1707 		{
  1708 			AIChat	*chat = [[self dictionaryForRequestID:identifier] objectForKey:@"Chat"];
  1709 			
  1710 			if (chat) {
  1711 				[chat receivedError:[NSNumber numberWithInt:AIChatUnknownError]];
  1712 				
  1713 				AILogWithSignature(@"%@ Chat send error on %@", self, chat);
  1714 			}
  1715 			break;
  1716 		}
  1717 			
  1718 		case AITwitterDisconnect:
  1719 			[self didDisconnect];
  1720 			break;
  1721 			
  1722 		case AITwitterInitialUserInfo:
  1723 			[self setLastDisconnectionError:AILocalizedString(@"Unable to retrieve user list [fail]", "Message when a (vital) twitter request to retrieve the follow list fails")];
  1724 			[self didDisconnect];
  1725 			break;
  1726 			
  1727 		case AITwitterUserIconPull:
  1728 		{
  1729 			AIListContact *listContact = [[self dictionaryForRequestID:identifier] objectForKey:@"ListContact"];
  1730 			
  1731 			// Image pull failed, flag ourselves as needing to try again.
  1732 			[listContact setValue:nil forProperty:TWITTER_PROPERTY_REQUESTED_USER_ICON notify:NotifyNever];
  1733 			break;
  1734 		}
  1735 			
  1736 		case AITwitterUpdateFollowedTimeline:
  1737 		case AITwitterUpdateReplies:
  1738 		{
  1739 			AIChat *timelineChat = [adium.chatController existingChatWithName:self.timelineChatName
  1740 																	onAccount:self];
  1741 			
  1742 			// Only print an error if the user already has the timeline open. Beyond annoying if we pop it open just to say "lol error"
  1743 			if (timelineChat && !timelineErrorMessagePrinted) {
  1744 				AIContentEvent *content = [AIContentEvent eventInChat:timelineChat
  1745 														   withSource:nil
  1746 														  destination:self
  1747 																 date:[NSDate date]
  1748 															  message:[NSAttributedString stringWithString:[NSString stringWithFormat:AILocalizedString(@"Unable to update timeline: %@", nil),
  1749 																											[self errorMessageForError:error]]]
  1750 															 withType:@"error"];
  1751 				
  1752 				content.postProcessContent = NO;
  1753 				content.coalescingKey = @"error";
  1754 				
  1755 				[adium.contentController receiveContentObject:content];
  1756 				
  1757 				// This gets reset to NO the next a periodic update fires.
  1758 				timelineErrorMessagePrinted = YES;
  1759 			}
  1760 			
  1761 			--pendingUpdateCount;
  1762 			break;
  1763 		}
  1764 			
  1765 		case AITwitterUpdateDirectMessage:
  1766 			--pendingUpdateCount;
  1767 			break;
  1768 			
  1769 		case AITwitterAddFollow:
  1770 			if(error.code == 404) {
  1771 				[adium.interfaceController handleErrorMessage:AILocalizedString(@"Unable to Add Contact", nil)
  1772 											  withDescription:[NSString stringWithFormat:AILocalizedString(@"Unable to add %@ to account %@, the user does not exist.", nil),
  1773 															   [[self dictionaryForRequestID:identifier] objectForKey:@"UID"],
  1774 															   self.explicitFormattedUID]];
  1775 			} else {
  1776 				[adium.interfaceController handleErrorMessage:AILocalizedString(@"Unable to Add Contact", nil)
  1777 											  withDescription:[NSString stringWithFormat:AILocalizedString(@"Unable to add %@ to account %@. %@",nil),
  1778 															   [[self dictionaryForRequestID:identifier] objectForKey:@"UID"],
  1779 															   self.explicitFormattedUID,
  1780 															   [self errorMessageForError:error]]];
  1781 			}
  1782 			break;
  1783 			
  1784 		case AITwitterRemoveFollow:
  1785 			[adium.interfaceController handleErrorMessage:AILocalizedString(@"Unable to Remove Contact", nil)
  1786 										  withDescription:[NSString stringWithFormat:AILocalizedString(@"Unable to remove %@ on account %@. %@", nil),
  1787 														   ((AIListContact *)[[self dictionaryForRequestID:identifier] objectForKey:@"ListContact"]).UID,
  1788 														   self.explicitFormattedUID,
  1789 														   [self errorMessageForError:error]]];
  1790 			break;
  1791 			
  1792 		case AITwitterValidateCredentials:
  1793 			if(error.code == 401) {	
  1794 				if(self.useOAuth) {
  1795 					[self setPasswordTemporarily:nil];
  1796 					[self setLastDisconnectionError:TWITTER_OAUTH_NOT_AUTHORIZED];
  1797 					
  1798 					[[NSNotificationCenter defaultCenter] postNotificationName:@"AIEditAccount"
  1799 																		object:self];
  1800 					
  1801 				} else {
  1802 					[self setLastDisconnectionError:TWITTER_INCORRECT_PASSWORD_MESSAGE];
  1803 					[self serverReportedInvalidPassword];
  1804 				}
  1805 				
  1806 				[self didDisconnect];
  1807 			} else {
  1808 				[self setLastDisconnectionError:AILocalizedString(@"Unable to validate credentials", nil)];
  1809 				[self didDisconnect];
  1810 			}
  1811 			break;
  1812 			
  1813 		case AITwitterFavoriteYes:
  1814 		case AITwitterFavoriteNo:
  1815 		{
  1816 			AIChat *timelineChat = self.timelineChat;
  1817 
  1818 			if (error.code == 403) {
  1819 				// We've attempted to add or remove when we already have it marked as such. Try the opposite.
  1820 				BOOL addAsFavorite = ([self requestTypeForRequestID:identifier] == AITwitterFavoriteNo);
  1821 				NSString *tweetID = [[self dictionaryForRequestID:identifier] objectForKey:@"tweetID"];
  1822 				
  1823 				NSString *requestID = [twitterEngine markUpdate:tweetID
  1824 													 asFavorite:addAsFavorite];
  1825 				
  1826 				if (requestID) {
  1827 					[self setRequestType:(addAsFavorite ? AITwitterFavoriteYes : AITwitterFavoriteNo)
  1828 							forRequestID:requestID
  1829 						  withDictionary:[NSDictionary dictionaryWithObjectsAndKeys:tweetID, @"tweetID", nil]];
  1830 				} else {
  1831 					[adium.contentController displayEvent:AILocalizedString(@"Attempt to favorite tweet failed to connect.", nil)
  1832 												   ofType:@"favorite"
  1833 												   inChat:timelineChat];
  1834 				}
  1835 			} else {
  1836 				[adium.contentController displayEvent:[NSString stringWithFormat:AILocalizedString(@"Attempt to favorite tweet failed. %@", nil), [self errorMessageForError:error]]
  1837 											   ofType:@"favorite"
  1838 											   inChat:timelineChat];				
  1839 			}
  1840 			
  1841 			break;
  1842 		}
  1843 			
  1844 		case AITwitterNotificationEnable:
  1845 		case AITwitterNotificationDisable:
  1846 		{
  1847 			BOOL			enableNotification = ([self requestTypeForRequestID:identifier] == AITwitterNotificationEnable);
  1848 			AIListContact	*listContact = [[self dictionaryForRequestID:identifier] objectForKey:@"ListContact"];
  1849 			
  1850 			[adium.interfaceController handleErrorMessage:(enableNotification ?
  1851 														   AILocalizedString(@"Unable to Enable Notifications", nil) :
  1852 														   AILocalizedString(@"Unable to Disable Notifications", nil))
  1853 										  withDescription:[NSString stringWithFormat:AILocalizedString(@"Cannot change notification setting for %@. %@", nil), listContact.UID, [self errorMessageForError:error]]];
  1854 			break;
  1855 		}
  1856 			
  1857 		case AITwitterDestroyStatus:
  1858 		{
  1859 			AIChat *timelineChat = self.timelineChat;
  1860 			
  1861 			[adium.contentController displayEvent:[NSString stringWithFormat:AILocalizedString(@"Your tweet failed to delete. %@", nil), [self errorMessageForError:error]]
  1862 										   ofType:@"delete"
  1863 										   inChat:timelineChat];
  1864 			break;
  1865 		}
  1866 			
  1867 		case AITwitterDestroyDM:
  1868 		{
  1869 				AIListContact *contact = [[self dictionaryForRequestID:identifier] objectForKey:@"ListContact"];
  1870 				AIChat *chat = [adium.chatController chatWithContact:contact];
  1871 				
  1872 				[adium.contentController displayEvent:[NSString stringWithFormat:AILocalizedString(@"The direct message failed to delete. %@", nil), [self errorMessageForError:error]]
  1873 											   ofType:@"delete"
  1874 											   inChat:chat];	
  1875 			break;
  1876 		}
  1877 			
  1878 		case AITwitterUnknownType:
  1879 		case AITwitterRateLimitStatus:
  1880 		case AITwitterProfileSelf:
  1881 		case AITwitterSelfUserIconPull:
  1882 		case AITwitterProfileUserInfo:
  1883 		case AITwitterProfileStatusUpdates:
  1884 			// While we don't handle the errors, it's a good idea to not have a "default" just to prevent accidentally letting something
  1885 			// we should really handle slip through.
  1886 			break;
  1887 
  1888 	}
  1889 	
  1890 	AILogWithSignature(@"%@ Request failed (%@ - %u) - %@", self, identifier, [self requestTypeForRequestID:identifier], error);
  1891 	
  1892 	[self clearRequestTypeForRequestID:identifier];
  1893 }
  1894 
  1895 /*!
  1896  * @brief Status updates received
  1897  */
  1898 - (void)statusesReceived:(NSArray *)statuses forRequest:(NSString *)identifier
  1899 {		
  1900 	if([self requestTypeForRequestID:identifier] == AITwitterUpdateFollowedTimeline ||
  1901 	   [self requestTypeForRequestID:identifier] == AITwitterUpdateReplies) {
  1902 		NSString *lastID;
  1903 		
  1904 		BOOL nextPageNecessary = NO;
  1905 		
  1906 		if([self requestTypeForRequestID:identifier] == AITwitterUpdateFollowedTimeline) {
  1907 			lastID = [self preferenceForKey:TWITTER_PREFERENCE_TIMELINE_LAST_ID
  1908 									  group:TWITTER_PREFERENCE_GROUP_UPDATES];
  1909 			
  1910 			nextPageNecessary = (lastID && statuses.count >= TWITTER_UPDATE_TIMELINE_COUNT - 5);
  1911 		} else {
  1912 			lastID = [self preferenceForKey:TWITTER_PREFERENCE_REPLIES_LAST_ID
  1913 									  group:TWITTER_PREFERENCE_GROUP_UPDATES];
  1914 			
  1915 			nextPageNecessary = (lastID && statuses.count >= TWITTER_UPDATE_REPLIES_COUNT - 5);
  1916 		}
  1917 		
  1918 		// Store the largest tweet ID we find; this will be our "last ID" the next time we run.
  1919 		NSString *largestTweet = [[self dictionaryForRequestID:identifier] objectForKey:@"LargestTweet"];
  1920 		
  1921 		// The largest ID is first, compare.
  1922 		if (statuses.count) {
  1923 			NSString *tweetID = [[statuses objectAtIndex:0] objectForKey:TWITTER_STATUS_ID];
  1924 			if (!largestTweet || [largestTweet compare:tweetID options:NSNumericSearch] == NSOrderedAscending) {
  1925 				largestTweet = tweetID;
  1926 			}
  1927 		}
  1928 		
  1929 		[queuedUpdates addObjectsFromArray:statuses];
  1930 		
  1931 		AILogWithSignature(@"%@ Last ID: %@ Largest Tweet: %@ Next Page Necessary: %d", self, lastID, largestTweet, nextPageNecessary);
  1932 		
  1933 		// See if we need to pull more updates.
  1934 		if (nextPageNecessary) {
  1935 			NSInteger	nextPage = [[[self dictionaryForRequestID:identifier] objectForKey:@"Page"] intValue] + 1;
  1936 			NSString	*requestID;
  1937 			
  1938 			if ([self requestTypeForRequestID:identifier] == AITwitterUpdateFollowedTimeline) {
  1939 				requestID = [twitterEngine getFollowedTimelineFor:nil
  1940 														  sinceID:lastID
  1941 												   startingAtPage:nextPage
  1942 															count:TWITTER_UPDATE_TIMELINE_COUNT];
  1943 				
  1944 				AILogWithSignature(@"%@ Pulling additional timeline page %d", self, nextPage);
  1945 				
  1946 				if (requestID) {
  1947 					[self setRequestType:AITwitterUpdateFollowedTimeline
  1948 							forRequestID:requestID
  1949 						  withDictionary:[NSDictionary dictionaryWithObjectsAndKeys:[NSNumber numberWithInt:nextPage], @"Page", 
  1950 										  largestTweet, @"LargestTweet", nil]];
  1951 				} else {
  1952 					// Gracefully fail: remove all stored objects.
  1953 					AILogWithSignature(@"%@ Immediate timeline fail", self);
  1954 					--pendingUpdateCount;
  1955 					[queuedUpdates removeAllObjects];
  1956 				}
  1957 				
  1958 			} else if ([self requestTypeForRequestID:identifier] == AITwitterUpdateReplies) {
  1959 				requestID = [twitterEngine getRepliesSinceID:lastID startingAtPage:nextPage];
  1960 				
  1961 				AILogWithSignature(@"%@ Pulling additional replies page %d", self, nextPage);
  1962 				
  1963 				if (requestID) {
  1964 					[self setRequestType:AITwitterUpdateReplies
  1965 							forRequestID:requestID
  1966 						  withDictionary:[NSDictionary dictionaryWithObjectsAndKeys:[NSNumber numberWithInt:nextPage], @"Page",
  1967 										  largestTweet, @"LargestTweet", nil]];
  1968 				} else {
  1969 					// Gracefully fail: remove all stored objects.
  1970 					AILogWithSignature(@"%@ Immediate reply fail", self);
  1971 					--pendingUpdateCount;
  1972 					[queuedUpdates removeAllObjects];
  1973 				}
  1974 			}
  1975 		} else {
  1976 			if([self requestTypeForRequestID:identifier] == AITwitterUpdateFollowedTimeline) {
  1977 				followedTimelineCompleted = YES;
  1978 				futureTimelineLastID = [largestTweet retain];
  1979 			} else if ([self requestTypeForRequestID:identifier] == AITwitterUpdateReplies) {
  1980 				repliesCompleted = YES;
  1981 				futureRepliesLastID = [largestTweet retain];
  1982 			}
  1983 			
  1984 			--pendingUpdateCount;
  1985 			
  1986 			AILogWithSignature(@"%@ Followed completed: %d Replies completed: %d", self, followedTimelineCompleted, repliesCompleted);
  1987 			
  1988 			if (followedTimelineCompleted && repliesCompleted) {
  1989 				if (queuedUpdates.count) {
  1990 					// Set the "last pulled" for the timeline and replies, since we've completed both.
  1991 					if(futureRepliesLastID) {
  1992 						AILogWithSignature(@"%@ futureRepliesLastID = %@", self, futureRepliesLastID);
  1993 						
  1994 						[self setPreference:futureRepliesLastID
  1995 									 forKey:TWITTER_PREFERENCE_REPLIES_LAST_ID
  1996 									  group:TWITTER_PREFERENCE_GROUP_UPDATES];
  1997 						
  1998 						[futureRepliesLastID release]; futureRepliesLastID = nil;
  1999 					}
  2000 					
  2001 					if(futureTimelineLastID) {
  2002 						AILogWithSignature(@"%@ futureTimelineLastID = %@", self, futureTimelineLastID);
  2003 						
  2004 						[self setPreference:futureTimelineLastID
  2005 									 forKey:TWITTER_PREFERENCE_TIMELINE_LAST_ID
  2006 									  group:TWITTER_PREFERENCE_GROUP_UPDATES];
  2007 						
  2008 						[futureTimelineLastID release]; futureTimelineLastID = nil;
  2009 					}
  2010 					
  2011 					[self displayQueuedUpdatesForRequestType:[self requestTypeForRequestID:identifier]];
  2012 				}
  2013 
  2014 				if (![self preferenceForKey:TWITTER_PREFERENCE_EVER_LOADED_TIMELINE group:TWITTER_PREFERENCE_GROUP_UPDATES]) {
  2015 					[self setPreference:[NSNumber numberWithBool:YES]
  2016 								 forKey:TWITTER_PREFERENCE_EVER_LOADED_TIMELINE
  2017 								  group:TWITTER_PREFERENCE_GROUP_UPDATES];
  2018 				}
  2019 			}
  2020 		}
  2021 	} else if ([self requestTypeForRequestID:identifier] == AITwitterProfileStatusUpdates) {
  2022 		AIListContact *listContact = [[self dictionaryForRequestID:identifier] objectForKey:@"ListContact"];
  2023 
  2024 		NSMutableArray *profileArray = [[listContact profileArray] mutableCopy];
  2025 		
  2026 		AILogWithSignature(@"%@ Updating statuses for profile, user %@", self, listContact);
  2027 		
  2028 		for (NSDictionary *update in statuses) {
  2029 			NSAttributedString *message = [self parseMessage:[update objectForKey:TWITTER_STATUS_TEXT]
  2030 													 tweetID:[update objectForKey:TWITTER_STATUS_ID]
  2031 													  userID:listContact.UID
  2032 											   inReplyToUser:[update objectForKey:TWITTER_STATUS_REPLY_UID]
  2033 											inReplyToTweetID:[update objectForKey:TWITTER_STATUS_REPLY_ID]];
  2034 			
  2035 			[profileArray addObject:[NSDictionary dictionaryWithObjectsAndKeys:message, KEY_VALUE, nil]];
  2036 		}
  2037 		
  2038 		[listContact setProfileArray:profileArray notify:NotifyNow];
  2039 	} else if ([self requestTypeForRequestID:identifier] == AITwitterSendUpdate) {
  2040 		if (updateAfterSend) {
  2041 			[self periodicUpdate];
  2042 		}
  2043 		
  2044 		if (statuses.count) {
  2045 			[adium.contentController displayEvent:AILocalizedString(@"Tweet successfully sent.", nil)
  2046 										   ofType:@"tweet"
  2047 										  inChat:self.timelineChat];
  2048 		}
  2049 				
  2050 		for(NSDictionary *update in statuses) {
  2051 			[[NSNotificationCenter defaultCenter] postNotificationName:AITwitterNotificationPostedStatus
  2052 																object:update
  2053 															  userInfo:[NSDictionary dictionaryWithObjectsAndKeys:self.timelineChat, @"AIChat", nil]];
  2054 			
  2055 			NSString *text = [[update objectForKey:TWITTER_STATUS_TEXT] stringByUnescapingFromXMLWithEntities:nil];
  2056 			
  2057 			if([[self preferenceForKey:TWITTER_PREFERENCE_UPDATE_GLOBAL group:TWITTER_PREFERENCE_GROUP_UPDATES] boolValue] &&
  2058 			   (![text hasPrefix:@"@"] || [[self preferenceForKey:TWITTER_PREFERENCE_UPDATE_GLOBAL_REPLIES group:TWITTER_PREFERENCE_GROUP_UPDATES] boolValue])) {
  2059 				AIStatus *availableStatus = [AIStatus statusOfType:AIAvailableStatusType];
  2060 				
  2061 				availableStatus.statusMessage = [NSAttributedString stringWithString:text];
  2062 				[adium.statusController setActiveStatusState:availableStatus];
  2063 			}
  2064 		}
  2065 	} else if ([self requestTypeForRequestID:identifier] == AITwitterFavoriteYes ||
  2066 			   [self requestTypeForRequestID:identifier] == AITwitterFavoriteNo) {
  2067 		AIChat *timelineChat = self.timelineChat;
  2068 
  2069 		for (NSDictionary *status in statuses) {
  2070 			NSString *message;
  2071 			
  2072 			// Use HTML for the status message since it's just easier to localize that way.
  2073 			
  2074 			if ([self requestTypeForRequestID:identifier] == AITwitterFavoriteYes) {
  2075 				message = AILocalizedString(@"The <a href=\"%@\">requested tweet</a> by <a href=\"%@\">%@</a> is now a favorite.", nil);
  2076 			} else {
  2077 				message = AILocalizedString(@"The <a href=\"%@\">requested tweet</a> by <a href=\"%@\">%@</a> is no longer a favorite.", nil);
  2078 			}
  2079 
  2080 			NSString *userID = [[status objectForKey:TWITTER_STATUS_USER] objectForKey:TWITTER_STATUS_UID];
  2081 			
  2082 			
  2083 			message = [NSString stringWithFormat:message,
  2084 					   [self addressForLinkType:AITwitterLinkStatus
  2085 										 userID:userID
  2086 									   statusID:[status objectForKey:TWITTER_STATUS_ID]
  2087 										context:nil],
  2088 					   [self addressForLinkType:AITwitterLinkUserPage
  2089 										 userID:userID
  2090 									   statusID:nil
  2091 										context:nil],
  2092 					   userID];
  2093 			
  2094 			NSAttributedString *attributedMessage = [[AIHTMLDecoder decoder] decodeHTML:message withDefaultAttributes:nil];
  2095 			
  2096 			AIContentEvent *content = [AIContentEvent eventInChat:timelineChat
  2097 													   withSource:nil
  2098 													  destination:self
  2099 															 date:[NSDate date]
  2100 														  message:attributedMessage
  2101 														 withType:@"favorite"];
  2102 			
  2103 			content.postProcessContent = NO;
  2104 			content.coalescingKey = @"favorite";
  2105 
  2106 			[adium.contentController receiveContentObject:content];
  2107 		}
  2108 	}
  2109 	
  2110 	[self clearRequestTypeForRequestID:identifier];
  2111 }
  2112 
  2113 /*!
  2114  * @brief Direct messages received
  2115  */
  2116 - (void)directMessagesReceived:(NSArray *)messages forRequest:(NSString *)identifier
  2117 {	
  2118 	if ([self requestTypeForRequestID:identifier] == AITwitterUpdateDirectMessage) {		
  2119 		NSString *lastID = [self preferenceForKey:TWITTER_PREFERENCE_DM_LAST_ID
  2120 											group:TWITTER_PREFERENCE_GROUP_UPDATES];
  2121 		
  2122 		BOOL nextPageNecessary = (lastID && messages.count >= TWITTER_UPDATE_DM_COUNT);
  2123 		
  2124 		// Store the largest tweet ID we find; this will be our "last ID" the next time we run.
  2125 		NSString *largestTweet = [[self dictionaryForRequestID:identifier] objectForKey:@"LargestTweet"];
  2126 		
  2127 		// The largest ID is first, compare.
  2128 		if (messages.count) {
  2129 			NSString *tweetID = [[messages objectAtIndex:0] objectForKey:TWITTER_DM_ID];
  2130 			if (!largestTweet || [largestTweet compare:tweetID] == NSOrderedAscending) {
  2131 				largestTweet = tweetID;
  2132 			}
  2133 		}
  2134 		
  2135 		[queuedDM addObjectsFromArray:messages];
  2136 		
  2137 		AILogWithSignature(@"%@ Last ID: %@ Largest Tweet: %@ Next Page Necessary: %d", self, lastID, largestTweet, nextPageNecessary);
  2138 		
  2139 		if(nextPageNecessary) {
  2140 			NSInteger	nextPage = [[[self dictionaryForRequestID:identifier] objectForKey:@"Page"] intValue] + 1;
  2141 			
  2142 			NSString	*requestID = [twitterEngine getDirectMessagesSinceID:lastID
  2143 														      startingAtPage:nextPage];
  2144 			
  2145 			AILogWithSignature(@"%@ Pulling additional DM page %d", self, nextPage);
  2146 			
  2147 			if(requestID) {
  2148 				[self setRequestType:AITwitterUpdateDirectMessage
  2149 						forRequestID:requestID
  2150 					  withDictionary:[NSDictionary dictionaryWithObjectsAndKeys:[NSNumber numberWithInt:nextPage], @"Page",
  2151 									  largestTweet, @"LargestTweet", nil]];
  2152 			} else {
  2153 				// Gracefully fail: remove all stored objects.
  2154 				AILogWithSignature(@"%@ Immediate DM pull fail", self);
  2155 				--pendingUpdateCount;
  2156 				[queuedDM removeAllObjects];
  2157 			}
  2158 		} else {		
  2159 			--pendingUpdateCount;
  2160 			
  2161 			if (largestTweet) {
  2162 				AILogWithSignature(@"%@ Largest DM pulled = %@", self, largestTweet);
  2163 				
  2164 				[self setPreference:largestTweet
  2165 							 forKey:TWITTER_PREFERENCE_DM_LAST_ID
  2166 							  group:TWITTER_PREFERENCE_GROUP_UPDATES];
  2167 			}
  2168 		
  2169 			// On first load, don't display any direct messages. Just ge the largest ID.
  2170 			if (queuedDM.count && lastID) {
  2171 				[self displayQueuedUpdatesForRequestType:[self requestTypeForRequestID:identifier]];
  2172 			} else {
  2173 				[queuedDM removeAllObjects];		
  2174 			}
  2175 		}
  2176 	} else if ([self requestTypeForRequestID:identifier] == AITwitterDirectMessageSend) {
  2177 		[queuedOutgoingDM addObjectsFromArray:messages];
  2178 		[self displayQueuedUpdatesForRequestType:AITwitterDirectMessageSend];
  2179 	}
  2180 	
  2181 	[self clearRequestTypeForRequestID:identifier];
  2182 }
  2183 
  2184 /*!
  2185  * @brief User information received
  2186  */
  2187 - (void)userInfoReceived:(NSArray *)userInfo forRequest:(NSString *)identifier
  2188 {	
  2189 	if (([self requestTypeForRequestID:identifier] == AITwitterInitialUserInfo ||
  2190 		 [self requestTypeForRequestID:identifier] == AITwitterAddFollow) &&
  2191 		[[self preferenceForKey:TWITTER_PREFERENCE_LOAD_CONTACTS group:TWITTER_PREFERENCE_GROUP_UPDATES] boolValue]) {
  2192 		[[AIContactObserverManager sharedManager] delayListObjectNotifications];
  2193 		
  2194 		// The current amount of friends per page is 100. Use >= just in case this changes.
  2195 		BOOL nextPageNecessary = (userInfo.count >= 100);
  2196 		
  2197 		AILogWithSignature(@"%@ User info pull, Next page necessary: %d Count: %d", self, nextPageNecessary, userInfo.count);
  2198 		
  2199 		for (NSDictionary *info in userInfo) {
  2200 			AIListContact *listContact = [self contactWithUID:[info objectForKey:TWITTER_INFO_UID]];
  2201 			
  2202 			// If the user isn't in a group, set them in the Twitter group.
  2203 			if(listContact.countOfRemoteGroupNames == 0) {
  2204 				[listContact addRemoteGroupName:TWITTER_REMOTE_GROUP_NAME];
  2205 			}
  2206 		
  2207 			// Grab the Twitter display name and set it as the remote alias.
  2208 			if (![[listContact valueForProperty:@"Server Display Name"] isEqualToString:[info objectForKey:TWITTER_INFO_DISPLAY_NAME]]) {
  2209 				[listContact setServersideAlias:[info objectForKey:TWITTER_INFO_DISPLAY_NAME]
  2210 									   silently:silentAndDelayed];
  2211 			}
  2212 			
  2213 			// Grab the user icon and set it as their serverside icon.
  2214 			[self updateUserIcon:[info objectForKey:TWITTER_INFO_ICON] forContact:listContact];
  2215 			
  2216 			// Set the user as available.
  2217 			[listContact setStatusWithName:nil
  2218 								statusType:AIAvailableStatusType
  2219 									notify:NotifyLater];
  2220 			
  2221 			// Set the user's status message to their current twitter status text
  2222 			NSString *statusText = [[info objectForKey:TWITTER_INFO_STATUS] objectForKey:TWITTER_INFO_STATUS_TEXT];
  2223 			if (!statusText) //nil if they've never tweeted
  2224 				statusText = @"";
  2225 			[listContact setStatusMessage:[NSAttributedString stringWithString:[statusText stringByUnescapingFromXMLWithEntities:nil]] notify:NotifyLater];
  2226 			
  2227 			// Set the user as online.
  2228 			[listContact setOnline:YES notify:NotifyLater silently:silentAndDelayed];
  2229 			
  2230 			[listContact notifyOfChangedPropertiesSilently:silentAndDelayed];
  2231 		}
  2232 		
  2233 		[[AIContactObserverManager sharedManager] endListObjectNotificationsDelay];
  2234 		
  2235 		if (nextPageNecessary) {
  2236 			NSInteger	nextPage = [[[self dictionaryForRequestID:identifier] objectForKey:@"Page"] intValue] + 1;
  2237 			NSString	*requestID = [twitterEngine getRecentlyUpdatedFriendsFor:self.UID startingAtPage:nextPage];
  2238 			
  2239 			AILogWithSignature(@"%@ Pulling additional user info page %d", self, nextPage);
  2240 			
  2241 			if(requestID) {
  2242 				[self setRequestType:AITwitterInitialUserInfo
  2243 						forRequestID:requestID
  2244 					  withDictionary:[NSDictionary dictionaryWithObject:[NSNumber numberWithInt:nextPage]
  2245 																 forKey:@"Page"]];
  2246 			} else { 
  2247 				[self setLastDisconnectionError:AILocalizedString(@"Unable to retrieve user list [additional fail]", "Message when a (vital) twitter request to retrieve the follow list fails")];
  2248 				[self didDisconnect];
  2249 			}
  2250 			
  2251 		} else if ([self valueForProperty:@"Connecting"]) {			
  2252 			// Trigger our normal update routine.
  2253 			[self didConnect];
  2254 		}
  2255 	} else if ([self requestTypeForRequestID:identifier] == AITwitterProfileUserInfo) {
  2256 		NSDictionary *thisUserInfo = [userInfo objectAtIndex:0];
  2257 		
  2258 		if (thisUserInfo) {	
  2259 			AIListContact	*listContact = [[self dictionaryForRequestID:identifier] objectForKey:@"ListContact"];
  2260 			
  2261 			NSArray *keyNames = [NSArray arrayWithObjects:@"name", @"location", @"description", @"url", @"friends_count", @"followers_count", @"statuses_count", nil];
  2262 			NSArray *readableNames = [NSArray arrayWithObjects:AILocalizedString(@"Name", nil), AILocalizedString(@"Location", nil),
  2263 									  AILocalizedString(@"Biography", nil), AILocalizedString(@"Website", nil), AILocalizedString(@"Following", nil),
  2264 									  AILocalizedString(@"Followers", nil), AILocalizedString(@"Updates", nil), nil];
  2265 			
  2266 			NSMutableArray *profileArray = [NSMutableArray array];
  2267 			
  2268 			for (NSUInteger index = 0; index < keyNames.count; index++) {
  2269 				NSString			*keyName = [keyNames objectAtIndex:index];
  2270 				NSString			*unattributedValue = [thisUserInfo objectForKey:keyName];
  2271 				
  2272 				if(![unattributedValue isEqualToString:@""]) {
  2273 					NSString			*readableName = [readableNames objectAtIndex:index];
  2274 					NSAttributedString	*value;
  2275 					
  2276 					if([keyName isEqualToString:@"friends_count"]) {
  2277 						value = [NSAttributedString attributedStringWithLinkLabel:unattributedValue
  2278 																  linkDestination:[self addressForLinkType:AITwitterLinkFriends userID:listContact.UID statusID:nil context:nil]];
  2279 					} else if ([keyName isEqualToString:@"followers_count"]) {
  2280 						value = [NSAttributedString attributedStringWithLinkLabel:unattributedValue
  2281 																  linkDestination:[self addressForLinkType:AITwitterLinkFollowers userID:listContact.UID statusID:nil context:nil]];
  2282 					} else if ([keyName isEqualToString:@"statuses_count"]) {
  2283 						value = [NSAttributedString attributedStringWithLinkLabel:unattributedValue
  2284 																  linkDestination:[self addressForLinkType:AITwitterLinkUserPage userID:listContact.UID statusID:nil context:nil]];
  2285 					} else {
  2286 						value = [NSAttributedString stringWithString:unattributedValue];
  2287 					}
  2288 						
  2289 					[profileArray addObject:[NSDictionary dictionaryWithObjectsAndKeys:readableName, KEY_KEY, value, KEY_VALUE, nil]];
  2290 				}
  2291 			}
  2292 			
  2293 			AILogWithSignature(@"%@ Updating profileArray for user %@", self, listContact);
  2294 			
  2295 			[listContact setProfileArray:profileArray notify:NotifyNow];
  2296 			
  2297 			// Grab their statuses.
  2298 			NSString *requestID = [twitterEngine getUserTimelineFor:listContact.UID since:nil startingAtPage:0 count:TWITTER_UPDATE_USER_INFO_COUNT];
  2299 			
  2300 			if (requestID) {
  2301 				[self setRequestType:AITwitterProfileStatusUpdates
  2302 						forRequestID:requestID
  2303 					  withDictionary:[NSDictionary dictionaryWithObject:listContact forKey:@"ListContact"]];
  2304 			}
  2305 		}
  2306 	} else if ([self requestTypeForRequestID:identifier] == AITwitterValidateCredentials ||
  2307 			   [self requestTypeForRequestID:identifier] == AITwitterProfileSelf) {
  2308 		for (NSDictionary *info in userInfo) {
  2309 			NSString *requestID = [twitterEngine getImageAtURL:[info objectForKey:TWITTER_INFO_ICON]];
  2310 			
  2311 			if (requestID) {
  2312 				[self setRequestType:AITwitterSelfUserIconPull
  2313 						forRequestID:requestID
  2314 					  withDictionary:nil];
  2315 			}
  2316 
  2317 			[self filterAndSetUID:[info objectForKey:TWITTER_INFO_UID]];
  2318 			
  2319 			if ([info objectForKey:@"name"]) {
  2320 				[self setPreference:[[NSAttributedString stringWithString:[info objectForKey:@"name"]] dataRepresentation]
  2321 								forKey:KEY_ACCOUNT_DISPLAY_NAME
  2322 								 group:GROUP_ACCOUNT_STATUS];		
  2323 			}
  2324 			
  2325 			[self setValue:[info objectForKey:@"name"] forProperty:@"Profile Name" notify:NotifyLater];
  2326 			[self setValue:[info objectForKey:@"url"] forProperty:@"Profile URL" notify:NotifyLater];
  2327 			[self setValue:[info objectForKey:@"location"] forProperty:@"Profile Location" notify:NotifyLater];
  2328 			[self setValue:[info objectForKey:@"description"] forProperty:@"Profile Description" notify:NotifyLater];
  2329 			[self notifyOfChangedPropertiesSilently:NO];
  2330 		}
  2331 		
  2332 		
  2333 		if([self requestTypeForRequestID:identifier] == AITwitterValidateCredentials) {
  2334 			// Our UID is definitely set; grab our friends.
  2335 			
  2336 			if ([[self preferenceForKey:TWITTER_PREFERENCE_LOAD_CONTACTS group:TWITTER_PREFERENCE_GROUP_UPDATES] boolValue]) {
  2337 				// If we load our follows as contacts, do so now.
  2338 				
  2339 				// Delay updates on initial login.
  2340 				[self silenceAllContactUpdatesForInterval:18.0];
  2341 				// Grab our user list.
  2342 				NSString	*requestID = [twitterEngine getRecentlyUpdatedFriendsFor:self.UID startingAtPage:1];
  2343 				
  2344 				if (requestID) {
  2345 					[self setRequestType:AITwitterInitialUserInfo
  2346 							forRequestID:requestID
  2347 						  withDictionary:[NSDictionary dictionaryWithObject:[NSNumber numberWithInt:1] forKey:@"Page"]];
  2348 				} else {
  2349 					[self setLastDisconnectionError:AILocalizedString(@"Unable to retrieve user list", nil)];
  2350 					[self didDisconnect];
  2351 				}
  2352 			} else {
  2353 				// If we don't load follows as contacts, we've finished connecting (fast, wasn't it?)
  2354 				[self didConnect];
  2355 			}
  2356 		}
  2357 	} else if ([self requestTypeForRequestID:identifier] == AITwitterNotificationEnable ||
  2358 			   [self requestTypeForRequestID:identifier] == AITwitterNotificationDisable) {
  2359 		BOOL			enableNotification = ([self requestTypeForRequestID:identifier] == AITwitterNotificationEnable);
  2360 		AIListContact	*listContact = [[self dictionaryForRequestID:identifier] objectForKey:@"ListContact"];
  2361 		
  2362 		for (NSDictionary *info in userInfo) {		
  2363 			[adium.interfaceController handleMessage:(enableNotification ?
  2364 													  AILocalizedString(@"Notifications Enabled", nil) :
  2365 													  AILocalizedString(@"Notifications Disabled", nil))
  2366 									 withDescription:[NSString stringWithFormat:(enableNotification ?
  2367 																				 AILocalizedString(@"You will now receive device notifications for %@.", nil) :
  2368 																				 AILocalizedString(@"You will no longer receive device notifications for %@.", nil)),
  2369 													  listContact.UID]
  2370 									 withWindowTitle:(enableNotification ?
  2371 													  AILocalizedString(@"Notifications Enabled", nil) :
  2372 													  AILocalizedString(@"Notifications Disabled", nil))];
  2373 		}
  2374 	}
  2375 	
  2376 	[self clearRequestTypeForRequestID:identifier];
  2377 }
  2378 
  2379 /*!
  2380  * @brief Miscellaneous information received
  2381  */
  2382 - (void)miscInfoReceived:(NSArray *)miscInfo forRequest:(NSString *)identifier
  2383 {
  2384 	if([self requestTypeForRequestID:identifier] == AITwitterRateLimitStatus) {
  2385 		NSDictionary *rateLimit = [miscInfo objectAtIndex:0];
  2386 		NSDate *resetDate = [NSDate dateWithTimeIntervalSince1970:[[rateLimit objectForKey:TWITTER_RATE_LIMIT_RESET_SECONDS] intValue]];
  2387 		
  2388 		[adium.interfaceController handleMessage:AILocalizedString(@"Current Twitter rate limit", "Message in the rate limit status window")
  2389 								 withDescription:[NSString stringWithFormat:AILocalizedString(@"You have %d/%d more requests for %@.", "The first %d is the number of requests, the second is the total number of requests per hour. The %@ is the duration of time until the count resets."),
  2390 													[[rateLimit objectForKey:TWITTER_RATE_LIMIT_REMAINING] intValue],
  2391 													[[rateLimit objectForKey:TWITTER_RATE_LIMIT_HOURLY_LIMIT] intValue],
  2392 													[NSDateFormatter stringForTimeInterval:[resetDate timeIntervalSinceNow]
  2393 																			showingSeconds:YES
  2394 																			   abbreviated:YES
  2395 																			  approximated:NO]]
  2396 								 withWindowTitle:AILocalizedString(@"Rate Limit Status", nil)];
  2397 	}
  2398 	
  2399 	[self clearRequestTypeForRequestID:identifier];
  2400 }
  2401 
  2402 /*!
  2403  * @brief Requested image received
  2404  */
  2405 - (void)imageReceived:(NSImage *)image forRequest:(NSString *)identifier
  2406 {
  2407 	if([self requestTypeForRequestID:identifier] == AITwitterUserIconPull) {
  2408 		AIListContact		*listContact = [[self dictionaryForRequestID:identifier] objectForKey:@"ListContact"];
  2409 		
  2410 		AILogWithSignature(@"%@ Updated user icon for %@", self, listContact);
  2411 		
  2412 		[listContact setServersideIconData:[image TIFFRepresentation]
  2413 									notify:NotifyLater];
  2414 		
  2415 		[listContact setValue:nil forProperty:TWITTER_PROPERTY_REQUESTED_USER_ICON notify:NotifyNever];
  2416 	} else if([self requestTypeForRequestID:identifier] == AITwitterSelfUserIconPull) {
  2417 		AILogWithSignature(@"Updated self icon for %@", self);
  2418 
  2419 		// Set a property so we don't re-send thie image we're just now downloading.
  2420 		[self setValue:[NSNumber numberWithBool:YES] forProperty:TWITTER_PROPERTY_REQUESTED_USER_ICON notify:NotifyNever];
  2421 		
  2422 		[self setPreference:[NSNumber numberWithBool:YES]
  2423 					 forKey:KEY_USE_USER_ICON
  2424 					  group:GROUP_ACCOUNT_STATUS];
  2425 		
  2426 		
  2427 		[self setPreference:[image TIFFRepresentation]
  2428 					 forKey:KEY_USER_ICON
  2429 					  group:GROUP_ACCOUNT_STATUS];
  2430 	}
  2431 	
  2432 	[self clearRequestTypeForRequestID:identifier];
  2433 }
  2434 
  2435 @end