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