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

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