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

On an annoying note, home_timeline (despite saying "Returns the 20 most recent statuses, including retweets, posted by the authenticating user and that user's friends") does not include outgoing retweets. This will either be fixed by Twitter quickly, or not. I'm tired of Twitter's inconsistent and buggy API. So, as such, there's currently no way to remove a retweet done by yourself.
     1 /* 
     2  * 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 Retweet the selected tweet.
  1199  *
  1200  * Attempts to retweet a tweet.
  1201  * Prints a status message in the chat on success/failure, behaves identical to sending a new tweet.
  1202  */
  1203 - (void)retweetTweet:(NSString *)tweetID
  1204 {
  1205 	NSString *requestID = [twitterEngine retweetUpdate:tweetID];
  1206 	
  1207 	if (requestID) {
  1208 		[self setRequestType:AITwitterSendUpdate
  1209 				forRequestID:requestID
  1210 			  withDictionary:[NSDictionary dictionaryWithObjectsAndKeys:tweetID, @"tweetID", nil]];
  1211 	} else {
  1212 		[self.timelineChat receivedError:[NSNumber numberWithInt:AIChatMessageSendingConnectionError]];
  1213 	}
  1214 }
  1215 
  1216 /*!
  1217  * @brief Toggle the favorite status for a tweet.
  1218  *
  1219  * Attempts to favorite a tweet. If that fails, it removes favorite status.
  1220  * Prints a status message in the chat on success/failure, since it's otherwise not obvious.
  1221  */
  1222 - (void)toggleFavoriteTweet:(NSString *)tweetID
  1223 {
  1224 	NSString *requestID = [twitterEngine markUpdate:tweetID asFavorite:YES];
  1225 	
  1226 	if (requestID) {
  1227 		[self setRequestType:AITwitterFavoriteYes
  1228 				forRequestID:requestID
  1229 			  withDictionary:[NSDictionary dictionaryWithObjectsAndKeys:tweetID, @"tweetID", nil]];
  1230 	} else {
  1231 		AIChat *timelineChat = self.timelineChat;
  1232 		
  1233 		[adium.contentController displayEvent:AILocalizedString(@"Attempt to favorite tweet failed to connect.", nil)
  1234 									   ofType:@"favorite"
  1235 									   inChat:timelineChat];
  1236 	}
  1237 }
  1238 
  1239 /*!
  1240  * @brief Destroy the tweet.
  1241  *
  1242  * The user has already confirmed they want to destroy it; send the message.
  1243  */
  1244 - (void)destroyTweet:(NSString *)tweetID
  1245 {
  1246 	NSString *requestID = [twitterEngine deleteUpdate:tweetID];
  1247 	
  1248 	if(requestID) {
  1249 		[self setRequestType:AITwitterDestroyStatus
  1250 				forRequestID:requestID
  1251 			  withDictionary:nil];
  1252 	} else {
  1253 		AIChat *timelineChat = self.timelineChat;
  1254 		
  1255 		[adium.contentController displayEvent:AILocalizedString(@"Attempt to delete tweet failed to connect.", nil)
  1256 									   ofType:@"delete"
  1257 									   inChat:timelineChat];
  1258 	}
  1259 }
  1260 
  1261 /*!
  1262  * @brief Destroy the DM.
  1263  *
  1264  * The user has already confirmed they want to destroy it; send the message.
  1265  */
  1266 - (void)destroyDirectMessage:(NSString *)messageID
  1267 					 forUser:(NSString *)userID
  1268 {
  1269 	NSString *requestID = [twitterEngine deleteDirectMessage:messageID];
  1270 	AIListContact *contact = [self contactWithUID:userID];
  1271 	
  1272 	if(requestID) {
  1273 		[self setRequestType:AITwitterDestroyDM
  1274 				forRequestID:requestID
  1275 			  withDictionary:[NSDictionary dictionaryWithObject:contact forKey:@"ListContact"]];
  1276 	} else {
  1277 		AIChat *chat = [adium.chatController chatWithContact:contact];
  1278 		
  1279 		[adium.contentController displayEvent:AILocalizedString(@"Attempt to delete tweet failed to connect.", nil)
  1280 									   ofType:@"delete"
  1281 									   inChat:chat];
  1282 	}	
  1283 }
  1284 
  1285 /*!
  1286  * @brief Convert a link URL and name into an attributed link
  1287  *
  1288  * @param label The text to display for the link.
  1289  * @param destination The destination address for the link.
  1290  * @param attributeName The name of the twitter link attribute for HTML processing.
  1291  */
  1292 - (NSAttributedString *)attributedStringWithLinkLabel:(NSString *)label
  1293 									  linkDestination:(NSString *)destination
  1294 											linkClass:(NSString *)className
  1295 {
  1296 	NSURL *url = [NSURL URLWithString:destination];
  1297 	NSDictionary *attributes = [NSDictionary dictionaryWithObjectsAndKeys:
  1298 								url, NSLinkAttributeName,
  1299 								className, AIElementClassAttributeName, nil];
  1300 	
  1301 	return [[[NSAttributedString alloc] initWithString:label attributes:attributes] autorelease];
  1302 }
  1303 
  1304 /*!
  1305  * @brief Parse an attributed string into a linkified version.
  1306  */
  1307 - (NSAttributedString *)linkifiedAttributedStringFromString:(NSAttributedString *)inString
  1308 {	
  1309 	NSAttributedString *attributedString;
  1310 	
  1311 	static NSCharacterSet *usernameCharacters = nil;
  1312 	static NSCharacterSet *hashCharacters = nil;
  1313 	
  1314 	if (!usernameCharacters) {
  1315 		usernameCharacters = [[NSCharacterSet characterSetWithCharactersInString:@"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_"] retain];
  1316 	}
  1317 	
  1318 	if (!hashCharacters) {
  1319 		NSMutableCharacterSet	*disallowedCharacters = [[NSCharacterSet punctuationCharacterSet] mutableCopy];
  1320 		[disallowedCharacters formUnionWithCharacterSet:[NSCharacterSet whitespaceCharacterSet]];
  1321 		
  1322 		hashCharacters = [[disallowedCharacters invertedSet] retain];
  1323 		
  1324 		[disallowedCharacters release];
  1325 	}
  1326 	
  1327 	attributedString = [AITwitterURLParser linkifiedStringFromAttributedString:inString
  1328 															forPrefixCharacter:@"@"
  1329 																   forLinkType:AITwitterLinkUserPage
  1330 																	forAccount:self
  1331 															 validCharacterSet:usernameCharacters];
  1332 	
  1333 	attributedString = [AITwitterURLParser linkifiedStringFromAttributedString:attributedString
  1334 															forPrefixCharacter:@"#"
  1335 																   forLinkType:AITwitterLinkSearchHash
  1336 																	forAccount:self
  1337 															 validCharacterSet:hashCharacters];
  1338 	
  1339 	return attributedString;
  1340 }
  1341 
  1342 /*!
  1343  * @brief Parses a Twitter message into an attributed string
  1344  */
  1345 - (NSAttributedString *)parseMessage:(NSString *)inMessage
  1346 							 tweetID:(NSString *)tweetID
  1347 							  userID:(NSString *)userID
  1348 					   inReplyToUser:(NSString *)replyUserID
  1349 					inReplyToTweetID:(NSString *)replyTweetID
  1350 {
  1351 	NSAttributedString *message;
  1352 	
  1353 	message = [NSAttributedString stringWithString:[inMessage stringByUnescapingFromXMLWithEntities:nil]];
  1354 	
  1355 	message = [self linkifiedAttributedStringFromString:message];
  1356 	
  1357 	BOOL replyTweet = (replyTweetID.length);
  1358 	BOOL tweetLink = (tweetID.length && userID.length);
  1359 	
  1360 	if (replyTweet || tweetLink) {
  1361 		NSMutableAttributedString *mutableMessage = [[message mutableCopy] autorelease];
  1362 		
  1363 		NSUInteger startIndex = message.length;
  1364 		
  1365 		[mutableMessage appendString:@"  (" withAttributes:nil];
  1366 	
  1367 		BOOL commaNeeded = NO;
  1368 		
  1369 		// Append a link to the tweet this is in reply to
  1370 		if (replyTweet) {
  1371 			NSString *linkAddress = [self addressForLinkType:AITwitterLinkStatus
  1372 													  userID:replyUserID
  1373 													statusID:replyTweetID
  1374 													 context:nil];
  1375 
  1376 			if([inMessage hasPrefix:@"@"] &&
  1377 			   inMessage.length >= replyUserID.length + 1 &&
  1378 			   [replyUserID isCaseInsensitivelyEqualToString:[inMessage substringWithRange:NSMakeRange(1, replyUserID.length)]]) {
  1379 				// If the message has a "@" prefix, it's a proper in_reply_to_status_id if the usernames match. Set a link appropriately.
  1380 				[mutableMessage setAttributes:[NSDictionary dictionaryWithObjectsAndKeys:linkAddress, NSLinkAttributeName, nil]
  1381 										range:NSMakeRange(0, replyUserID.length + 1)];
  1382 			} else {
  1383 				// This happens for mentions which are in_reply_to_status_id but the @target isn't the first part of the message.
  1384 				
  1385 				[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")
  1386 																		   linkDestination:linkAddress
  1387 																				 linkClass:AITwitterInReplyToClassName]];
  1388 				
  1389 				commaNeeded = YES;	
  1390 			}
  1391 		}
  1392 		
  1393 		// Append a link to reply to this tweet
  1394 		if (tweetLink) {
  1395 			NSString *linkAddress;
  1396 			
  1397 			if(![self.UID isCaseInsensitivelyEqualToString:userID]) {
  1398 				// A message from someone other than ourselves. RT and @ is permissible.
  1399 				if (retweetLink) {				
  1400 					if(commaNeeded) {
  1401 						[mutableMessage appendString:@", " withAttributes:nil];
  1402 					}
  1403 					
  1404 					linkAddress = [self addressForLinkType:AITwitterLinkRetweet
  1405 													userID:userID
  1406 												  statusID:tweetID
  1407 												   context:[inMessage stringByAddingPercentEscapesForAllCharacters]];
  1408 					
  1409 					[mutableMessage appendAttributedString:[self attributedStringWithLinkLabel:@"RT"
  1410 																			   linkDestination:linkAddress
  1411 																					 linkClass:AITwitterRetweetClassName]];
  1412 					commaNeeded = YES;
  1413 				}
  1414 				
  1415 				if (commaNeeded) {
  1416 					[mutableMessage appendString:@", " withAttributes:nil];
  1417 				}			
  1418 				
  1419 				linkAddress = [self addressForLinkType:AITwitterLinkReply
  1420 												userID:userID
  1421 											  statusID:tweetID
  1422 											   context:nil];
  1423 				
  1424 				[mutableMessage appendAttributedString:[self attributedStringWithLinkLabel:@"@"
  1425 																		   linkDestination:linkAddress
  1426 																				 linkClass:AITwitterReplyClassName]];
  1427 			} else {
  1428 				if(commaNeeded) {
  1429 					[mutableMessage appendString:@", " withAttributes:nil];
  1430 				}
  1431 				
  1432 				// Our own message. Display a destroy link.
  1433 				linkAddress = [self addressForLinkType:AITwitterLinkDestroyStatus
  1434 												userID:userID
  1435 											  statusID:tweetID
  1436 											   context:[inMessage stringByAddingPercentEscapesForAllCharacters]];
  1437 				
  1438 				[mutableMessage appendAttributedString:[self attributedStringWithLinkLabel:@"\u232B"
  1439 																		   linkDestination:linkAddress
  1440 																				 linkClass:AITwitterDeleteClassName]];
  1441 			}
  1442 			
  1443 			[mutableMessage appendString:@", " withAttributes:nil];
  1444 
  1445 			linkAddress = [self addressForLinkType:AITwitterLinkFavorite
  1446 											userID:userID
  1447 										  statusID:tweetID
  1448 										   context:nil];
  1449 
  1450 			[mutableMessage appendAttributedString:[self attributedStringWithLinkLabel:@"\u2606"
  1451 																	   linkDestination:linkAddress
  1452 																			 linkClass:AITwitterFavoriteClassName]];
  1453 
  1454 			[mutableMessage appendString:@", " withAttributes:nil];
  1455 			
  1456 			linkAddress = [self addressForLinkType:AITwitterLinkStatus
  1457 											userID:userID
  1458 										  statusID:tweetID
  1459 										   context:nil];
  1460 			
  1461 			[mutableMessage appendAttributedString:[self attributedStringWithLinkLabel:@"#"
  1462 																	   linkDestination:linkAddress
  1463 																			 linkClass:AITwitterStatusLinkClassName]];
  1464 
  1465 		}
  1466 	
  1467 		[mutableMessage appendString:@")" withAttributes:nil];
  1468 		
  1469 		[mutableMessage addAttributes:[NSDictionary dictionaryWithObjectsAndKeys:
  1470 									   [NSNumber numberWithBool:YES], AITwitterActionLinksAttributeName,
  1471 									   [NSNumber numberWithBool:YES], AIHiddenMessagePartAttributeName, nil]
  1472 								range:NSMakeRange(startIndex, mutableMessage.length - startIndex)];
  1473 	
  1474 		return mutableMessage;
  1475 	} else {
  1476 		return message;
  1477 	}
  1478 }
  1479 
  1480 /*!
  1481  * @brief Parse a direct message
  1482  */
  1483 - (NSAttributedString *)parseDirectMessage:(NSString *)inMessage
  1484 									withID:(NSString *)dmID
  1485 								  fromUser:(NSString *)sourceUID
  1486 {
  1487 	NSAttributedString *message;
  1488 	
  1489 	message = [NSAttributedString stringWithString:[inMessage stringByUnescapingFromXMLWithEntities:nil]];
  1490 	
  1491 	message = [self linkifiedAttributedStringFromString:message];
  1492 	
  1493 	NSMutableAttributedString *mutableMessage = [[message mutableCopy] autorelease];
  1494 	
  1495 	NSUInteger startIndex = message.length;
  1496 	
  1497 	[mutableMessage appendString:@"  (" withAttributes:nil];
  1498 	
  1499 	NSString *linkAddress = [self addressForLinkType:AITwitterLinkDestroyDM
  1500 											  userID:sourceUID
  1501 											statusID:dmID
  1502 											 context:[inMessage stringByAddingPercentEscapesForAllCharacters]];
  1503 	
  1504 	[mutableMessage appendAttributedString:[self attributedStringWithLinkLabel:@"\u232B"
  1505 															   linkDestination:linkAddress
  1506 																	 linkClass:AITwitterDeleteClassName]];
  1507 	
  1508 	[mutableMessage appendString:@")" withAttributes:nil];
  1509 	
  1510 	[mutableMessage addAttributes:[NSDictionary dictionaryWithObjectsAndKeys:
  1511 								   [NSNumber numberWithBool:YES], AITwitterActionLinksAttributeName,
  1512 								   [NSNumber numberWithBool:YES], AIHiddenMessagePartAttributeName, nil]
  1513 							range:NSMakeRange(startIndex, mutableMessage.length - startIndex)];
  1514 	
  1515 	return mutableMessage;
  1516 }
  1517 
  1518 
  1519 /*!
  1520  * @brief Sort status updates
  1521  */
  1522 NSInteger queuedUpdatesSort(id update1, id update2, void *context)
  1523 {
  1524 	return [[update1 objectForKey:TWITTER_STATUS_CREATED] compare:[update2 objectForKey:TWITTER_STATUS_CREATED]];
  1525 }
  1526 
  1527 /*!
  1528  * @brief Sort direct messages
  1529  */
  1530 NSInteger queuedDMSort(id dm1, id dm2, void *context)
  1531 {
  1532 	return [[dm1 objectForKey:TWITTER_DM_CREATED] compare:[dm2 objectForKey:TWITTER_DM_CREATED]];	
  1533 }
  1534 
  1535 /*!
  1536  * @brief Remove duplicate status updates.
  1537  *
  1538  * If we're following someone who replies to us, we'll receive a status update in both the
  1539  * timeline and the reply feed.
  1540  *
  1541  * @param inArray The sorted array of Tweets
  1542  */
  1543 - (NSArray *)arrayWithDuplicateTweetsRemoved:(NSArray *)inArray
  1544 {
  1545 	NSMutableArray *mutableArray = [inArray mutableCopy];
  1546 	
  1547 	NSDictionary *status = nil, *previousStatus = nil;
  1548 	
  1549 	// Starting at index 1, checking backwards. We'll never exceed bounds this way.
  1550 	for(NSUInteger index = 1; index < inArray.count; index++)
  1551 	{
  1552 		status = [inArray objectAtIndex:index];
  1553 		previousStatus = [inArray objectAtIndex:index-1];
  1554 		
  1555 		if([[status objectForKey:TWITTER_STATUS_ID] isEqualToString:[previousStatus objectForKey:TWITTER_STATUS_ID]]) {
  1556 			[mutableArray removeObject:status];
  1557 		}
  1558 	}
  1559 	
  1560 	return [mutableArray autorelease];
  1561 }
  1562 
  1563 /*!
  1564  * @brief Display queued updates or direct messages
  1565  *
  1566  * This could potentially be simplified since both DMs and updates have the same format.
  1567  */
  1568 - (void)displayQueuedUpdatesForRequestType:(AITwitterRequestType)requestType
  1569 {
  1570 	if(requestType == AITwitterUpdateReplies || requestType == AITwitterUpdateFollowedTimeline) {
  1571 		if(!queuedUpdates.count) {
  1572 			return;
  1573 		}
  1574 		
  1575 		AILogWithSignature(@"%@ Displaying %d updates", self, queuedUpdates.count);
  1576 		
  1577 		// Sort the queued updates (since we're intermingling pages of data from different souces)
  1578 		NSArray *sortedQueuedUpdates = [queuedUpdates sortedArrayUsingFunction:queuedUpdatesSort context:nil];
  1579 		
  1580 		sortedQueuedUpdates = [self arrayWithDuplicateTweetsRemoved:sortedQueuedUpdates];
  1581 		
  1582 		BOOL trackContent = [[self preferenceForKey:TWITTER_PREFERENCE_EVER_LOADED_TIMELINE group:TWITTER_PREFERENCE_GROUP_UPDATES] boolValue];
  1583 		
  1584 		AIChat *timelineChat = self.timelineChat;
  1585 		
  1586 		[[AIContactObserverManager sharedManager] delayListObjectNotifications];
  1587 		
  1588 		for (NSDictionary *status in sortedQueuedUpdates) {
  1589 			NSDate			*date = [status objectForKey:TWITTER_STATUS_CREATED];
  1590 			NSString		*text = [status objectForKey:TWITTER_STATUS_TEXT];
  1591 			
  1592 			NSString *contactUID = [[status objectForKey:TWITTER_STATUS_USER] objectForKey:TWITTER_STATUS_UID];
  1593 			
  1594 			id fromObject = nil;
  1595 			
  1596 			if(![self.UID isCaseInsensitivelyEqualToString:contactUID]) {
  1597 				AIListContact *listContact = [self contactWithUID:contactUID];
  1598 				
  1599 				// Update the user's status message
  1600 				[listContact setStatusMessage:[NSAttributedString stringWithString:[text stringByUnescapingFromXMLWithEntities:nil]]
  1601 									   notify:NotifyNow];
  1602 				
  1603 				[self updateUserIcon:[[status objectForKey:TWITTER_STATUS_USER] objectForKey:TWITTER_INFO_ICON] forContact:listContact];
  1604 				
  1605 				[timelineChat addParticipatingListObject:listContact notify:NotifyNow];
  1606 				
  1607 				fromObject = (id)listContact;
  1608 			} else {
  1609 				fromObject = (id)self;
  1610 			}
  1611 
  1612 			NSAttributedString *message = [self parseMessage:text
  1613 													 tweetID:[status objectForKey:TWITTER_STATUS_ID]
  1614 													  userID:contactUID
  1615 											   inReplyToUser:[status objectForKey:TWITTER_STATUS_REPLY_UID]
  1616 											inReplyToTweetID:[status objectForKey:TWITTER_STATUS_REPLY_ID]];
  1617 			
  1618 			AIContentMessage *contentMessage = [AIContentMessage messageInChat:timelineChat
  1619 																	withSource:fromObject
  1620 																   destination:self
  1621 																		  date:date
  1622 																	   message:message
  1623 																	 autoreply:NO];
  1624 			
  1625 			contentMessage.trackContent = trackContent;
  1626 			
  1627 			[adium.contentController receiveContentObject:contentMessage];
  1628 		}
  1629 		
  1630 		[[AIContactObserverManager sharedManager] endListObjectNotificationsDelay];
  1631 		
  1632 		[queuedUpdates removeAllObjects];
  1633 	} else if (requestType == AITwitterUpdateDirectMessage || requestType == AITwitterDirectMessageSend) {
  1634 		NSMutableArray **unsortedArray = (requestType == AITwitterUpdateDirectMessage) ? &queuedDM : &queuedOutgoingDM;
  1635 		
  1636 		if (!(*unsortedArray).count) {
  1637 			return;
  1638 		}
  1639 		
  1640 		AILogWithSignature(@"%@ Displaying %d DMs", self, queuedDM.count);
  1641 		
  1642 		NSArray *sortedQueuedDM = [*unsortedArray sortedArrayUsingFunction:queuedDMSort context:nil];
  1643 		
  1644 		for (NSDictionary *message in sortedQueuedDM) {
  1645 			NSDate			*date = [message objectForKey:TWITTER_DM_CREATED];
  1646 			NSString		*text = [message objectForKey:TWITTER_DM_TEXT];
  1647 			NSString		*fromUID = [message objectForKey:TWITTER_DM_SENDER_UID];
  1648 			NSString		*toUID = [message objectForKey:TWITTER_DM_RECIPIENT_UID];
  1649 			
  1650 			AIListObject *source = nil, *destination = nil;
  1651 			AIChat *chat = nil;
  1652 			
  1653 			if([self.UID isCaseInsensitivelyEqualToString:fromUID]) {
  1654 				// This is a message we sent; display as coming from us.
  1655 				source = self;
  1656 				destination = [self contactWithUID:toUID];
  1657 				chat = [adium.chatController chatWithContact:(AIListContact *)destination];
  1658 			} else {
  1659 				source = [self contactWithUID:fromUID];
  1660 				destination = self;
  1661 				chat = [adium.chatController chatWithContact:(AIListContact *)source];
  1662 			}
  1663 			
  1664 			if(chat && source && destination) {
  1665 				AIContentMessage *contentMessage = [AIContentMessage messageInChat:chat
  1666 																		withSource:source
  1667 																	   destination:destination
  1668 																			  date:date
  1669 																		   message:[self parseDirectMessage:text
  1670 																									 withID:[message objectForKey:TWITTER_DM_ID]
  1671 																								   fromUser:chat.listObject.UID]
  1672 																		 autoreply:NO];
  1673 				
  1674 				[adium.contentController receiveContentObject:contentMessage];
  1675 			}
  1676 		}
  1677 		
  1678 		[*unsortedArray removeAllObjects];
  1679 	}
  1680 }
  1681 
  1682 #pragma mark MGTwitterEngine Delegate Methods
  1683 /*!
  1684  * @brief A request was successful
  1685  *
  1686  * We only care about requests succeeding if they aren't specifically handled in another location.
  1687  */
  1688 - (void)requestSucceeded:(NSString *)identifier
  1689 {
  1690 	// If a request succeeds and we think we're offline, call ourselves online.
  1691 	if ([self requestTypeForRequestID:identifier] == AITwitterDisconnect) {
  1692 		[self didDisconnect];
  1693 	} else if ([self requestTypeForRequestID:identifier] == AITwitterRemoveFollow) {
  1694 		AIListContact *listContact = [[self dictionaryForRequestID:identifier] objectForKey:@"ListContact"];
  1695 		
  1696 		for (NSString *groupName in listContact.remoteGroupNames) {
  1697 			[listContact removeRemoteGroupName:groupName];
  1698 		}
  1699 	} else if ([self requestTypeForRequestID:identifier] == AITwitterDestroyStatus) {
  1700 		AIChat *timelineChat = self.timelineChat;
  1701 		
  1702 		[adium.contentController displayEvent:AILocalizedString(@"Your tweet has been successfully deleted.", nil)
  1703 									  ofType:@"delete"
  1704 									  inChat:timelineChat];
  1705 	} else if ([self requestTypeForRequestID:identifier] == AITwitterDestroyDM) {
  1706 		AIListContact *contact = [[self dictionaryForRequestID:identifier] objectForKey:@"ListContact"];
  1707 		AIChat *chat = [adium.chatController chatWithContact:contact];
  1708 		
  1709 		[adium.contentController displayEvent:AILocalizedString(@"The direct message has been successfully deleted.", nil)
  1710 									   ofType:@"delete"
  1711 									   inChat:chat];		
  1712 	}
  1713 }
  1714 
  1715 /*!
  1716  * @brief A request failed
  1717  *
  1718  * If it's a fatal error, we need to kill the session and retry. Otherwise, twitter's reliability is
  1719  * pretty terrible, so let's ignore errors for the most part.
  1720  */
  1721 - (void)requestFailed:(NSString *)identifier withError:(NSError *)error
  1722 {	
  1723 	switch ([self requestTypeForRequestID:identifier]) {
  1724 		case AITwitterDirectMessageSend:
  1725 		case AITwitterSendUpdate:
  1726 		{
  1727 			AIChat	*chat = [[self dictionaryForRequestID:identifier] objectForKey:@"Chat"];
  1728 			
  1729 			if (chat) {
  1730 				[chat receivedError:[NSNumber numberWithInt:AIChatMessageSendingConnectionError]];
  1731 				
  1732 				AILogWithSignature(@"%@ Chat send error on %@", self, chat);
  1733 			}
  1734 			break;
  1735 		}
  1736 			
  1737 		case AITwitterDisconnect:
  1738 			[self didDisconnect];
  1739 			break;
  1740 			
  1741 		case AITwitterInitialUserInfo:
  1742 			[self setLastDisconnectionError:AILocalizedString(@"Unable to retrieve user list [fail]", "Message when a (vital) twitter request to retrieve the follow list fails")];
  1743 			[self didDisconnect];
  1744 			break;
  1745 			
  1746 		case AITwitterUserIconPull:
  1747 		{
  1748 			AIListContact *listContact = [[self dictionaryForRequestID:identifier] objectForKey:@"ListContact"];
  1749 			
  1750 			// Image pull failed, flag ourselves as needing to try again.
  1751 			[listContact setValue:nil forProperty:TWITTER_PROPERTY_REQUESTED_USER_ICON notify:NotifyNever];
  1752 			break;
  1753 		}
  1754 			
  1755 		case AITwitterUpdateFollowedTimeline:
  1756 		case AITwitterUpdateReplies:
  1757 		{
  1758 			AIChat *timelineChat = [adium.chatController existingChatWithName:self.timelineChatName
  1759 																	onAccount:self];
  1760 			
  1761 			// Only print an error if the user already has the timeline open. Beyond annoying if we pop it open just to say "lol error"
  1762 			if (timelineChat && !timelineErrorMessagePrinted) {
  1763 				AIContentEvent *content = [AIContentEvent eventInChat:timelineChat
  1764 														   withSource:nil
  1765 														  destination:self
  1766 																 date:[NSDate date]
  1767 															  message:[NSAttributedString stringWithString:[NSString stringWithFormat:AILocalizedString(@"Unable to update timeline: %@", nil),
  1768 																											[self errorMessageForError:error]]]
  1769 															 withType:@"error"];
  1770 				
  1771 				content.postProcessContent = NO;
  1772 				content.coalescingKey = @"error";
  1773 				
  1774 				[adium.contentController receiveContentObject:content];
  1775 				
  1776 				// This gets reset to NO the next a periodic update fires.
  1777 				timelineErrorMessagePrinted = YES;
  1778 			}
  1779 			
  1780 			--pendingUpdateCount;
  1781 			break;
  1782 		}
  1783 			
  1784 		case AITwitterUpdateDirectMessage:
  1785 			--pendingUpdateCount;
  1786 			break;
  1787 			
  1788 		case AITwitterAddFollow:
  1789 			if(error.code == 404) {
  1790 				[adium.interfaceController handleErrorMessage:AILocalizedString(@"Unable to Add Contact", nil)
  1791 											  withDescription:[NSString stringWithFormat:AILocalizedString(@"Unable to add %@ to account %@, the user does not exist.", nil),
  1792 															   [[self dictionaryForRequestID:identifier] objectForKey:@"UID"],
  1793 															   self.explicitFormattedUID]];
  1794 			} else {
  1795 				[adium.interfaceController handleErrorMessage:AILocalizedString(@"Unable to Add Contact", nil)
  1796 											  withDescription:[NSString stringWithFormat:AILocalizedString(@"Unable to add %@ to account %@. %@",nil),
  1797 															   [[self dictionaryForRequestID:identifier] objectForKey:@"UID"],
  1798 															   self.explicitFormattedUID,
  1799 															   [self errorMessageForError:error]]];
  1800 			}
  1801 			break;
  1802 			
  1803 		case AITwitterRemoveFollow:
  1804 			[adium.interfaceController handleErrorMessage:AILocalizedString(@"Unable to Remove Contact", nil)
  1805 										  withDescription:[NSString stringWithFormat:AILocalizedString(@"Unable to remove %@ on account %@. %@", nil),
  1806 														   ((AIListContact *)[[self dictionaryForRequestID:identifier] objectForKey:@"ListContact"]).UID,
  1807 														   self.explicitFormattedUID,
  1808 														   [self errorMessageForError:error]]];
  1809 			break;
  1810 			
  1811 		case AITwitterValidateCredentials:
  1812 			if(error.code == 401) {	
  1813 				if(self.useOAuth) {
  1814 					[self setPasswordTemporarily:nil];
  1815 					[self setLastDisconnectionError:TWITTER_OAUTH_NOT_AUTHORIZED];
  1816 					
  1817 					[[NSNotificationCenter defaultCenter] postNotificationName:@"AIEditAccount"
  1818 																		object:self];
  1819 					
  1820 				} else {
  1821 					[self setLastDisconnectionError:TWITTER_INCORRECT_PASSWORD_MESSAGE];
  1822 					[self serverReportedInvalidPassword];
  1823 				}
  1824 				
  1825 				[self didDisconnect];
  1826 			} else {
  1827 				[self setLastDisconnectionError:AILocalizedString(@"Unable to validate credentials", nil)];
  1828 				[self didDisconnect];
  1829 			}
  1830 			break;
  1831 			
  1832 		case AITwitterFavoriteYes:
  1833 		case AITwitterFavoriteNo:
  1834 		{
  1835 			AIChat *timelineChat = self.timelineChat;
  1836 
  1837 			if (error.code == 403) {
  1838 				// We've attempted to add or remove when we already have it marked as such. Try the opposite.
  1839 				BOOL addAsFavorite = ([self requestTypeForRequestID:identifier] == AITwitterFavoriteNo);
  1840 				NSString *tweetID = [[self dictionaryForRequestID:identifier] objectForKey:@"tweetID"];
  1841 				
  1842 				NSString *requestID = [twitterEngine markUpdate:tweetID
  1843 													 asFavorite:addAsFavorite];
  1844 				
  1845 				if (requestID) {
  1846 					[self setRequestType:(addAsFavorite ? AITwitterFavoriteYes : AITwitterFavoriteNo)
  1847 							forRequestID:requestID
  1848 						  withDictionary:[NSDictionary dictionaryWithObjectsAndKeys:tweetID, @"tweetID", nil]];
  1849 				} else {
  1850 					[adium.contentController displayEvent:AILocalizedString(@"Attempt to favorite tweet failed to connect.", nil)
  1851 												   ofType:@"favorite"
  1852 												   inChat:timelineChat];
  1853 				}
  1854 			} else {
  1855 				[adium.contentController displayEvent:[NSString stringWithFormat:AILocalizedString(@"Attempt to favorite tweet failed. %@", nil), [self errorMessageForError:error]]
  1856 											   ofType:@"favorite"
  1857 											   inChat:timelineChat];				
  1858 			}
  1859 			
  1860 			break;
  1861 		}
  1862 			
  1863 		case AITwitterNotificationEnable:
  1864 		case AITwitterNotificationDisable:
  1865 		{
  1866 			BOOL			enableNotification = ([self requestTypeForRequestID:identifier] == AITwitterNotificationEnable);
  1867 			AIListContact	*listContact = [[self dictionaryForRequestID:identifier] objectForKey:@"ListContact"];
  1868 			
  1869 			[adium.interfaceController handleErrorMessage:(enableNotification ?
  1870 														   AILocalizedString(@"Unable to Enable Notifications", nil) :
  1871 														   AILocalizedString(@"Unable to Disable Notifications", nil))
  1872 										  withDescription:[NSString stringWithFormat:AILocalizedString(@"Cannot change notification setting for %@. %@", nil), listContact.UID, [self errorMessageForError:error]]];
  1873 			break;
  1874 		}
  1875 			
  1876 		case AITwitterDestroyStatus:
  1877 		{
  1878 			AIChat *timelineChat = self.timelineChat;
  1879 			
  1880 			[adium.contentController displayEvent:[NSString stringWithFormat:AILocalizedString(@"Your tweet failed to delete. %@", nil), [self errorMessageForError:error]]
  1881 										   ofType:@"delete"
  1882 										   inChat:timelineChat];
  1883 			break;
  1884 		}
  1885 			
  1886 		case AITwitterDestroyDM:
  1887 		{
  1888 				AIListContact *contact = [[self dictionaryForRequestID:identifier] objectForKey:@"ListContact"];
  1889 				AIChat *chat = [adium.chatController chatWithContact:contact];
  1890 				
  1891 				[adium.contentController displayEvent:[NSString stringWithFormat:AILocalizedString(@"The direct message failed to delete. %@", nil), [self errorMessageForError:error]]
  1892 											   ofType:@"delete"
  1893 											   inChat:chat];	
  1894 			break;
  1895 		}
  1896 			
  1897 		case AITwitterUnknownType:
  1898 		case AITwitterRateLimitStatus:
  1899 		case AITwitterProfileSelf:
  1900 		case AITwitterSelfUserIconPull:
  1901 		case AITwitterProfileUserInfo:
  1902 		case AITwitterProfileStatusUpdates:
  1903 			// While we don't handle the errors, it's a good idea to not have a "default" just to prevent accidentally letting something
  1904 			// we should really handle slip through.
  1905 			break;
  1906 
  1907 	}
  1908 	
  1909 	AILogWithSignature(@"%@ Request failed (%@ - %u) - %@", self, identifier, [self requestTypeForRequestID:identifier], error);
  1910 	
  1911 	[self clearRequestTypeForRequestID:identifier];
  1912 }
  1913 
  1914 /*!
  1915  * @brief Status updates received
  1916  */
  1917 - (void)statusesReceived:(NSArray *)statuses forRequest:(NSString *)identifier
  1918 {
  1919 	if([self requestTypeForRequestID:identifier] == AITwitterUpdateFollowedTimeline ||
  1920 	   [self requestTypeForRequestID:identifier] == AITwitterUpdateReplies) {
  1921 		NSString *lastID;
  1922 		
  1923 		BOOL nextPageNecessary = NO;
  1924 		
  1925 		if([self requestTypeForRequestID:identifier] == AITwitterUpdateFollowedTimeline) {
  1926 			lastID = [self preferenceForKey:TWITTER_PREFERENCE_TIMELINE_LAST_ID
  1927 									  group:TWITTER_PREFERENCE_GROUP_UPDATES];
  1928 			
  1929 			nextPageNecessary = (lastID && statuses.count >= TWITTER_UPDATE_TIMELINE_COUNT - 5);
  1930 		} else {
  1931 			lastID = [self preferenceForKey:TWITTER_PREFERENCE_REPLIES_LAST_ID
  1932 									  group:TWITTER_PREFERENCE_GROUP_UPDATES];
  1933 			
  1934 			nextPageNecessary = (lastID && statuses.count >= TWITTER_UPDATE_REPLIES_COUNT - 5);
  1935 		}
  1936 		
  1937 		// Store the largest tweet ID we find; this will be our "last ID" the next time we run.
  1938 		NSString *largestTweet = [[self dictionaryForRequestID:identifier] objectForKey:@"LargestTweet"];
  1939 		
  1940 		// The largest ID is first, compare.
  1941 		if (statuses.count) {
  1942 			NSString *tweetID = [[statuses objectAtIndex:0] objectForKey:TWITTER_STATUS_ID];
  1943 			if (!largestTweet || [largestTweet compare:tweetID options:NSNumericSearch] == NSOrderedAscending) {
  1944 				largestTweet = tweetID;
  1945 			}
  1946 		}
  1947 		
  1948 		[queuedUpdates addObjectsFromArray:statuses];
  1949 		
  1950 		AILogWithSignature(@"%@ Last ID: %@ Largest Tweet: %@ Next Page Necessary: %d", self, lastID, largestTweet, nextPageNecessary);
  1951 		
  1952 		// See if we need to pull more updates.
  1953 		if (nextPageNecessary) {
  1954 			NSInteger	nextPage = [[[self dictionaryForRequestID:identifier] objectForKey:@"Page"] intValue] + 1;
  1955 			NSString	*requestID;
  1956 			
  1957 			if ([self requestTypeForRequestID:identifier] == AITwitterUpdateFollowedTimeline) {
  1958 				requestID = [twitterEngine getFollowedTimelineFor:nil
  1959 														  sinceID:lastID
  1960 												   startingAtPage:nextPage
  1961 															count:TWITTER_UPDATE_TIMELINE_COUNT];
  1962 				
  1963 				AILogWithSignature(@"%@ Pulling additional timeline page %d", self, nextPage);
  1964 				
  1965 				if (requestID) {
  1966 					[self setRequestType:AITwitterUpdateFollowedTimeline
  1967 							forRequestID:requestID
  1968 						  withDictionary:[NSDictionary dictionaryWithObjectsAndKeys:[NSNumber numberWithInt:nextPage], @"Page", 
  1969 										  largestTweet, @"LargestTweet", nil]];
  1970 				} else {
  1971 					// Gracefully fail: remove all stored objects.
  1972 					AILogWithSignature(@"%@ Immediate timeline fail", self);
  1973 					--pendingUpdateCount;
  1974 					[queuedUpdates removeAllObjects];
  1975 				}
  1976 				
  1977 			} else if ([self requestTypeForRequestID:identifier] == AITwitterUpdateReplies) {
  1978 				requestID = [twitterEngine getRepliesSinceID:lastID startingAtPage:nextPage];
  1979 				
  1980 				AILogWithSignature(@"%@ Pulling additional replies page %d", self, nextPage);
  1981 				
  1982 				if (requestID) {
  1983 					[self setRequestType:AITwitterUpdateReplies
  1984 							forRequestID:requestID
  1985 						  withDictionary:[NSDictionary dictionaryWithObjectsAndKeys:[NSNumber numberWithInt:nextPage], @"Page",
  1986 										  largestTweet, @"LargestTweet", nil]];
  1987 				} else {
  1988 					// Gracefully fail: remove all stored objects.
  1989 					AILogWithSignature(@"%@ Immediate reply fail", self);
  1990 					--pendingUpdateCount;
  1991 					[queuedUpdates removeAllObjects];
  1992 				}
  1993 			}
  1994 		} else {
  1995 			if([self requestTypeForRequestID:identifier] == AITwitterUpdateFollowedTimeline) {
  1996 				followedTimelineCompleted = YES;
  1997 				futureTimelineLastID = [largestTweet retain];
  1998 			} else if ([self requestTypeForRequestID:identifier] == AITwitterUpdateReplies) {
  1999 				repliesCompleted = YES;
  2000 				futureRepliesLastID = [largestTweet retain];
  2001 			}
  2002 			
  2003 			--pendingUpdateCount;
  2004 			
  2005 			AILogWithSignature(@"%@ Followed completed: %d Replies completed: %d", self, followedTimelineCompleted, repliesCompleted);
  2006 			
  2007 			if (followedTimelineCompleted && repliesCompleted) {
  2008 				if (queuedUpdates.count) {
  2009 					// Set the "last pulled" for the timeline and replies, since we've completed both.
  2010 					if(futureRepliesLastID) {
  2011 						AILogWithSignature(@"%@ futureRepliesLastID = %@", self, futureRepliesLastID);
  2012 						
  2013 						[self setPreference:futureRepliesLastID
  2014 									 forKey:TWITTER_PREFERENCE_REPLIES_LAST_ID
  2015 									  group:TWITTER_PREFERENCE_GROUP_UPDATES];
  2016 						
  2017 						[futureRepliesLastID release]; futureRepliesLastID = nil;
  2018 					}
  2019 					
  2020 					if(futureTimelineLastID) {
  2021 						AILogWithSignature(@"%@ futureTimelineLastID = %@", self, futureTimelineLastID);
  2022 						
  2023 						[self setPreference:futureTimelineLastID
  2024 									 forKey:TWITTER_PREFERENCE_TIMELINE_LAST_ID
  2025 									  group:TWITTER_PREFERENCE_GROUP_UPDATES];
  2026 						
  2027 						[futureTimelineLastID release]; futureTimelineLastID = nil;
  2028 					}
  2029 					
  2030 					[self displayQueuedUpdatesForRequestType:[self requestTypeForRequestID:identifier]];
  2031 				}
  2032 
  2033 				if (![self preferenceForKey:TWITTER_PREFERENCE_EVER_LOADED_TIMELINE group:TWITTER_PREFERENCE_GROUP_UPDATES]) {
  2034 					[self setPreference:[NSNumber numberWithBool:YES]
  2035 								 forKey:TWITTER_PREFERENCE_EVER_LOADED_TIMELINE
  2036 								  group:TWITTER_PREFERENCE_GROUP_UPDATES];
  2037 				}
  2038 			}
  2039 		}
  2040 	} else if ([self requestTypeForRequestID:identifier] == AITwitterProfileStatusUpdates) {
  2041 		AIListContact *listContact = [[self dictionaryForRequestID:identifier] objectForKey:@"ListContact"];
  2042 
  2043 		NSMutableArray *profileArray = [[listContact profileArray] mutableCopy];
  2044 		
  2045 		AILogWithSignature(@"%@ Updating statuses for profile, user %@", self, listContact);
  2046 		
  2047 		for (NSDictionary *update in statuses) {
  2048 			NSAttributedString *message = [self parseMessage:[update objectForKey:TWITTER_STATUS_TEXT]
  2049 													 tweetID:[update objectForKey:TWITTER_STATUS_ID]
  2050 													  userID:listContact.UID
  2051 											   inReplyToUser:[update objectForKey:TWITTER_STATUS_REPLY_UID]
  2052 											inReplyToTweetID:[update objectForKey:TWITTER_STATUS_REPLY_ID]];
  2053 			
  2054 			[profileArray addObject:[NSDictionary dictionaryWithObjectsAndKeys:message, KEY_VALUE, nil]];
  2055 		}
  2056 		
  2057 		[listContact setProfileArray:profileArray notify:NotifyNow];
  2058 	} else if ([self requestTypeForRequestID:identifier] == AITwitterSendUpdate) {
  2059 		if (updateAfterSend) {
  2060 			[self periodicUpdate];
  2061 		}
  2062 		
  2063 		if (statuses.count) {
  2064 			[adium.contentController displayEvent:AILocalizedString(@"Tweet successfully sent.", nil)
  2065 										   ofType:@"tweet"
  2066 										  inChat:self.timelineChat];
  2067 		}
  2068 				
  2069 		for(NSDictionary *update in statuses) {
  2070 			[[NSNotificationCenter defaultCenter] postNotificationName:AITwitterNotificationPostedStatus
  2071 																object:update
  2072 															  userInfo:[NSDictionary dictionaryWithObjectsAndKeys:self.timelineChat, @"AIChat", nil]];
  2073 			
  2074 			NSString *text = [[update objectForKey:TWITTER_STATUS_TEXT] stringByUnescapingFromXMLWithEntities:nil];
  2075 			
  2076 			if([[self preferenceForKey:TWITTER_PREFERENCE_UPDATE_GLOBAL group:TWITTER_PREFERENCE_GROUP_UPDATES] boolValue] &&
  2077 			   (![text hasPrefix:@"@"] || [[self preferenceForKey:TWITTER_PREFERENCE_UPDATE_GLOBAL_REPLIES group:TWITTER_PREFERENCE_GROUP_UPDATES] boolValue])) {
  2078 				AIStatus *availableStatus = [AIStatus statusOfType:AIAvailableStatusType];
  2079 				
  2080 				availableStatus.statusMessage = [NSAttributedString stringWithString:text];
  2081 				[adium.statusController setActiveStatusState:availableStatus];
  2082 			}
  2083 		}
  2084 	} else if ([self requestTypeForRequestID:identifier] == AITwitterFavoriteYes ||
  2085 			   [self requestTypeForRequestID:identifier] == AITwitterFavoriteNo) {
  2086 		AIChat *timelineChat = self.timelineChat;
  2087 
  2088 		for (NSDictionary *status in statuses) {
  2089 			NSString *message;
  2090 			
  2091 			// Use HTML for the status message since it's just easier to localize that way.
  2092 			
  2093 			if ([self requestTypeForRequestID:identifier] == AITwitterFavoriteYes) {
  2094 				message = AILocalizedString(@"The <a href=\"%@\">requested tweet</a> by <a href=\"%@\">%@</a> is now a favorite.", nil);
  2095 			} else {
  2096 				message = AILocalizedString(@"The <a href=\"%@\">requested tweet</a> by <a href=\"%@\">%@</a> is no longer a favorite.", nil);
  2097 			}
  2098 
  2099 			NSString *userID = [[status objectForKey:TWITTER_STATUS_USER] objectForKey:TWITTER_STATUS_UID];
  2100 			
  2101 			
  2102 			message = [NSString stringWithFormat:message,
  2103 					   [self addressForLinkType:AITwitterLinkStatus
  2104 										 userID:userID
  2105 									   statusID:[status objectForKey:TWITTER_STATUS_ID]
  2106 										context:nil],
  2107 					   [self addressForLinkType:AITwitterLinkUserPage
  2108 										 userID:userID
  2109 									   statusID:nil
  2110 										context:nil],
  2111 					   userID];
  2112 			
  2113 			NSAttributedString *attributedMessage = [[AIHTMLDecoder decoder] decodeHTML:message withDefaultAttributes:nil];
  2114 			
  2115 			AIContentEvent *content = [AIContentEvent eventInChat:timelineChat
  2116 													   withSource:nil
  2117 													  destination:self
  2118 															 date:[NSDate date]
  2119 														  message:attributedMessage
  2120 														 withType:@"favorite"];
  2121 			
  2122 			content.postProcessContent = NO;
  2123 			content.coalescingKey = @"favorite";
  2124 
  2125 			[adium.contentController receiveContentObject:content];
  2126 		}
  2127 	}
  2128 	
  2129 	[self clearRequestTypeForRequestID:identifier];
  2130 }
  2131 
  2132 /*!
  2133  * @brief Direct messages received
  2134  */
  2135 - (void)directMessagesReceived:(NSArray *)messages forRequest:(NSString *)identifier
  2136 {	
  2137 	if ([self requestTypeForRequestID:identifier] == AITwitterUpdateDirectMessage) {		
  2138 		NSString *lastID = [self preferenceForKey:TWITTER_PREFERENCE_DM_LAST_ID
  2139 											group:TWITTER_PREFERENCE_GROUP_UPDATES];
  2140 		
  2141 		BOOL nextPageNecessary = (lastID && messages.count >= TWITTER_UPDATE_DM_COUNT);
  2142 		
  2143 		// Store the largest tweet ID we find; this will be our "last ID" the next time we run.
  2144 		NSString *largestTweet = [[self dictionaryForRequestID:identifier] objectForKey:@"LargestTweet"];
  2145 		
  2146 		// The largest ID is first, compare.
  2147 		if (messages.count) {
  2148 			NSString *tweetID = [[messages objectAtIndex:0] objectForKey:TWITTER_DM_ID];
  2149 			if (!largestTweet || [largestTweet compare:tweetID] == NSOrderedAscending) {
  2150 				largestTweet = tweetID;
  2151 			}
  2152 		}
  2153 		
  2154 		[queuedDM addObjectsFromArray:messages];
  2155 		
  2156 		AILogWithSignature(@"%@ Last ID: %@ Largest Tweet: %@ Next Page Necessary: %d", self, lastID, largestTweet, nextPageNecessary);
  2157 		
  2158 		if(nextPageNecessary) {
  2159 			NSInteger	nextPage = [[[self dictionaryForRequestID:identifier] objectForKey:@"Page"] intValue] + 1;
  2160 			
  2161 			NSString	*requestID = [twitterEngine getDirectMessagesSinceID:lastID
  2162 														      startingAtPage:nextPage];
  2163 			
  2164 			AILogWithSignature(@"%@ Pulling additional DM page %d", self, nextPage);
  2165 			
  2166 			if(requestID) {
  2167 				[self setRequestType:AITwitterUpdateDirectMessage
  2168 						forRequestID:requestID
  2169 					  withDictionary:[NSDictionary dictionaryWithObjectsAndKeys:[NSNumber numberWithInt:nextPage], @"Page",
  2170 									  largestTweet, @"LargestTweet", nil]];
  2171 			} else {
  2172 				// Gracefully fail: remove all stored objects.
  2173 				AILogWithSignature(@"%@ Immediate DM pull fail", self);
  2174 				--pendingUpdateCount;
  2175 				[queuedDM removeAllObjects];
  2176 			}
  2177 		} else {		
  2178 			--pendingUpdateCount;
  2179 			
  2180 			if (largestTweet) {
  2181 				AILogWithSignature(@"%@ Largest DM pulled = %@", self, largestTweet);
  2182 				
  2183 				[self setPreference:largestTweet
  2184 							 forKey:TWITTER_PREFERENCE_DM_LAST_ID
  2185 							  group:TWITTER_PREFERENCE_GROUP_UPDATES];
  2186 			}
  2187 		
  2188 			// On first load, don't display any direct messages. Just ge the largest ID.
  2189 			if (queuedDM.count && lastID) {
  2190 				[self displayQueuedUpdatesForRequestType:[self requestTypeForRequestID:identifier]];
  2191 			} else {
  2192 				[queuedDM removeAllObjects];		
  2193 			}
  2194 		}
  2195 	} else if ([self requestTypeForRequestID:identifier] == AITwitterDirectMessageSend) {
  2196 		[queuedOutgoingDM addObjectsFromArray:messages];
  2197 		[self displayQueuedUpdatesForRequestType:AITwitterDirectMessageSend];
  2198 	}
  2199 	
  2200 	[self clearRequestTypeForRequestID:identifier];
  2201 }
  2202 
  2203 /*!
  2204  * @brief User information received
  2205  */
  2206 - (void)userInfoReceived:(NSArray *)userInfo forRequest:(NSString *)identifier
  2207 {	
  2208 	if (([self requestTypeForRequestID:identifier] == AITwitterInitialUserInfo ||
  2209 		 [self requestTypeForRequestID:identifier] == AITwitterAddFollow) &&
  2210 		[[self preferenceForKey:TWITTER_PREFERENCE_LOAD_CONTACTS group:TWITTER_PREFERENCE_GROUP_UPDATES] boolValue]) {
  2211 		[[AIContactObserverManager sharedManager] delayListObjectNotifications];
  2212 		
  2213 		// The current amount of friends per page is 100. Use >= just in case this changes.
  2214 		BOOL nextPageNecessary = (userInfo.count >= 100);
  2215 		
  2216 		AILogWithSignature(@"%@ User info pull, Next page necessary: %d Count: %d", self, nextPageNecessary, userInfo.count);
  2217 		
  2218 		for (NSDictionary *info in userInfo) {
  2219 			AIListContact *listContact = [self contactWithUID:[info objectForKey:TWITTER_INFO_UID]];
  2220 			
  2221 			// If the user isn't in a group, set them in the Twitter group.
  2222 			if(listContact.countOfRemoteGroupNames == 0) {
  2223 				[listContact addRemoteGroupName:TWITTER_REMOTE_GROUP_NAME];
  2224 			}
  2225 		
  2226 			// Grab the Twitter display name and set it as the remote alias.
  2227 			if (![[listContact valueForProperty:@"Server Display Name"] isEqualToString:[info objectForKey:TWITTER_INFO_DISPLAY_NAME]]) {
  2228 				[listContact setServersideAlias:[info objectForKey:TWITTER_INFO_DISPLAY_NAME]
  2229 									   silently:silentAndDelayed];
  2230 			}
  2231 			
  2232 			// Grab the user icon and set it as their serverside icon.
  2233 			[self updateUserIcon:[info objectForKey:TWITTER_INFO_ICON] forContact:listContact];
  2234 			
  2235 			// Set the user as available.
  2236 			[listContact setStatusWithName:nil
  2237 								statusType:AIAvailableStatusType
  2238 									notify:NotifyLater];
  2239 			
  2240 			// Set the user's status message to their current twitter status text
  2241 			NSString *statusText = [[info objectForKey:TWITTER_INFO_STATUS] objectForKey:TWITTER_INFO_STATUS_TEXT];
  2242 			if (!statusText) //nil if they've never tweeted
  2243 				statusText = @"";
  2244 			[listContact setStatusMessage:[NSAttributedString stringWithString:[statusText stringByUnescapingFromXMLWithEntities:nil]] notify:NotifyLater];
  2245 			
  2246 			// Set the user as online.
  2247 			[listContact setOnline:YES notify:NotifyLater silently:silentAndDelayed];
  2248 			
  2249 			[listContact notifyOfChangedPropertiesSilently:silentAndDelayed];
  2250 		}
  2251 		
  2252 		[[AIContactObserverManager sharedManager] endListObjectNotificationsDelay];
  2253 		
  2254 		if (nextPageNecessary) {
  2255 			NSInteger	nextPage = [[[self dictionaryForRequestID:identifier] objectForKey:@"Page"] intValue] + 1;
  2256 			NSString	*requestID = [twitterEngine getRecentlyUpdatedFriendsFor:self.UID startingAtPage:nextPage];
  2257 			
  2258 			AILogWithSignature(@"%@ Pulling additional user info page %d", self, nextPage);
  2259 			
  2260 			if(requestID) {
  2261 				[self setRequestType:AITwitterInitialUserInfo
  2262 						forRequestID:requestID
  2263 					  withDictionary:[NSDictionary dictionaryWithObject:[NSNumber numberWithInt:nextPage]
  2264 																 forKey:@"Page"]];
  2265 			} else { 
  2266 				[self setLastDisconnectionError:AILocalizedString(@"Unable to retrieve user list [additional fail]", "Message when a (vital) twitter request to retrieve the follow list fails")];
  2267 				[self didDisconnect];
  2268 			}
  2269 			
  2270 		} else if ([self valueForProperty:@"Connecting"]) {			
  2271 			// Trigger our normal update routine.
  2272 			[self didConnect];
  2273 		}
  2274 	} else if ([self requestTypeForRequestID:identifier] == AITwitterProfileUserInfo) {
  2275 		NSDictionary *thisUserInfo = [userInfo objectAtIndex:0];
  2276 		
  2277 		if (thisUserInfo) {	
  2278 			AIListContact	*listContact = [[self dictionaryForRequestID:identifier] objectForKey:@"ListContact"];
  2279 			
  2280 			NSArray *keyNames = [NSArray arrayWithObjects:@"name", @"location", @"description", @"url", @"friends_count", @"followers_count", @"statuses_count", nil];
  2281 			NSArray *readableNames = [NSArray arrayWithObjects:AILocalizedString(@"Name", nil), AILocalizedString(@"Location", nil),
  2282 									  AILocalizedString(@"Biography", nil), AILocalizedString(@"Website", nil), AILocalizedString(@"Following", nil),
  2283 									  AILocalizedString(@"Followers", nil), AILocalizedString(@"Updates", nil), nil];
  2284 			
  2285 			NSMutableArray *profileArray = [NSMutableArray array];
  2286 			
  2287 			for (NSUInteger index = 0; index < keyNames.count; index++) {
  2288 				NSString			*keyName = [keyNames objectAtIndex:index];
  2289 				NSString			*unattributedValue = [thisUserInfo objectForKey:keyName];
  2290 				
  2291 				if(![unattributedValue isEqualToString:@""]) {
  2292 					NSString			*readableName = [readableNames objectAtIndex:index];
  2293 					NSAttributedString	*value;
  2294 					
  2295 					if([keyName isEqualToString:@"friends_count"]) {
  2296 						value = [NSAttributedString attributedStringWithLinkLabel:unattributedValue
  2297 																  linkDestination:[self addressForLinkType:AITwitterLinkFriends userID:listContact.UID statusID:nil context:nil]];
  2298 					} else if ([keyName isEqualToString:@"followers_count"]) {
  2299 						value = [NSAttributedString attributedStringWithLinkLabel:unattributedValue
  2300 																  linkDestination:[self addressForLinkType:AITwitterLinkFollowers userID:listContact.UID statusID:nil context:nil]];
  2301 					} else if ([keyName isEqualToString:@"statuses_count"]) {
  2302 						value = [NSAttributedString attributedStringWithLinkLabel:unattributedValue
  2303 																  linkDestination:[self addressForLinkType:AITwitterLinkUserPage userID:listContact.UID statusID:nil context:nil]];
  2304 					} else {
  2305 						value = [NSAttributedString stringWithString:unattributedValue];
  2306 					}
  2307 						
  2308 					[profileArray addObject:[NSDictionary dictionaryWithObjectsAndKeys:readableName, KEY_KEY, value, KEY_VALUE, nil]];
  2309 				}
  2310 			}
  2311 			
  2312 			AILogWithSignature(@"%@ Updating profileArray for user %@", self, listContact);
  2313 			
  2314 			[listContact setProfileArray:profileArray notify:NotifyNow];
  2315 			
  2316 			// Grab their statuses.
  2317 			NSString *requestID = [twitterEngine getUserTimelineFor:listContact.UID since:nil startingAtPage:0 count:TWITTER_UPDATE_USER_INFO_COUNT];
  2318 			
  2319 			if (requestID) {
  2320 				[self setRequestType:AITwitterProfileStatusUpdates
  2321 						forRequestID:requestID
  2322 					  withDictionary:[NSDictionary dictionaryWithObject:listContact forKey:@"ListContact"]];
  2323 			}
  2324 		}
  2325 	} else if ([self requestTypeForRequestID:identifier] == AITwitterValidateCredentials ||
  2326 			   [self requestTypeForRequestID:identifier] == AITwitterProfileSelf) {
  2327 		for (NSDictionary *info in userInfo) {
  2328 			NSString *requestID = [twitterEngine getImageAtURL:[info objectForKey:TWITTER_INFO_ICON]];
  2329 			
  2330 			if (requestID) {
  2331 				[self setRequestType:AITwitterSelfUserIconPull
  2332 						forRequestID:requestID
  2333 					  withDictionary:nil];
  2334 			}
  2335 
  2336 			[self filterAndSetUID:[info objectForKey:TWITTER_INFO_UID]];
  2337 			
  2338 			if ([info objectForKey:@"name"]) {
  2339 				[self setPreference:[[NSAttributedString stringWithString:[info objectForKey:@"name"]] dataRepresentation]
  2340 								forKey:KEY_ACCOUNT_DISPLAY_NAME
  2341 								 group:GROUP_ACCOUNT_STATUS];		
  2342 			}
  2343 			
  2344 			[self setValue:[info objectForKey:@"name"] forProperty:@"Profile Name" notify:NotifyLater];
  2345 			[self setValue:[info objectForKey:@"url"] forProperty:@"Profile URL" notify:NotifyLater];
  2346 			[self setValue:[info objectForKey:@"location"] forProperty:@"Profile Location" notify:NotifyLater];
  2347 			[self setValue:[info objectForKey:@"description"] forProperty:@"Profile Description" notify:NotifyLater];
  2348 			[self notifyOfChangedPropertiesSilently:NO];
  2349 		}
  2350 		
  2351 		
  2352 		if([self requestTypeForRequestID:identifier] == AITwitterValidateCredentials) {
  2353 			// Our UID is definitely set; grab our friends.
  2354 			
  2355 			if ([[self preferenceForKey:TWITTER_PREFERENCE_LOAD_CONTACTS group:TWITTER_PREFERENCE_GROUP_UPDATES] boolValue]) {
  2356 				// If we load our follows as contacts, do so now.
  2357 				
  2358 				// Delay updates on initial login.
  2359 				[self silenceAllContactUpdatesForInterval:18.0];
  2360 				// Grab our user list.
  2361 				NSString	*requestID = [twitterEngine getRecentlyUpdatedFriendsFor:self.UID startingAtPage:1];
  2362 				
  2363 				if (requestID) {
  2364 					[self setRequestType:AITwitterInitialUserInfo
  2365 							forRequestID:requestID
  2366 						  withDictionary:[NSDictionary dictionaryWithObject:[NSNumber numberWithInt:1] forKey:@"Page"]];
  2367 				} else {
  2368 					[self setLastDisconnectionError:AILocalizedString(@"Unable to retrieve user list", nil)];
  2369 					[self didDisconnect];
  2370 				}
  2371 			} else {
  2372 				// If we don't load follows as contacts, we've finished connecting (fast, wasn't it?)
  2373 				[self didConnect];
  2374 			}
  2375 		}
  2376 	} else if ([self requestTypeForRequestID:identifier] == AITwitterNotificationEnable ||
  2377 			   [self requestTypeForRequestID:identifier] == AITwitterNotificationDisable) {
  2378 		BOOL			enableNotification = ([self requestTypeForRequestID:identifier] == AITwitterNotificationEnable);
  2379 		AIListContact	*listContact = [[self dictionaryForRequestID:identifier] objectForKey:@"ListContact"];
  2380 		
  2381 		for (NSDictionary *info in userInfo) {		
  2382 			[adium.interfaceController handleMessage:(enableNotification ?
  2383 													  AILocalizedString(@"Notifications Enabled", nil) :
  2384 													  AILocalizedString(@"Notifications Disabled", nil))
  2385 									 withDescription:[NSString stringWithFormat:(enableNotification ?
  2386 																				 AILocalizedString(@"You will now receive device notifications for %@.", nil) :
  2387 																				 AILocalizedString(@"You will no longer receive device notifications for %@.", nil)),
  2388 													  listContact.UID]
  2389 									 withWindowTitle:(enableNotification ?
  2390 													  AILocalizedString(@"Notifications Enabled", nil) :
  2391 													  AILocalizedString(@"Notifications Disabled", nil))];
  2392 		}
  2393 	}
  2394 	
  2395 	[self clearRequestTypeForRequestID:identifier];
  2396 }
  2397 
  2398 /*!
  2399  * @brief Miscellaneous information received
  2400  */
  2401 - (void)miscInfoReceived:(NSArray *)miscInfo forRequest:(NSString *)identifier
  2402 {
  2403 	if([self requestTypeForRequestID:identifier] == AITwitterRateLimitStatus) {
  2404 		NSDictionary *rateLimit = [miscInfo objectAtIndex:0];
  2405 		NSDate *resetDate = [NSDate dateWithTimeIntervalSince1970:[[rateLimit objectForKey:TWITTER_RATE_LIMIT_RESET_SECONDS] intValue]];
  2406 		
  2407 		[adium.interfaceController handleMessage:AILocalizedString(@"Current Twitter rate limit", "Message in the rate limit status window")
  2408 								 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."),
  2409 													[[rateLimit objectForKey:TWITTER_RATE_LIMIT_REMAINING] intValue],
  2410 													[[rateLimit objectForKey:TWITTER_RATE_LIMIT_HOURLY_LIMIT] intValue],
  2411 													[NSDateFormatter stringForTimeInterval:[resetDate timeIntervalSinceNow]
  2412 																			showingSeconds:YES
  2413 																			   abbreviated:YES
  2414 																			  approximated:NO]]
  2415 								 withWindowTitle:AILocalizedString(@"Rate Limit Status", nil)];
  2416 	}
  2417 	
  2418 	[self clearRequestTypeForRequestID:identifier];
  2419 }
  2420 
  2421 /*!
  2422  * @brief Requested image received
  2423  */
  2424 - (void)imageReceived:(NSImage *)image forRequest:(NSString *)identifier
  2425 {
  2426 	if([self requestTypeForRequestID:identifier] == AITwitterUserIconPull) {
  2427 		AIListContact		*listContact = [[self dictionaryForRequestID:identifier] objectForKey:@"ListContact"];
  2428 		
  2429 		AILogWithSignature(@"%@ Updated user icon for %@", self, listContact);
  2430 		
  2431 		[listContact setServersideIconData:[image TIFFRepresentation]
  2432 									notify:NotifyLater];
  2433 		
  2434 		[listContact setValue:nil forProperty:TWITTER_PROPERTY_REQUESTED_USER_ICON notify:NotifyNever];
  2435 	} else if([self requestTypeForRequestID:identifier] == AITwitterSelfUserIconPull) {
  2436 		AILogWithSignature(@"Updated self icon for %@", self);
  2437 
  2438 		// Set a property so we don't re-send thie image we're just now downloading.
  2439 		[self setValue:[NSNumber numberWithBool:YES] forProperty:TWITTER_PROPERTY_REQUESTED_USER_ICON notify:NotifyNever];
  2440 		
  2441 		[self setPreference:[NSNumber numberWithBool:YES]
  2442 					 forKey:KEY_USE_USER_ICON
  2443 					  group:GROUP_ACCOUNT_STATUS];
  2444 		
  2445 		
  2446 		[self setPreference:[image TIFFRepresentation]
  2447 					 forKey:KEY_USER_ICON
  2448 					  group:GROUP_ACCOUNT_STATUS];
  2449 	}
  2450 	
  2451 	[self clearRequestTypeForRequestID:identifier];
  2452 }
  2453 
  2454 @end