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