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