Don't allow a user to try and follow themselves. Fixes #12092.
2 * Adium is the legal property of its developers, whose names are listed in the copyright file included
3 * with this source distribution.
5 * This program is free software; you can redistribute it and/or modify it under the terms of the GNU
6 * General Public License as published by the Free Software Foundation; either version 2 of the License,
7 * or (at your option) any later version.
9 * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
10 * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
11 * Public License for more details.
13 * You should have received a copy of the GNU General Public License along with this program; if not,
14 * write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
17 #import "AITwitterAccount.h"
18 #import "AITwitterURLParser.h"
19 #import "AITwitterReplyWindowController.h"
20 #import "MGTwitterEngine/MGTwitterEngine.h"
21 #import <AIUtilities/AIAttributedStringAdditions.h>
22 #import <AIUtilities/AIStringAdditions.h>
23 #import <AIUtilities/AIMenuAdditions.h>
24 #import <AIUtilities/AIApplicationAdditions.h>
25 #import <AIUtilities/AIDateFormatterAdditions.h>
26 #import <Adium/AIChatControllerProtocol.h>
27 #import <Adium/AIContentControllerProtocol.h>
28 #import <Adium/AIContactControllerProtocol.h>
29 #import <Adium/AIStatusControllerProtocol.h>
30 #import <Adium/AIInterfaceControllerProtocol.h>
31 #import <Adium/AIAccountControllerProtocol.h>
32 #import <Adium/AIContactObserverManager.h>
33 #import <Adium/AIListContact.h>
34 #import <Adium/AIListGroup.h>
35 #import <Adium/AIContentMessage.h>
36 #import <Adium/AIListBookmark.h>
37 #import <Adium/AIChat.h>
38 #import <Adium/AIUserIcons.h>
39 #import <Adium/AIService.h>
40 #import <Adium/AIStatus.h>
41 #import <Adium/AIHTMLDecoder.h>
42 #import <Adium/AIContentEvent.h>
44 @interface AITwitterAccount()
45 - (void)updateUserIcon:(NSString *)url forContact:(AIListContact *)listContact;
47 - (void)updateTimelineChat:(AIChat *)timelineChat;
49 - (NSAttributedString *)parseMessage:(NSString *)inMessage
50 tweetID:(NSString *)tweetID
51 userID:(NSString *)userID
52 inReplyToUser:(NSString *)replyUserID
53 inReplyToTweetID:(NSString *)replyTweetID;
54 - (NSAttributedString *)parseDirectMessage:(NSString *)inMessage
55 withID:(NSString *)dmID
56 fromUser:(NSString *)sourceUID;
57 - (NSAttributedString *)attributedStringWithLinkLabel:(NSString *)label
58 linkDestination:(NSString *)destination
59 linkClass:(NSString *)attributeName;
61 - (void)setRequestType:(AITwitterRequestType)type forRequestID:(NSString *)requestID withDictionary:(NSDictionary *)info;
62 - (AITwitterRequestType)requestTypeForRequestID:(NSString *)requestID;
63 - (NSDictionary *)dictionaryForRequestID:(NSString *)requestID;
64 - (void)clearRequestTypeForRequestID:(NSString *)requestID;
66 - (void)periodicUpdate;
67 - (void)displayQueuedUpdatesForRequestType:(AITwitterRequestType)requestType;
69 - (void)getRateLimitAmount;
72 @implementation AITwitterAccount
77 pendingRequests = [[NSMutableDictionary alloc] init];
78 queuedUpdates = [[NSMutableArray alloc] init];
79 queuedDM = [[NSMutableArray alloc] init];
80 queuedOutgoingDM = [[NSMutableArray alloc] init];
82 [[NSNotificationCenter defaultCenter] addObserver:self
83 selector:@selector(chatDidOpen:)
87 [adium.preferenceController registerDefaults:[NSDictionary dictionaryWithObjectsAndKeys:
88 [NSNumber numberWithInt:TWITTER_UPDATE_INTERVAL_MINUTES], TWITTER_PREFERENCE_UPDATE_INTERVAL,
89 [NSNumber numberWithBool:YES], TWITTER_PREFERENCE_UPDATE_AFTER_SEND,
90 [NSNumber numberWithBool:YES], TWITTER_PREFERENCE_LOAD_CONTACTS, nil]
91 forGroup:TWITTER_PREFERENCE_GROUP_UPDATES
94 // If we don't have a server set, set our default (if we have one)
95 if (!self.host && self.defaultServer) {
96 [self setPreference:self.defaultServer forKey:KEY_CONNECT_HOST group:GROUP_ACCOUNT_STATUS];
99 [adium.preferenceController registerPreferenceObserver:self forGroup:TWITTER_PREFERENCE_GROUP_UPDATES];
100 [adium.preferenceController informObserversOfChangedKey:nil inGroup:TWITTER_PREFERENCE_GROUP_UPDATES object:self];
105 [[NSNotificationCenter defaultCenter] removeObserver:self];
106 [adium.preferenceController unregisterPreferenceObserver:self];
108 [twitterEngine release];
109 [pendingRequests release];
110 [queuedUpdates release];
112 [queuedOutgoingDM release];
118 * @brief Our default server if none is provided.
120 - (NSString *)defaultServer
122 return @"twitter.com";
125 #pragma mark AIAccount methods
127 * @brief We've been asked to connect.
129 * Sets our username and password for MGTwitterEngine, and validates credentials.
131 * Our connection procedure:
132 * 1. Validate credentials
133 * 2. Retrieve friends
134 * 3. Trigger "periodic" update - DM, replies, timeline
140 [twitterEngine release];
142 twitterEngine = [[MGTwitterEngine alloc] initWithDelegate:self];
144 [twitterEngine setClientName:@"Adium"
145 version:[NSApp applicationVersion]
146 URL:@"http://www.adiumx.com"
147 token:self.sourceToken];
149 [twitterEngine setAPIDomain:[self.host stringByAppendingPathComponent:self.apiPath]];
151 [twitterEngine setUsesSecureConnection:self.useSSL];
154 if (!self.passwordWhileConnected.length) {
155 [self setLastDisconnectionError:TWITTER_OAUTH_NOT_AUTHORIZED];
157 [[NSNotificationCenter defaultCenter] postNotificationName:@"AIEditAccount"
160 [self didDisconnect];
162 // Don't try and connect.
166 twitterEngine.useOAuth = YES;
168 OAToken *token = [[[OAToken alloc] initWithHTTPResponseBody:self.passwordWhileConnected] autorelease];
169 OAConsumer *consumer = [[[OAConsumer alloc] initWithKey:self.consumerKey secret:self.secretKey] autorelease];
171 twitterEngine.accessToken = token;
172 twitterEngine.consumer = consumer;
175 [twitterEngine setUsername:self.UID password:self.passwordWhileConnected];
178 AILogWithSignature(@"%@ connecting to %@", self, twitterEngine.APIDomain);
180 NSString *requestID = [twitterEngine checkUserCredentials];
183 [self setRequestType:AITwitterValidateCredentials forRequestID:requestID withDictionary:nil];
185 [self setLastDisconnectionError:AILocalizedString(@"Unable to connect to server", nil)];
186 [self didDisconnect];
191 * @brief Connection successful
193 * Our credentials were validated correctly. Set up the timeline chat, and request our friends from the server.
199 //Clear any previous disconnection error
200 [self setLastDisconnectionError:nil];
202 // Creating the fake timeline account.
203 AIListBookmark *timelineBookmark = nil;
205 if(!(timelineBookmark = [adium.contactController existingBookmarkForChatName:self.timelineChatName
207 chatCreationInfo:nil])) {
208 AIChat *newTimelineChat = [adium.chatController chatWithName:self.timelineChatName
211 chatCreationInfo:nil];
213 [newTimelineChat setDisplayName:self.timelineChatName];
215 timelineBookmark = [adium.contactController bookmarkForChat:newTimelineChat inGroup:[adium.contactController groupWithUID:TWITTER_REMOTE_GROUP_NAME]];
219 NSTimeInterval updateInterval = [[self preferenceForKey:TWITTER_PREFERENCE_UPDATE_INTERVAL group:TWITTER_PREFERENCE_GROUP_UPDATES] integerValue] * 60;
221 if(updateInterval > 0) {
222 [updateTimer invalidate];
223 updateTimer = [NSTimer scheduledTimerWithTimeInterval:updateInterval
225 selector:@selector(periodicUpdate)
229 [self periodicUpdate];
234 * @brief We've been asked to disconnect.
242 [twitterEngine release]; twitterEngine = nil;
243 [updateTimer invalidate]; updateTimer = nil;
245 [self didDisconnect];
249 * @brief Account will be deleted
251 - (void)willBeDeleted
253 [updateTimer invalidate]; updateTimer = nil;
255 [super willBeDeleted];
259 * @brief Session ended
261 * Remove all state information.
263 - (void)didDisconnect
265 [updateTimer invalidate]; updateTimer = nil;
266 [pendingRequests removeAllObjects];
267 [queuedDM removeAllObjects];
268 [queuedOutgoingDM removeAllObjects];
269 [queuedUpdates removeAllObjects];
271 [super didDisconnect];
277 * The API path extension for the given host.
279 - (NSString *)apiPath
285 * @brief Our source token
287 * On Twitter, our given source token is "adiumofficial".
289 - (NSString *)sourceToken
291 return @"adiumofficial";
295 * @brief Returns whether or not to connect to Twitter API over HTTPS.
303 * @brief Returns whether or not this account is connected via an encrypted connection.
307 return (self.online && [twitterEngine usesSecureConnection]);
311 * @brief Affirm we can open chats.
313 - (BOOL)openChat:(AIChat *)chat
315 [chat setValue:[NSNumber numberWithBool:YES] forProperty:@"Account Joined" notify:NotifyNow];
321 * @brief Allow all chats to close.
323 - (BOOL)closeChat:(AIChat *)inChat
329 * @brief Rejoin the requested chat.
331 - (BOOL)rejoinChat:(AIChat *)inChat
333 [self displayYouHaveConnectedInChat:inChat];
339 * @brief We always want to autocomplete the UID.
341 - (BOOL)chatShouldAutocompleteUID:(AIChat *)inChat
347 * @brief A chat opened.
349 * If this is a group chat which belongs to us, aka a timeline chat, set it up how we want it.
351 - (void)chatDidOpen:(NSNotification *)notification
353 AIChat *chat = [notification object];
355 if(chat.isGroupChat && chat.account == self) {
356 [self updateTimelineChat:chat];
361 * @brief We support adding and removing follows.
363 - (BOOL)contactListEditable
369 * @brief Move contacts
371 * Move existing contacts to a specific group on this account. The passed contacts should already exist somewhere on
373 * @param objects NSArray of AIListContact objects to remove
374 * @param group AIListGroup destination for contacts
376 - (void)moveListObjects:(NSArray *)objects oldGroups:(NSSet *)oldGroups toGroups:(NSSet *)groups
378 // XXX do twitter grouping
382 * @brief Rename a group
384 * Rename a group on this account.
385 * @param group AIListGroup to rename
386 * @param newName NSString name for the group
388 - (void)renameGroup:(AIListGroup *)group to:(NSString *)newName
390 // XXX do twitter grouping
394 * @brief For an invalid password, fail but don't try and reconnect or report it. We do it ourself.
396 - (AIReconnectDelayType)shouldAttemptReconnectAfterDisconnectionError:(NSString **)disconnectionError
398 AIReconnectDelayType reconnectDelayType = [super shouldAttemptReconnectAfterDisconnectionError:disconnectionError];
400 if ([*disconnectionError isEqualToString:TWITTER_INCORRECT_PASSWORD_MESSAGE]) {
401 reconnectDelayType = AIReconnectImmediately;
402 } else if ([*disconnectionError isEqualToString:TWITTER_OAUTH_NOT_AUTHORIZED]) {
403 reconnectDelayType = AIReconnectNeverNoMessage;
406 return reconnectDelayType;
410 * @brief Don't allow OTR encryption.
412 - (BOOL)allowSecureMessagingTogglingForChat:(AIChat *)inChat
418 * @brief Update our status
420 - (void)setSocialNetworkingStatusMessage:(NSAttributedString *)statusMessage
422 NSString *requestID = [twitterEngine sendUpdate:[statusMessage string]];
425 [self setRequestType:AITwitterSendUpdate
426 forRequestID:requestID
431 - (NSString *)encodedAttributedString:(NSAttributedString *)inAttributedString forListObject:(AIListObject *)inListObject
433 return [[inAttributedString attributedStringByConvertingLinksToURLStrings] string];
437 * @brief Send a message
439 * Sends a direct message to the user requested.
440 * If it fails to send, i.e. the request fails, an unknown error will occur.
441 * This is usually caused by the target not following the user.
443 - (BOOL)sendMessageObject:(AIContentMessage *)inContentMessage
447 if(inContentMessage.chat.isGroupChat) {
448 requestID = [twitterEngine sendUpdate:inContentMessage.encodedMessage
449 inReplyTo:[inContentMessage.chat valueForProperty:@"TweetInReplyToStatusID"]];
452 [self setRequestType:AITwitterSendUpdate
453 forRequestID:requestID
454 withDictionary:[NSDictionary dictionaryWithObject:inContentMessage.chat
457 inContentMessage.displayContent = NO;
459 AILogWithSignature(@"%@ Sending update [in reply to %@]: %@", self, [inContentMessage.chat valueForProperty:@"TweetInReplyToStatusID"], inContentMessage.encodedMessage);
463 requestID = [twitterEngine sendDirectMessage:inContentMessage.encodedMessage
464 to:inContentMessage.destination.UID];
467 [self setRequestType:AITwitterDirectMessageSend
468 forRequestID:requestID
469 withDictionary:[NSDictionary dictionaryWithObject:inContentMessage.chat
472 inContentMessage.displayContent = NO;
474 AILogWithSignature(@"%@ Sending DM to %@: %@", self, inContentMessage.destination.UID, inContentMessage.encodedMessage);
479 AILogWithSignature(@"%@ Message immediate fail.", self);
482 return (requestID != nil);
486 * @brief Trigger an info update
488 * This is called when the info inspector wants more information on a contact.
489 * Grab the user's profile information, set everything up accordingly in the user info method.
491 - (void)delayedUpdateContactStatus:(AIListContact *)inContact
497 NSString *requestID = [twitterEngine getUserInformationFor:inContact.UID];
500 [self setRequestType:AITwitterProfileUserInfo
501 forRequestID:requestID
502 withDictionary:[NSDictionary dictionaryWithObject:inContact forKey:@"ListContact"]];
507 * @brief Should an autoreply be sent to this message?
509 - (BOOL)shouldSendAutoreplyToMessage:(AIContentMessage *)message
515 * @brief Update the Twitter profile
517 - (void)setProfileName:(NSString *)name
519 location:(NSString *)location
520 description:(NSString *)description
522 NSString *requestID = [twitterEngine updateProfileName:name
526 description:description];
529 [self setRequestType:AITwitterProfileSelf
530 forRequestID:requestID
537 * @brief Should we store our password based on internal object ID?
539 * We only need to if we're using OAuth.
541 - (BOOL)useInternalObjectIDForPasswordName
543 return self.useOAuth;
547 * @brief Should we connect using OAuth?
549 * If enabled, the account view will display the OAuth setup. Basic authentication will not be used.
557 * @brief OAuth consumer key
559 - (NSString *)consumerKey
561 return @"amjYVOrzKpKkkHAsdEaClA";
565 * @brief OAuth secret key
567 - (NSString *)secretKey
569 return @"kvqM2CQsUO3J6NHctJVhTOzlKZ0k7FsTaR5NwakYU";
573 * @brief Token request URL
575 - (NSString *)tokenRequestURL
577 return @"https://twitter.com/oauth/request_token";
581 * @brief Token access URL
583 - (NSString *)tokenAccessURL
585 return @"https://twitter.com/oauth/access_token";
589 * @brief Token authorize URL
591 - (NSString *)tokenAuthorizeURL
593 return @"https://twitter.com/oauth/authorize";
596 #pragma mark Menu Items
598 * @brief Menu items for contact
600 * Returns an array of menu items for a contact on this account. This is the best place to add protocol-specific
601 * actions that aren't otherwise supported by Adium.
602 * @param inContact AIListContact for menu items
603 * @return NSArray of NSMenuItem instances for the passed contact
605 - (NSArray *)menuItemsForContact:(AIListContact *)inContact
607 NSMutableArray *menuItemArray = [NSMutableArray array];
609 NSMenuItem *menuItem;
611 NSImage *serviceIcon = [AIServiceIcons serviceIconForService:self.service
612 type:AIServiceIconSmall
613 direction:AIIconNormal];
615 menuItem = [[[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:[NSString stringWithFormat:AILocalizedString(@"Open %@'s user page",nil), inContact.UID]
617 action:@selector(openUserPage:)
618 keyEquivalent:@""] autorelease];
619 [menuItem setImage:serviceIcon];
620 [menuItem setRepresentedObject:inContact];
621 [menuItemArray addObject:menuItem];
623 menuItem = [[[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:[NSString stringWithFormat:AILocalizedString(@"Enable device notifications for %@",nil), inContact.UID]
625 action:@selector(enableOrDisableNotifications:)
626 keyEquivalent:@""] autorelease];
627 [menuItem setTag:YES];
628 [menuItem setImage:serviceIcon];
629 [menuItem setRepresentedObject:inContact];
630 [menuItemArray addObject:menuItem];
632 menuItem = [[[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:[NSString stringWithFormat:AILocalizedString(@"Disable device notifications for %@",nil), inContact.UID]
634 action:@selector(enableOrDisableNotifications:)
635 keyEquivalent:@""] autorelease];
636 [menuItem setTag:NO];
637 [menuItem setImage:serviceIcon];
638 [menuItem setRepresentedObject:inContact];
639 [menuItemArray addObject:menuItem];
641 return menuItemArray;
645 * @brief Open the represented objec'ts user page
647 - (void)openUserPage:(NSMenuItem *)menuItem
649 [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:[self addressForLinkType:AITwitterLinkUserPage
650 userID:((AIListContact *)menuItem.representedObject).UID
656 * @brief Enable or disable notifications for a contact.
658 * If the menuItem's tag is YES, we're adding. Otherwise we're removing.
660 - (void)enableOrDisableNotifications:(NSMenuItem *)menuItem
662 if(![menuItem.representedObject isKindOfClass:[AIListContact class]]) {
666 BOOL enableNotification = menuItem.tag;
667 AIListContact *contact = menuItem.representedObject;
669 NSString *requestID = nil;
671 BOOL initialFailure = NO;
673 if (enableNotification) {
674 requestID = [twitterEngine enableNotificationsFor:contact.UID];
677 [self setRequestType:AITwitterNotificationEnable
678 forRequestID:requestID
679 withDictionary:[NSDictionary dictionaryWithObjectsAndKeys:contact, @"ListContact", nil]];
681 initialFailure = YES;
685 requestID = [twitterEngine disableNotificationsFor:contact.UID];
688 [self setRequestType:AITwitterNotificationDisable
689 forRequestID:requestID
690 withDictionary:[NSDictionary dictionaryWithObjectsAndKeys:contact, @"ListContact", nil]];
692 initialFailure = YES;
696 if (initialFailure) {
697 [adium.interfaceController handleErrorMessage:(enableNotification ?
698 AILocalizedString(@"Unable to Enable Notifications", nil) :
699 AILocalizedString(@"Unable to Disable Notifications", nil))
700 withDescription:AILocalizedString(@"Unable to connect to the Twitter server.", nil)];
705 * @brief Menu items for chat
707 * Returns an array of menu items for a chat on this account. This is the best place to add protocol-specific
708 * actions that aren't otherwise supported by Adium.
709 * @param inChat AIChat for menu items
710 * @return NSArray of NSMenuItem instances for the passed contact
712 - (NSArray *)menuItemsForChat:(AIChat *)inChat
714 NSMutableArray *menuItemArray = [NSMutableArray array];
716 NSMenuItem *menuItem;
718 NSImage *serviceIcon = [AIServiceIcons serviceIconForService:self.service
719 type:AIServiceIconSmall
720 direction:AIIconNormal];
722 menuItem = [[[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:AILocalizedString(@"Update Tweets",nil)
724 action:@selector(periodicUpdate)
725 keyEquivalent:@""] autorelease];
726 [menuItem setImage:serviceIcon];
727 [menuItemArray addObject:menuItem];
729 menuItem = [[[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:AILocalizedString(@"Reply to a Tweet",nil)
731 action:@selector(replyToTweet)
732 keyEquivalent:@""] autorelease];
733 [menuItem setImage:serviceIcon];
734 [menuItemArray addObject:menuItem];
736 return menuItemArray;
740 * @brief Menu items for the account's actions
742 * Returns an array of menu items for account-specific actions. This is the best place to add protocol-specific
743 * actions that aren't otherwise supported by Adium. It will only be queried if the account is online.
744 * @return NSArray of NSMenuItem instances for this account
746 - (NSArray *)accountActionMenuItems
748 NSMutableArray *menuItemArray = [NSMutableArray array];
750 NSMenuItem *menuItem;
752 menuItem = [[[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:AILocalizedString(@"Update Tweets",nil)
754 action:@selector(periodicUpdate)
755 keyEquivalent:@""] autorelease];
756 [menuItemArray addObject:menuItem];
758 menuItem = [[[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:AILocalizedString(@"Reply to a Tweet",nil)
760 action:@selector(replyToTweet)
761 keyEquivalent:@""] autorelease];
762 [menuItemArray addObject:menuItem];
764 menuItem = [[[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:AILocalizedString(@"Get Rate Limit Amount",nil)
766 action:@selector(getRateLimitAmount)
767 keyEquivalent:@""] autorelease];
768 [menuItemArray addObject:menuItem];
770 return menuItemArray;
774 * @brief Open the reply to tweet window
776 * Opens a window in which the user can create a reply featuring in_reply_to_status_id being set.
780 [AITwitterReplyWindowController showReplyWindowForAccount:self];
784 * @brief Gets the current rate limit amount.
786 - (void)getRateLimitAmount
788 NSString *requestID = [twitterEngine getRateLimitStatus];
791 [self setRequestType:AITwitterRateLimitStatus
792 forRequestID:requestID
798 #pragma mark Contact handling
800 * @brief The name of our timeline chat
802 - (NSString *)timelineChatName
804 return [NSString stringWithFormat:TWITTER_TIMELINE_NAME, self.UID];
808 * @brief Our timeline chat
810 * If the timeline chat is not already active, it is created.
812 - (AIChat *)timelineChat
814 AIChat *timelineChat = [adium.chatController existingChatWithName:self.timelineChatName
818 timelineChat = [adium.chatController chatWithName:self.timelineChatName
821 chatCreationInfo:nil];
828 * @brief Update the timeline chat
830 * Remove the userlist
832 - (void)updateTimelineChat:(AIChat *)timelineChat
834 // Disable the user list on the chat.
835 if (timelineChat.chatContainer.chatViewController.userListVisible) {
836 [timelineChat.chatContainer.chatViewController toggleUserList];
839 // Update the participant list.
840 [timelineChat addParticipatingListObjects:self.contacts notify:NotifyNow];
842 [timelineChat setValue:[NSNumber numberWithInt:140] forProperty:@"Character Counter Max" notify:NotifyNow];
846 * @brief Update serverside icon
848 * This is called by AIUserIcons when it needs an icon update for a contact.
849 * If we already have an icon set (even a cached icon), ignore it.
850 * Otherwise return the Twitter service icon.
852 * This is so that when an unknown contact appears, it has an actual image
853 * to replace in the WKMV when an actual icon update is returned.
855 * This service icon will not remain saved very long, I see no harm in using it.
856 * This only occurs for "strangers".
858 - (NSData *)serversideIconDataForContact:(AIListContact *)listContact
860 if (![AIUserIcons userIconSourceForObject:listContact] &&
861 ![AIUserIcons cachedUserIconDataForObject:listContact]) {
862 return [[self.service defaultServiceIconOfType:AIServiceIconLarge] TIFFRepresentation];
869 * @brief Update a user icon from a URL if necessary
871 - (void)updateUserIcon:(NSString *)url forContact:(AIListContact *)listContact;
873 // If we don't already have an icon for the user...
874 if(![[listContact valueForProperty:TWITTER_PROPERTY_REQUESTED_USER_ICON] boolValue]) {
875 NSString *fileName = [[url lastPathComponent] stringByReplacingOccurrencesOfString:@"_normal." withString:@"_bigger."];
877 url = [[url stringByDeletingLastPathComponent] stringByAppendingPathComponent:fileName];
879 // Grab the user icon and set it as their serverside icon.
880 NSString *requestID = [twitterEngine getImageAtURL:url];
883 [self setRequestType:AITwitterUserIconPull
884 forRequestID:requestID
885 withDictionary:[NSDictionary dictionaryWithObject:listContact forKey:@"ListContact"]];
888 [listContact setValue:[NSNumber numberWithBool:YES] forProperty:TWITTER_PROPERTY_REQUESTED_USER_ICON notify:NotifyNever];
893 * @brief Unfollow the requested contacts.
895 - (void)removeContacts:(NSArray *)objects fromGroups:(NSArray *)groups
897 for (AIListContact *object in objects) {
898 NSString *requestID = [twitterEngine disableUpdatesFor:object.UID];
900 AILogWithSignature(@"%@ Requesting unfollow for: %@", self, object.UID);
903 [self setRequestType:AITwitterRemoveFollow
904 forRequestID:requestID
905 withDictionary:[NSDictionary dictionaryWithObject:object forKey:@"ListContact"]];
911 * @brief Follow the requested contact, trigger an information pull for them.
913 - (void)addContact:(AIListContact *)contact toGroup:(AIListGroup *)group
915 if ([contact.UID isCaseInsensitivelyEqualToString:self.UID]) {
916 AILogWithSignature(@"Not adding contact %@ to group %@, it's me!", contact.UID, group.UID);
920 NSString *requestID = [twitterEngine enableUpdatesFor:contact.UID];
922 AILogWithSignature(@"%@ Requesting follow for: %@", self, contact.UID);
925 NSString *updateRequestID = [twitterEngine getUserInformationFor:contact.UID];
927 if (updateRequestID) {
928 [self setRequestType:AITwitterAddFollow
929 forRequestID:updateRequestID
930 withDictionary:[NSDictionary dictionaryWithObjectsAndKeys:contact.UID, @"UID", nil]];
935 #pragma mark Request cataloguing
937 * @brief Set the type and optional dictionary for a request ID
939 * Sets the AITwitterRequestType for a particular request ID, so when the request finishes we can identify what it is for.
940 * Optionally sets a dictionary which can be retrieved in association with the request type.
942 - (void)setRequestType:(AITwitterRequestType)type forRequestID:(NSString *)requestID withDictionary:(NSDictionary *)info
944 [pendingRequests setObject:[NSDictionary dictionaryWithObjectsAndKeys:[NSNumber numberWithInt:type], @"Type",
950 * @brief Get the request type for a request ID
952 - (AITwitterRequestType)requestTypeForRequestID:(NSString *)requestID
954 return [(NSNumber *)[[pendingRequests objectForKey:requestID] objectForKey:@"Type"] intValue];
958 * @brief Get the dictionary associated with a request ID
960 - (NSDictionary *)dictionaryForRequestID:(NSString *)requestID
962 return (NSDictionary *)[[pendingRequests objectForKey:requestID] objectForKey:@"Info"];
966 * @brief Remove a request ID's saved information.
968 - (void)clearRequestTypeForRequestID:(NSString *)requestID
970 [pendingRequests removeObjectForKey:requestID];
973 #pragma mark Preference updating
974 - (void)preferencesChangedForGroup:(NSString *)group key:(NSString *)key object:(AIListObject *)object
975 preferenceDict:(NSDictionary *)prefDict firstTime:(BOOL)firstTime
977 [super preferencesChangedForGroup:group key:key object:object preferenceDict:prefDict firstTime:firstTime];
979 // We only care about our changes.
980 if (object != self) {
984 if([group isEqualToString:GROUP_ACCOUNT_STATUS]) {
985 if([key isEqualToString:KEY_USER_ICON]) {
986 // Avoid pushing an icon update which we just downloaded.
987 if(![self boolValueForProperty:TWITTER_PROPERTY_REQUESTED_USER_ICON]) {
988 NSString *requestID = [twitterEngine updateProfileImage:[prefDict objectForKey:KEY_USER_ICON]];
991 AILogWithSignature(@"%@ Pushing self icon update", self);
993 [self setRequestType:AITwitterProfileSelf
994 forRequestID:requestID
999 [self setValue:nil forProperty:TWITTER_PROPERTY_REQUESTED_USER_ICON notify:NotifyNever];
1003 if([group isEqualToString:TWITTER_PREFERENCE_GROUP_UPDATES]) {
1004 if(!firstTime && [key isEqualToString:TWITTER_PREFERENCE_UPDATE_INTERVAL]) {
1005 NSTimeInterval timeInterval = [updateTimer timeInterval];
1006 NSTimeInterval newTimeInterval = [[prefDict objectForKey:TWITTER_PREFERENCE_UPDATE_INTERVAL] intValue] * 60;
1008 if (timeInterval != newTimeInterval && self.online) {
1009 [updateTimer invalidate]; updateTimer = nil;
1011 if(newTimeInterval > 0) {
1012 updateTimer = [NSTimer scheduledTimerWithTimeInterval:newTimeInterval
1014 selector:@selector(periodicUpdate)
1021 updateAfterSend = [[prefDict objectForKey:TWITTER_PREFERENCE_UPDATE_AFTER_SEND] boolValue];
1022 retweetLink = [[prefDict objectForKey:TWITTER_PREFERENCE_RETWEET_SPAM] boolValue];
1024 if ([key isEqualToString:TWITTER_PREFERENCE_LOAD_CONTACTS] && self.online) {
1025 if ([[prefDict objectForKey:TWITTER_PREFERENCE_LOAD_CONTACTS] boolValue]) {
1026 // Delay updates when loading our contacts list.
1027 [self silenceAllContactUpdatesForInterval:18.0];
1028 // Grab our user list.
1029 NSString *requestID = [twitterEngine getRecentlyUpdatedFriendsFor:self.UID startingAtPage:1];
1032 [self setRequestType:AITwitterInitialUserInfo
1033 forRequestID:requestID
1034 withDictionary:[NSDictionary dictionaryWithObject:[NSNumber numberWithInt:1] forKey:@"Page"]];
1037 [self removeAllContacts];
1043 #pragma mark Periodic update scheduler
1045 * @brief Trigger our periodic updates
1047 - (void)periodicUpdate
1049 if (pendingUpdateCount) {
1050 AILogWithSignature(@"%@ Update already in progress. Count = %d", self, pendingUpdateCount);
1054 NSString *requestID;
1057 // We haven't completed the timeline nor replies. This lets us know if we should display statuses.
1058 followedTimelineCompleted = repliesCompleted = NO;
1059 futureTimelineLastID = futureRepliesLastID = nil;
1061 // Prevent triggering this update routine multiple times.
1062 pendingUpdateCount = 3;
1064 // We haven't printed error messages for this set.
1065 timelineErrorMessagePrinted = NO;
1067 [queuedUpdates removeAllObjects];
1068 [queuedDM removeAllObjects];
1070 AILogWithSignature(@"%@ Periodic update fire", self);
1072 // Pull direct messages
1073 lastID = [self preferenceForKey:TWITTER_PREFERENCE_DM_LAST_ID
1074 group:TWITTER_PREFERENCE_GROUP_UPDATES];
1076 requestID = [twitterEngine getDirectMessagesSinceID:lastID startingAtPage:1];
1079 [self setRequestType:AITwitterUpdateDirectMessage
1080 forRequestID:requestID
1081 withDictionary:[NSDictionary dictionaryWithObjectsAndKeys:[NSNumber numberWithInt:1], @"Page", nil]];
1083 --pendingUpdateCount;
1086 // Pull followed timeline
1087 lastID = [self preferenceForKey:TWITTER_PREFERENCE_TIMELINE_LAST_ID
1088 group:TWITTER_PREFERENCE_GROUP_UPDATES];
1090 requestID = [twitterEngine getFollowedTimelineFor:nil
1093 count:(lastID ? TWITTER_UPDATE_TIMELINE_COUNT : TWITTER_UPDATE_TIMELINE_COUNT_FIRST_RUN)];
1096 [self setRequestType:AITwitterUpdateFollowedTimeline
1097 forRequestID:requestID
1098 withDictionary:[NSDictionary dictionaryWithObjectsAndKeys:[NSNumber numberWithInt:1], @"Page", nil]];
1100 --pendingUpdateCount;
1103 // Pull the replies feed
1104 lastID = [self preferenceForKey:TWITTER_PREFERENCE_REPLIES_LAST_ID
1105 group:TWITTER_PREFERENCE_GROUP_UPDATES];
1107 requestID = [twitterEngine getRepliesSinceID:lastID startingAtPage:1];
1110 [self setRequestType:AITwitterUpdateReplies
1111 forRequestID:requestID
1112 withDictionary:[NSDictionary dictionaryWithObjectsAndKeys:[NSNumber numberWithInt:1], @"Page", nil]];
1114 --pendingUpdateCount;
1118 #pragma mark Message Display
1120 * @brief Returns a user-readable message for an error code.
1122 - (NSString *)errorMessageForError:(NSError *)error
1124 switch (error.code) {
1126 // Bad Request: your request is invalid, and we'll return an error message that tells you why.
1127 // This is the status code returned if you've exceeded the rate limit.
1128 return AILocalizedString(@"You've exceeded the rate limit.", nil);
1132 // Not Authorized: either you need to provide authentication credentials, or the credentials provided aren't valid.
1133 return AILocalizedString(@"Your credentials do not allow you access.", nil);
1137 // Forbidden: we understand your request, but are refusing to fulfill it. An accompanying error message should explain why.
1138 return AILocalizedString(@"Request refused by the server.", nil);
1142 // Not Found: either you're requesting an invalid URI or the resource in question doesn't exist (ex: no such user).
1143 return AILocalizedString(@"Requested resource not found.", nil);
1147 // Internal Server Error: we did something wrong. Please post to the group about it and the Twitter team will investigate.
1148 return AILocalizedString(@"The server reported an internal error.", nil);
1152 // Bad Gateway: returned if Twitter is down or being upgraded.
1153 return AILocalizedString(@"The server is currently down.", nil);
1159 // Service Unavailable: the Twitter servers are up, but are overloaded with requests. Try again later.
1160 return AILocalizedString(@"The server is overloaded with requests.", nil);
1165 return [NSString stringWithFormat:AILocalizedString(@"Unknown error: code %d, %@", nil), error.code, error.localizedDescription];
1169 * @brief Returns the link URL for a specific type of link
1171 - (NSString *)addressForLinkType:(AITwitterLinkType)linkType
1172 userID:(NSString *)userID
1173 statusID:(NSString *)statusID
1174 context:(NSString *)context
1176 NSString *address = nil;
1178 if (linkType == AITwitterLinkStatus) {
1179 address = [NSString stringWithFormat:@"https://twitter.com/%@/status/%@", userID, statusID];
1180 } else if (linkType == AITwitterLinkFriends) {
1181 address = [NSString stringWithFormat:@"https://twitter.com/%@/friends", userID];
1182 } else if (linkType == AITwitterLinkFollowers) {
1183 address = [NSString stringWithFormat:@"https://twitter.com/%@/followers", userID];
1184 } else if (linkType == AITwitterLinkUserPage) {
1185 address = [NSString stringWithFormat:@"https://twitter.com/%@", userID];
1186 } else if (linkType == AITwitterLinkSearchHash) {
1187 address = [NSString stringWithFormat:@"http://search.twitter.com/search?q=%%23%@", context];
1188 } else if (linkType == AITwitterLinkReply) {
1189 address = [NSString stringWithFormat:@"twitterreply://%@@%@?action=reply&status=%@", self.internalObjectID, userID, statusID];
1190 } else if (linkType == AITwitterLinkRetweet) {
1191 address = [NSString stringWithFormat:@"twitterreply://%@@%@?action=retweet&status=%@&message=%@", self.internalObjectID, userID, statusID, context];
1192 } else if (linkType == AITwitterLinkFavorite) {
1193 address = [NSString stringWithFormat:@"twitterreply://%@@%@?action=favorite&status=%@", self.internalObjectID, userID, statusID];
1194 } else if (linkType == AITwitterLinkDestroyStatus) {
1195 address = [NSString stringWithFormat:@"twitterreply://%@@%@?action=destroy&status=%@&message=%@", self.internalObjectID, userID, statusID, context];
1196 } else if (linkType == AITwitterLinkDestroyDM) {
1197 address = [NSString stringWithFormat:@"twitterreply://%@@%@?action=destroy&dm=%@&message=%@", self.internalObjectID, userID, statusID, context];
1204 * @brief Retweet the selected tweet.
1206 * Attempts to retweet a tweet.
1207 * Prints a status message in the chat on success/failure, behaves identical to sending a new tweet.
1209 * @returns YES if the account could send a retweet message, NO if the account doesn't support it.
1211 - (BOOL)retweetTweet:(NSString *)tweetID
1213 NSString *requestID = [twitterEngine retweetUpdate:tweetID];
1216 [self setRequestType:AITwitterSendUpdate
1217 forRequestID:requestID
1218 withDictionary:[NSDictionary dictionaryWithObjectsAndKeys:tweetID, @"tweetID", nil]];
1220 [self.timelineChat receivedError:[NSNumber numberWithInt:AIChatMessageSendingConnectionError]];
1227 * @brief Toggle the favorite status for a tweet.
1229 * Attempts to favorite a tweet. If that fails, it removes favorite status.
1230 * Prints a status message in the chat on success/failure, since it's otherwise not obvious.
1232 - (void)toggleFavoriteTweet:(NSString *)tweetID
1234 NSString *requestID = [twitterEngine markUpdate:tweetID asFavorite:YES];
1237 [self setRequestType:AITwitterFavoriteYes
1238 forRequestID:requestID
1239 withDictionary:[NSDictionary dictionaryWithObjectsAndKeys:tweetID, @"tweetID", nil]];
1241 AIChat *timelineChat = self.timelineChat;
1243 [adium.contentController displayEvent:AILocalizedString(@"Attempt to favorite tweet failed to connect.", nil)
1245 inChat:timelineChat];
1250 * @brief Destroy the tweet.
1252 * The user has already confirmed they want to destroy it; send the message.
1254 - (void)destroyTweet:(NSString *)tweetID
1256 NSString *requestID = [twitterEngine deleteUpdate:tweetID];
1259 [self setRequestType:AITwitterDestroyStatus
1260 forRequestID:requestID
1261 withDictionary:nil];
1263 AIChat *timelineChat = self.timelineChat;
1265 [adium.contentController displayEvent:AILocalizedString(@"Attempt to delete tweet failed to connect.", nil)
1267 inChat:timelineChat];
1272 * @brief Destroy the DM.
1274 * The user has already confirmed they want to destroy it; send the message.
1276 - (void)destroyDirectMessage:(NSString *)messageID
1277 forUser:(NSString *)userID
1279 NSString *requestID = [twitterEngine deleteDirectMessage:messageID];
1280 AIListContact *contact = [self contactWithUID:userID];
1283 [self setRequestType:AITwitterDestroyDM
1284 forRequestID:requestID
1285 withDictionary:[NSDictionary dictionaryWithObject:contact forKey:@"ListContact"]];
1287 AIChat *chat = [adium.chatController chatWithContact:contact];
1289 [adium.contentController displayEvent:AILocalizedString(@"Attempt to delete tweet failed to connect.", nil)
1296 * @brief Convert a link URL and name into an attributed link
1298 * @param label The text to display for the link.
1299 * @param destination The destination address for the link.
1300 * @param attributeName The name of the twitter link attribute for HTML processing.
1302 - (NSAttributedString *)attributedStringWithLinkLabel:(NSString *)label
1303 linkDestination:(NSString *)destination
1304 linkClass:(NSString *)className
1306 NSURL *url = [NSURL URLWithString:destination];
1307 NSDictionary *attributes = [NSDictionary dictionaryWithObjectsAndKeys:
1308 url, NSLinkAttributeName,
1309 className, AIElementClassAttributeName, nil];
1311 return [[[NSAttributedString alloc] initWithString:label attributes:attributes] autorelease];
1315 * @brief Parse an attributed string into a linkified version.
1317 - (NSAttributedString *)linkifiedAttributedStringFromString:(NSAttributedString *)inString
1319 NSAttributedString *attributedString;
1321 static NSCharacterSet *usernameCharacters = nil;
1322 static NSCharacterSet *hashCharacters = nil;
1324 if (!usernameCharacters) {
1325 usernameCharacters = [[NSCharacterSet characterSetWithCharactersInString:@"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_"] retain];
1328 if (!hashCharacters) {
1329 NSMutableCharacterSet *disallowedCharacters = [[NSCharacterSet punctuationCharacterSet] mutableCopy];
1330 [disallowedCharacters formUnionWithCharacterSet:[NSCharacterSet whitespaceCharacterSet]];
1332 hashCharacters = [[disallowedCharacters invertedSet] retain];
1334 [disallowedCharacters release];
1337 attributedString = [AITwitterURLParser linkifiedStringFromAttributedString:inString
1338 forPrefixCharacter:@"@"
1339 forLinkType:AITwitterLinkUserPage
1341 validCharacterSet:usernameCharacters];
1343 attributedString = [AITwitterURLParser linkifiedStringFromAttributedString:attributedString
1344 forPrefixCharacter:@"#"
1345 forLinkType:AITwitterLinkSearchHash
1347 validCharacterSet:hashCharacters];
1349 return attributedString;
1353 * @brief Parses a Twitter message into an attributed string
1355 - (NSAttributedString *)parseMessage:(NSString *)inMessage
1356 tweetID:(NSString *)tweetID
1357 userID:(NSString *)userID
1358 inReplyToUser:(NSString *)replyUserID
1359 inReplyToTweetID:(NSString *)replyTweetID
1361 NSAttributedString *message;
1363 message = [NSAttributedString stringWithString:[inMessage stringByUnescapingFromXMLWithEntities:nil]];
1365 message = [self linkifiedAttributedStringFromString:message];
1367 BOOL replyTweet = (replyTweetID.length);
1368 BOOL tweetLink = (tweetID.length && userID.length);
1370 if (replyTweet || tweetLink) {
1371 NSMutableAttributedString *mutableMessage = [[message mutableCopy] autorelease];
1373 NSUInteger startIndex = message.length;
1375 [mutableMessage appendString:@" (" withAttributes:nil];
1377 BOOL commaNeeded = NO;
1379 // Append a link to the tweet this is in reply to
1381 NSString *linkAddress = [self addressForLinkType:AITwitterLinkStatus
1383 statusID:replyTweetID
1386 if([inMessage hasPrefix:@"@"] &&
1387 inMessage.length >= replyUserID.length + 1 &&
1388 [replyUserID isCaseInsensitivelyEqualToString:[inMessage substringWithRange:NSMakeRange(1, replyUserID.length)]]) {
1389 // If the message has a "@" prefix, it's a proper in_reply_to_status_id if the usernames match. Set a link appropriately.
1390 [mutableMessage setAttributes:[NSDictionary dictionaryWithObjectsAndKeys:linkAddress, NSLinkAttributeName, nil]
1391 range:NSMakeRange(0, replyUserID.length + 1)];
1393 // This happens for mentions which are in_reply_to_status_id but the @target isn't the first part of the message.
1395 [mutableMessage appendAttributedString:[self attributedStringWithLinkLabel:AILocalizedString(@"IRT", "An abbreviation for 'in reply to' - placed at the beginning of the tweet tools for those which are directly in reply to another")
1396 linkDestination:linkAddress
1397 linkClass:AITwitterInReplyToClassName]];
1403 // Append a link to reply to this tweet
1405 NSString *linkAddress;
1407 if(![self.UID isCaseInsensitivelyEqualToString:userID]) {
1408 // A message from someone other than ourselves. RT and @ is permissible.
1411 [mutableMessage appendString:@", " withAttributes:nil];
1414 linkAddress = [self addressForLinkType:AITwitterLinkRetweet
1417 context:[inMessage stringByAddingPercentEscapesForAllCharacters]];
1419 [mutableMessage appendAttributedString:[self attributedStringWithLinkLabel:@"RT"
1420 linkDestination:linkAddress
1421 linkClass:AITwitterRetweetClassName]];
1426 [mutableMessage appendString:@", " withAttributes:nil];
1429 linkAddress = [self addressForLinkType:AITwitterLinkReply
1434 [mutableMessage appendAttributedString:[self attributedStringWithLinkLabel:@"@"
1435 linkDestination:linkAddress
1436 linkClass:AITwitterReplyClassName]];
1439 [mutableMessage appendString:@", " withAttributes:nil];
1442 // Our own message. Display a destroy link.
1443 linkAddress = [self addressForLinkType:AITwitterLinkDestroyStatus
1446 context:[inMessage stringByAddingPercentEscapesForAllCharacters]];
1448 [mutableMessage appendAttributedString:[self attributedStringWithLinkLabel:@"\u232B"
1449 linkDestination:linkAddress
1450 linkClass:AITwitterDeleteClassName]];
1453 [mutableMessage appendString:@", " withAttributes:nil];
1455 linkAddress = [self addressForLinkType:AITwitterLinkFavorite
1460 [mutableMessage appendAttributedString:[self attributedStringWithLinkLabel:@"\u2606"
1461 linkDestination:linkAddress
1462 linkClass:AITwitterFavoriteClassName]];
1464 [mutableMessage appendString:@", " withAttributes:nil];
1466 linkAddress = [self addressForLinkType:AITwitterLinkStatus
1471 [mutableMessage appendAttributedString:[self attributedStringWithLinkLabel:@"#"
1472 linkDestination:linkAddress
1473 linkClass:AITwitterStatusLinkClassName]];
1477 [mutableMessage appendString:@")" withAttributes:nil];
1479 [mutableMessage addAttributes:[NSDictionary dictionaryWithObjectsAndKeys:
1480 [NSNumber numberWithBool:YES], AITwitterActionLinksAttributeName,
1481 [NSNumber numberWithBool:YES], AIHiddenMessagePartAttributeName, nil]
1482 range:NSMakeRange(startIndex, mutableMessage.length - startIndex)];
1484 return mutableMessage;
1491 * @brief Parse a direct message
1493 - (NSAttributedString *)parseDirectMessage:(NSString *)inMessage
1494 withID:(NSString *)dmID
1495 fromUser:(NSString *)sourceUID
1497 NSAttributedString *message;
1499 message = [NSAttributedString stringWithString:[inMessage stringByUnescapingFromXMLWithEntities:nil]];
1501 message = [self linkifiedAttributedStringFromString:message];
1503 NSMutableAttributedString *mutableMessage = [[message mutableCopy] autorelease];
1505 NSUInteger startIndex = message.length;
1507 [mutableMessage appendString:@" (" withAttributes:nil];
1509 NSString *linkAddress = [self addressForLinkType:AITwitterLinkDestroyDM
1512 context:[inMessage stringByAddingPercentEscapesForAllCharacters]];
1514 [mutableMessage appendAttributedString:[self attributedStringWithLinkLabel:@"\u232B"
1515 linkDestination:linkAddress
1516 linkClass:AITwitterDeleteClassName]];
1518 [mutableMessage appendString:@")" withAttributes:nil];
1520 [mutableMessage addAttributes:[NSDictionary dictionaryWithObjectsAndKeys:
1521 [NSNumber numberWithBool:YES], AITwitterActionLinksAttributeName,
1522 [NSNumber numberWithBool:YES], AIHiddenMessagePartAttributeName, nil]
1523 range:NSMakeRange(startIndex, mutableMessage.length - startIndex)];
1525 return mutableMessage;
1530 * @brief Sort status updates
1532 NSInteger queuedUpdatesSort(id update1, id update2, void *context)
1534 return [[update1 objectForKey:TWITTER_STATUS_CREATED] compare:[update2 objectForKey:TWITTER_STATUS_CREATED]];
1538 * @brief Sort direct messages
1540 NSInteger queuedDMSort(id dm1, id dm2, void *context)
1542 return [[dm1 objectForKey:TWITTER_DM_CREATED] compare:[dm2 objectForKey:TWITTER_DM_CREATED]];
1546 * @brief Remove duplicate status updates.
1548 * If we're following someone who replies to us, we'll receive a status update in both the
1549 * timeline and the reply feed.
1551 * @param inArray The sorted array of Tweets
1553 - (NSArray *)arrayWithDuplicateTweetsRemoved:(NSArray *)inArray
1555 NSMutableArray *mutableArray = [inArray mutableCopy];
1557 NSDictionary *status = nil, *previousStatus = nil;
1559 // Starting at index 1, checking backwards. We'll never exceed bounds this way.
1560 for(NSUInteger index = 1; index < inArray.count; index++)
1562 status = [inArray objectAtIndex:index];
1563 previousStatus = [inArray objectAtIndex:index-1];
1565 if([[status objectForKey:TWITTER_STATUS_ID] isEqualToString:[previousStatus objectForKey:TWITTER_STATUS_ID]]) {
1566 [mutableArray removeObject:status];
1570 return [mutableArray autorelease];
1574 * @brief Display queued updates or direct messages
1576 * This could potentially be simplified since both DMs and updates have the same format.
1578 - (void)displayQueuedUpdatesForRequestType:(AITwitterRequestType)requestType
1580 if(requestType == AITwitterUpdateReplies || requestType == AITwitterUpdateFollowedTimeline) {
1581 if(!queuedUpdates.count) {
1585 AILogWithSignature(@"%@ Displaying %d updates", self, queuedUpdates.count);
1587 // Sort the queued updates (since we're intermingling pages of data from different souces)
1588 NSArray *sortedQueuedUpdates = [queuedUpdates sortedArrayUsingFunction:queuedUpdatesSort context:nil];
1590 sortedQueuedUpdates = [self arrayWithDuplicateTweetsRemoved:sortedQueuedUpdates];
1592 BOOL trackContent = [[self preferenceForKey:TWITTER_PREFERENCE_EVER_LOADED_TIMELINE group:TWITTER_PREFERENCE_GROUP_UPDATES] boolValue];
1594 AIChat *timelineChat = self.timelineChat;
1596 [[AIContactObserverManager sharedManager] delayListObjectNotifications];
1598 for (NSDictionary *status in sortedQueuedUpdates) {
1599 NSDate *date = [status objectForKey:TWITTER_STATUS_CREATED];
1600 NSString *text = [status objectForKey:TWITTER_STATUS_TEXT];
1602 NSString *contactUID = [[status objectForKey:TWITTER_STATUS_USER] objectForKey:TWITTER_STATUS_UID];
1604 id fromObject = nil;
1606 if(![self.UID isCaseInsensitivelyEqualToString:contactUID]) {
1607 AIListContact *listContact = [self contactWithUID:contactUID];
1609 // Update the user's status message
1610 [listContact setStatusMessage:[NSAttributedString stringWithString:[text stringByUnescapingFromXMLWithEntities:nil]]
1613 [self updateUserIcon:[[status objectForKey:TWITTER_STATUS_USER] objectForKey:TWITTER_INFO_ICON] forContact:listContact];
1615 [timelineChat addParticipatingListObject:listContact notify:NotifyNow];
1617 fromObject = (id)listContact;
1619 fromObject = (id)self;
1622 NSAttributedString *message = [self parseMessage:text
1623 tweetID:[status objectForKey:TWITTER_STATUS_ID]
1625 inReplyToUser:[status objectForKey:TWITTER_STATUS_REPLY_UID]
1626 inReplyToTweetID:[status objectForKey:TWITTER_STATUS_REPLY_ID]];
1628 AIContentMessage *contentMessage = [AIContentMessage messageInChat:timelineChat
1629 withSource:fromObject
1635 contentMessage.trackContent = trackContent;
1637 [adium.contentController receiveContentObject:contentMessage];
1640 [[AIContactObserverManager sharedManager] endListObjectNotificationsDelay];
1642 [queuedUpdates removeAllObjects];
1643 } else if (requestType == AITwitterUpdateDirectMessage || requestType == AITwitterDirectMessageSend) {
1644 NSMutableArray **unsortedArray = (requestType == AITwitterUpdateDirectMessage) ? &queuedDM : &queuedOutgoingDM;
1646 if (!(*unsortedArray).count) {
1650 AILogWithSignature(@"%@ Displaying %d DMs", self, queuedDM.count);
1652 NSArray *sortedQueuedDM = [*unsortedArray sortedArrayUsingFunction:queuedDMSort context:nil];
1654 for (NSDictionary *message in sortedQueuedDM) {
1655 NSDate *date = [message objectForKey:TWITTER_DM_CREATED];
1656 NSString *text = [message objectForKey:TWITTER_DM_TEXT];
1657 NSString *fromUID = [message objectForKey:TWITTER_DM_SENDER_UID];
1658 NSString *toUID = [message objectForKey:TWITTER_DM_RECIPIENT_UID];
1660 AIListObject *source = nil, *destination = nil;
1663 if([self.UID isCaseInsensitivelyEqualToString:fromUID]) {
1664 // This is a message we sent; display as coming from us.
1666 destination = [self contactWithUID:toUID];
1667 chat = [adium.chatController chatWithContact:(AIListContact *)destination];
1669 source = [self contactWithUID:fromUID];
1671 chat = [adium.chatController chatWithContact:(AIListContact *)source];
1674 if(chat && source && destination) {
1675 AIContentMessage *contentMessage = [AIContentMessage messageInChat:chat
1677 destination:destination
1679 message:[self parseDirectMessage:text
1680 withID:[message objectForKey:TWITTER_DM_ID]
1681 fromUser:chat.listObject.UID]
1684 [adium.contentController receiveContentObject:contentMessage];
1688 [*unsortedArray removeAllObjects];
1692 #pragma mark MGTwitterEngine Delegate Methods
1694 * @brief A request was successful
1696 * We only care about requests succeeding if they aren't specifically handled in another location.
1698 - (void)requestSucceeded:(NSString *)identifier
1700 // If a request succeeds and we think we're offline, call ourselves online.
1701 if ([self requestTypeForRequestID:identifier] == AITwitterDisconnect) {
1702 [self didDisconnect];
1703 } else if ([self requestTypeForRequestID:identifier] == AITwitterRemoveFollow) {
1704 AIListContact *listContact = [[self dictionaryForRequestID:identifier] objectForKey:@"ListContact"];
1706 for (NSString *groupName in listContact.remoteGroupNames) {
1707 [listContact removeRemoteGroupName:groupName];
1709 } else if ([self requestTypeForRequestID:identifier] == AITwitterDestroyStatus) {
1710 AIChat *timelineChat = self.timelineChat;
1712 [adium.contentController displayEvent:AILocalizedString(@"Your tweet has been successfully deleted.", nil)
1714 inChat:timelineChat];
1715 } else if ([self requestTypeForRequestID:identifier] == AITwitterDestroyDM) {
1716 AIListContact *contact = [[self dictionaryForRequestID:identifier] objectForKey:@"ListContact"];
1717 AIChat *chat = [adium.chatController chatWithContact:contact];
1719 [adium.contentController displayEvent:AILocalizedString(@"The direct message has been successfully deleted.", nil)
1726 * @brief A request failed
1728 * If it's a fatal error, we need to kill the session and retry. Otherwise, twitter's reliability is
1729 * pretty terrible, so let's ignore errors for the most part.
1731 - (void)requestFailed:(NSString *)identifier withError:(NSError *)error
1733 switch ([self requestTypeForRequestID:identifier]) {
1734 case AITwitterDirectMessageSend:
1735 case AITwitterSendUpdate:
1737 AIChat *chat = [[self dictionaryForRequestID:identifier] objectForKey:@"Chat"];
1740 [chat receivedError:[NSNumber numberWithInt:AIChatMessageSendingConnectionError]];
1742 AILogWithSignature(@"%@ Chat send error on %@", self, chat);
1747 case AITwitterDisconnect:
1748 [self didDisconnect];
1751 case AITwitterInitialUserInfo:
1752 [self setLastDisconnectionError:AILocalizedString(@"Unable to retrieve user list [fail]", "Message when a (vital) twitter request to retrieve the follow list fails")];
1753 [self didDisconnect];
1756 case AITwitterUserIconPull:
1758 AIListContact *listContact = [[self dictionaryForRequestID:identifier] objectForKey:@"ListContact"];
1760 // Image pull failed, flag ourselves as needing to try again.
1761 [listContact setValue:nil forProperty:TWITTER_PROPERTY_REQUESTED_USER_ICON notify:NotifyNever];
1765 case AITwitterUpdateFollowedTimeline:
1766 case AITwitterUpdateReplies:
1768 AIChat *timelineChat = [adium.chatController existingChatWithName:self.timelineChatName
1771 // Only print an error if the user already has the timeline open. Beyond annoying if we pop it open just to say "lol error"
1772 if (timelineChat && !timelineErrorMessagePrinted) {
1773 AIContentEvent *content = [AIContentEvent eventInChat:timelineChat
1777 message:[NSAttributedString stringWithString:[NSString stringWithFormat:AILocalizedString(@"Unable to update timeline: %@", nil),
1778 [self errorMessageForError:error]]]
1781 content.postProcessContent = NO;
1782 content.coalescingKey = @"error";
1784 [adium.contentController receiveContentObject:content];
1786 // This gets reset to NO the next a periodic update fires.
1787 timelineErrorMessagePrinted = YES;
1790 --pendingUpdateCount;
1794 case AITwitterUpdateDirectMessage:
1795 --pendingUpdateCount;
1798 case AITwitterAddFollow:
1799 if(error.code == 404) {
1800 [adium.interfaceController handleErrorMessage:AILocalizedString(@"Unable to Add Contact", nil)
1801 withDescription:[NSString stringWithFormat:AILocalizedString(@"Unable to add %@ to account %@, the user does not exist.", nil),
1802 [[self dictionaryForRequestID:identifier] objectForKey:@"UID"],
1803 self.explicitFormattedUID]];
1805 [adium.interfaceController handleErrorMessage:AILocalizedString(@"Unable to Add Contact", nil)
1806 withDescription:[NSString stringWithFormat:AILocalizedString(@"Unable to add %@ to account %@. %@",nil),
1807 [[self dictionaryForRequestID:identifier] objectForKey:@"UID"],
1808 self.explicitFormattedUID,
1809 [self errorMessageForError:error]]];
1813 case AITwitterRemoveFollow:
1814 [adium.interfaceController handleErrorMessage:AILocalizedString(@"Unable to Remove Contact", nil)
1815 withDescription:[NSString stringWithFormat:AILocalizedString(@"Unable to remove %@ on account %@. %@", nil),
1816 ((AIListContact *)[[self dictionaryForRequestID:identifier] objectForKey:@"ListContact"]).UID,
1817 self.explicitFormattedUID,
1818 [self errorMessageForError:error]]];
1821 case AITwitterValidateCredentials:
1822 if(error.code == 401) {
1824 [self setPasswordTemporarily:nil];
1825 [self setLastDisconnectionError:TWITTER_OAUTH_NOT_AUTHORIZED];
1827 [[NSNotificationCenter defaultCenter] postNotificationName:@"AIEditAccount"
1831 [self setLastDisconnectionError:TWITTER_INCORRECT_PASSWORD_MESSAGE];
1832 [self serverReportedInvalidPassword];
1835 [self didDisconnect];
1837 [self setLastDisconnectionError:AILocalizedString(@"Unable to validate credentials", nil)];
1838 [self didDisconnect];
1842 case AITwitterFavoriteYes:
1843 case AITwitterFavoriteNo:
1845 AIChat *timelineChat = self.timelineChat;
1847 if (error.code == 403) {
1848 // We've attempted to add or remove when we already have it marked as such. Try the opposite.
1849 BOOL addAsFavorite = ([self requestTypeForRequestID:identifier] == AITwitterFavoriteNo);
1850 NSString *tweetID = [[self dictionaryForRequestID:identifier] objectForKey:@"tweetID"];
1852 NSString *requestID = [twitterEngine markUpdate:tweetID
1853 asFavorite:addAsFavorite];
1856 [self setRequestType:(addAsFavorite ? AITwitterFavoriteYes : AITwitterFavoriteNo)
1857 forRequestID:requestID
1858 withDictionary:[NSDictionary dictionaryWithObjectsAndKeys:tweetID, @"tweetID", nil]];
1860 [adium.contentController displayEvent:AILocalizedString(@"Attempt to favorite tweet failed to connect.", nil)
1862 inChat:timelineChat];
1865 [adium.contentController displayEvent:[NSString stringWithFormat:AILocalizedString(@"Attempt to favorite tweet failed. %@", nil), [self errorMessageForError:error]]
1867 inChat:timelineChat];
1873 case AITwitterNotificationEnable:
1874 case AITwitterNotificationDisable:
1876 BOOL enableNotification = ([self requestTypeForRequestID:identifier] == AITwitterNotificationEnable);
1877 AIListContact *listContact = [[self dictionaryForRequestID:identifier] objectForKey:@"ListContact"];
1879 [adium.interfaceController handleErrorMessage:(enableNotification ?
1880 AILocalizedString(@"Unable to Enable Notifications", nil) :
1881 AILocalizedString(@"Unable to Disable Notifications", nil))
1882 withDescription:[NSString stringWithFormat:AILocalizedString(@"Cannot change notification setting for %@. %@", nil), listContact.UID, [self errorMessageForError:error]]];
1886 case AITwitterDestroyStatus:
1888 AIChat *timelineChat = self.timelineChat;
1890 [adium.contentController displayEvent:[NSString stringWithFormat:AILocalizedString(@"Your tweet failed to delete. %@", nil), [self errorMessageForError:error]]
1892 inChat:timelineChat];
1896 case AITwitterDestroyDM:
1898 AIListContact *contact = [[self dictionaryForRequestID:identifier] objectForKey:@"ListContact"];
1899 AIChat *chat = [adium.chatController chatWithContact:contact];
1901 [adium.contentController displayEvent:[NSString stringWithFormat:AILocalizedString(@"The direct message failed to delete. %@", nil), [self errorMessageForError:error]]
1907 case AITwitterUnknownType:
1908 case AITwitterRateLimitStatus:
1909 case AITwitterProfileSelf:
1910 case AITwitterSelfUserIconPull:
1911 case AITwitterProfileUserInfo:
1912 case AITwitterProfileStatusUpdates:
1913 // While we don't handle the errors, it's a good idea to not have a "default" just to prevent accidentally letting something
1914 // we should really handle slip through.
1919 AILogWithSignature(@"%@ Request failed (%@ - %u) - %@", self, identifier, [self requestTypeForRequestID:identifier], error);
1921 [self clearRequestTypeForRequestID:identifier];
1925 * @brief Status updates received
1927 - (void)statusesReceived:(NSArray *)statuses forRequest:(NSString *)identifier
1929 if([self requestTypeForRequestID:identifier] == AITwitterUpdateFollowedTimeline ||
1930 [self requestTypeForRequestID:identifier] == AITwitterUpdateReplies) {
1933 BOOL nextPageNecessary = NO;
1935 if([self requestTypeForRequestID:identifier] == AITwitterUpdateFollowedTimeline) {
1936 lastID = [self preferenceForKey:TWITTER_PREFERENCE_TIMELINE_LAST_ID
1937 group:TWITTER_PREFERENCE_GROUP_UPDATES];
1939 nextPageNecessary = (lastID && statuses.count >= TWITTER_UPDATE_TIMELINE_COUNT - 5);
1941 lastID = [self preferenceForKey:TWITTER_PREFERENCE_REPLIES_LAST_ID
1942 group:TWITTER_PREFERENCE_GROUP_UPDATES];
1944 nextPageNecessary = (lastID && statuses.count >= TWITTER_UPDATE_REPLIES_COUNT - 5);
1947 // Store the largest tweet ID we find; this will be our "last ID" the next time we run.
1948 NSString *largestTweet = [[self dictionaryForRequestID:identifier] objectForKey:@"LargestTweet"];
1950 // The largest ID is first, compare.
1951 if (statuses.count) {
1952 NSString *tweetID = [[statuses objectAtIndex:0] objectForKey:TWITTER_STATUS_ID];
1953 if (!largestTweet || [largestTweet compare:tweetID options:NSNumericSearch] == NSOrderedAscending) {
1954 largestTweet = tweetID;
1958 [queuedUpdates addObjectsFromArray:statuses];
1960 AILogWithSignature(@"%@ Last ID: %@ Largest Tweet: %@ Next Page Necessary: %d", self, lastID, largestTweet, nextPageNecessary);
1962 // See if we need to pull more updates.
1963 if (nextPageNecessary) {
1964 NSInteger nextPage = [[[self dictionaryForRequestID:identifier] objectForKey:@"Page"] intValue] + 1;
1965 NSString *requestID;
1967 if ([self requestTypeForRequestID:identifier] == AITwitterUpdateFollowedTimeline) {
1968 requestID = [twitterEngine getFollowedTimelineFor:nil
1970 startingAtPage:nextPage
1971 count:TWITTER_UPDATE_TIMELINE_COUNT];
1973 AILogWithSignature(@"%@ Pulling additional timeline page %d", self, nextPage);
1976 [self setRequestType:AITwitterUpdateFollowedTimeline
1977 forRequestID:requestID
1978 withDictionary:[NSDictionary dictionaryWithObjectsAndKeys:[NSNumber numberWithInt:nextPage], @"Page",
1979 largestTweet, @"LargestTweet", nil]];
1981 // Gracefully fail: remove all stored objects.
1982 AILogWithSignature(@"%@ Immediate timeline fail", self);
1983 --pendingUpdateCount;
1984 [queuedUpdates removeAllObjects];
1987 } else if ([self requestTypeForRequestID:identifier] == AITwitterUpdateReplies) {
1988 requestID = [twitterEngine getRepliesSinceID:lastID startingAtPage:nextPage];
1990 AILogWithSignature(@"%@ Pulling additional replies page %d", self, nextPage);
1993 [self setRequestType:AITwitterUpdateReplies
1994 forRequestID:requestID
1995 withDictionary:[NSDictionary dictionaryWithObjectsAndKeys:[NSNumber numberWithInt:nextPage], @"Page",
1996 largestTweet, @"LargestTweet", nil]];
1998 // Gracefully fail: remove all stored objects.
1999 AILogWithSignature(@"%@ Immediate reply fail", self);
2000 --pendingUpdateCount;
2001 [queuedUpdates removeAllObjects];
2005 if([self requestTypeForRequestID:identifier] == AITwitterUpdateFollowedTimeline) {
2006 followedTimelineCompleted = YES;
2007 futureTimelineLastID = [largestTweet retain];
2008 } else if ([self requestTypeForRequestID:identifier] == AITwitterUpdateReplies) {
2009 repliesCompleted = YES;
2010 futureRepliesLastID = [largestTweet retain];
2013 --pendingUpdateCount;
2015 AILogWithSignature(@"%@ Followed completed: %d Replies completed: %d", self, followedTimelineCompleted, repliesCompleted);
2017 if (followedTimelineCompleted && repliesCompleted) {
2018 if (queuedUpdates.count) {
2019 // Set the "last pulled" for the timeline and replies, since we've completed both.
2020 if(futureRepliesLastID) {
2021 AILogWithSignature(@"%@ futureRepliesLastID = %@", self, futureRepliesLastID);
2023 [self setPreference:futureRepliesLastID
2024 forKey:TWITTER_PREFERENCE_REPLIES_LAST_ID
2025 group:TWITTER_PREFERENCE_GROUP_UPDATES];
2027 [futureRepliesLastID release]; futureRepliesLastID = nil;
2030 if(futureTimelineLastID) {
2031 AILogWithSignature(@"%@ futureTimelineLastID = %@", self, futureTimelineLastID);
2033 [self setPreference:futureTimelineLastID
2034 forKey:TWITTER_PREFERENCE_TIMELINE_LAST_ID
2035 group:TWITTER_PREFERENCE_GROUP_UPDATES];
2037 [futureTimelineLastID release]; futureTimelineLastID = nil;
2040 [self displayQueuedUpdatesForRequestType:[self requestTypeForRequestID:identifier]];
2043 if (![self preferenceForKey:TWITTER_PREFERENCE_EVER_LOADED_TIMELINE group:TWITTER_PREFERENCE_GROUP_UPDATES]) {
2044 [self setPreference:[NSNumber numberWithBool:YES]
2045 forKey:TWITTER_PREFERENCE_EVER_LOADED_TIMELINE
2046 group:TWITTER_PREFERENCE_GROUP_UPDATES];
2050 } else if ([self requestTypeForRequestID:identifier] == AITwitterProfileStatusUpdates) {
2051 AIListContact *listContact = [[self dictionaryForRequestID:identifier] objectForKey:@"ListContact"];
2053 NSMutableArray *profileArray = [[listContact profileArray] mutableCopy];
2055 AILogWithSignature(@"%@ Updating statuses for profile, user %@", self, listContact);
2057 for (NSDictionary *update in statuses) {
2058 NSAttributedString *message = [self parseMessage:[update objectForKey:TWITTER_STATUS_TEXT]
2059 tweetID:[update objectForKey:TWITTER_STATUS_ID]
2060 userID:listContact.UID
2061 inReplyToUser:[update objectForKey:TWITTER_STATUS_REPLY_UID]
2062 inReplyToTweetID:[update objectForKey:TWITTER_STATUS_REPLY_ID]];
2064 [profileArray addObject:[NSDictionary dictionaryWithObjectsAndKeys:message, KEY_VALUE, nil]];
2067 [listContact setProfileArray:profileArray notify:NotifyNow];
2068 } else if ([self requestTypeForRequestID:identifier] == AITwitterSendUpdate) {
2069 if (updateAfterSend) {
2070 [self periodicUpdate];
2073 if (statuses.count) {
2074 [adium.contentController displayEvent:AILocalizedString(@"Tweet successfully sent.", nil)
2076 inChat:self.timelineChat];
2079 for(NSDictionary *update in statuses) {
2080 [[NSNotificationCenter defaultCenter] postNotificationName:AITwitterNotificationPostedStatus
2082 userInfo:[NSDictionary dictionaryWithObjectsAndKeys:self.timelineChat, @"AIChat", nil]];
2084 NSString *text = [[update objectForKey:TWITTER_STATUS_TEXT] stringByUnescapingFromXMLWithEntities:nil];
2086 if([[self preferenceForKey:TWITTER_PREFERENCE_UPDATE_GLOBAL group:TWITTER_PREFERENCE_GROUP_UPDATES] boolValue] &&
2087 (![text hasPrefix:@"@"] || [[self preferenceForKey:TWITTER_PREFERENCE_UPDATE_GLOBAL_REPLIES group:TWITTER_PREFERENCE_GROUP_UPDATES] boolValue])) {
2088 AIStatus *availableStatus = [AIStatus statusOfType:AIAvailableStatusType];
2090 availableStatus.statusMessage = [NSAttributedString stringWithString:text];
2091 [adium.statusController setActiveStatusState:availableStatus];
2094 } else if ([self requestTypeForRequestID:identifier] == AITwitterFavoriteYes ||
2095 [self requestTypeForRequestID:identifier] == AITwitterFavoriteNo) {
2096 AIChat *timelineChat = self.timelineChat;
2098 for (NSDictionary *status in statuses) {
2101 // Use HTML for the status message since it's just easier to localize that way.
2103 if ([self requestTypeForRequestID:identifier] == AITwitterFavoriteYes) {
2104 message = AILocalizedString(@"The <a href=\"%@\">requested tweet</a> by <a href=\"%@\">%@</a> is now a favorite.", nil);
2106 message = AILocalizedString(@"The <a href=\"%@\">requested tweet</a> by <a href=\"%@\">%@</a> is no longer a favorite.", nil);
2109 NSString *userID = [[status objectForKey:TWITTER_STATUS_USER] objectForKey:TWITTER_STATUS_UID];
2112 message = [NSString stringWithFormat:message,
2113 [self addressForLinkType:AITwitterLinkStatus
2115 statusID:[status objectForKey:TWITTER_STATUS_ID]
2117 [self addressForLinkType:AITwitterLinkUserPage
2123 NSAttributedString *attributedMessage = [[AIHTMLDecoder decoder] decodeHTML:message withDefaultAttributes:nil];
2125 AIContentEvent *content = [AIContentEvent eventInChat:timelineChat
2129 message:attributedMessage
2130 withType:@"favorite"];
2132 content.postProcessContent = NO;
2133 content.coalescingKey = @"favorite";
2135 [adium.contentController receiveContentObject:content];
2139 [self clearRequestTypeForRequestID:identifier];
2143 * @brief Direct messages received
2145 - (void)directMessagesReceived:(NSArray *)messages forRequest:(NSString *)identifier
2147 if ([self requestTypeForRequestID:identifier] == AITwitterUpdateDirectMessage) {
2148 NSString *lastID = [self preferenceForKey:TWITTER_PREFERENCE_DM_LAST_ID
2149 group:TWITTER_PREFERENCE_GROUP_UPDATES];
2151 BOOL nextPageNecessary = (lastID && messages.count >= TWITTER_UPDATE_DM_COUNT);
2153 // Store the largest tweet ID we find; this will be our "last ID" the next time we run.
2154 NSString *largestTweet = [[self dictionaryForRequestID:identifier] objectForKey:@"LargestTweet"];
2156 // The largest ID is first, compare.
2157 if (messages.count) {
2158 NSString *tweetID = [[messages objectAtIndex:0] objectForKey:TWITTER_DM_ID];
2159 if (!largestTweet || [largestTweet compare:tweetID] == NSOrderedAscending) {
2160 largestTweet = tweetID;
2164 [queuedDM addObjectsFromArray:messages];
2166 AILogWithSignature(@"%@ Last ID: %@ Largest Tweet: %@ Next Page Necessary: %d", self, lastID, largestTweet, nextPageNecessary);
2168 if(nextPageNecessary) {
2169 NSInteger nextPage = [[[self dictionaryForRequestID:identifier] objectForKey:@"Page"] intValue] + 1;
2171 NSString *requestID = [twitterEngine getDirectMessagesSinceID:lastID
2172 startingAtPage:nextPage];
2174 AILogWithSignature(@"%@ Pulling additional DM page %d", self, nextPage);
2177 [self setRequestType:AITwitterUpdateDirectMessage
2178 forRequestID:requestID
2179 withDictionary:[NSDictionary dictionaryWithObjectsAndKeys:[NSNumber numberWithInt:nextPage], @"Page",
2180 largestTweet, @"LargestTweet", nil]];
2182 // Gracefully fail: remove all stored objects.
2183 AILogWithSignature(@"%@ Immediate DM pull fail", self);
2184 --pendingUpdateCount;
2185 [queuedDM removeAllObjects];
2188 --pendingUpdateCount;
2191 AILogWithSignature(@"%@ Largest DM pulled = %@", self, largestTweet);
2193 [self setPreference:largestTweet
2194 forKey:TWITTER_PREFERENCE_DM_LAST_ID
2195 group:TWITTER_PREFERENCE_GROUP_UPDATES];
2198 // On first load, don't display any direct messages. Just ge the largest ID.
2199 if (queuedDM.count && lastID) {
2200 [self displayQueuedUpdatesForRequestType:[self requestTypeForRequestID:identifier]];
2202 [queuedDM removeAllObjects];
2205 } else if ([self requestTypeForRequestID:identifier] == AITwitterDirectMessageSend) {
2206 [queuedOutgoingDM addObjectsFromArray:messages];
2207 [self displayQueuedUpdatesForRequestType:AITwitterDirectMessageSend];
2210 [self clearRequestTypeForRequestID:identifier];
2214 * @brief User information received
2216 - (void)userInfoReceived:(NSArray *)userInfo forRequest:(NSString *)identifier
2218 if (([self requestTypeForRequestID:identifier] == AITwitterInitialUserInfo ||
2219 [self requestTypeForRequestID:identifier] == AITwitterAddFollow) &&
2220 [[self preferenceForKey:TWITTER_PREFERENCE_LOAD_CONTACTS group:TWITTER_PREFERENCE_GROUP_UPDATES] boolValue]) {
2221 [[AIContactObserverManager sharedManager] delayListObjectNotifications];
2223 // The current amount of friends per page is 100. Use >= just in case this changes.
2224 BOOL nextPageNecessary = (userInfo.count >= 100);
2226 AILogWithSignature(@"%@ User info pull, Next page necessary: %d Count: %d", self, nextPageNecessary, userInfo.count);
2228 for (NSDictionary *info in userInfo) {
2229 AIListContact *listContact = [self contactWithUID:[info objectForKey:TWITTER_INFO_UID]];
2231 // If the user isn't in a group, set them in the Twitter group.
2232 if(listContact.countOfRemoteGroupNames == 0) {
2233 [listContact addRemoteGroupName:TWITTER_REMOTE_GROUP_NAME];
2236 // Grab the Twitter display name and set it as the remote alias.
2237 if (![[listContact valueForProperty:@"Server Display Name"] isEqualToString:[info objectForKey:TWITTER_INFO_DISPLAY_NAME]]) {
2238 [listContact setServersideAlias:[info objectForKey:TWITTER_INFO_DISPLAY_NAME]
2239 silently:silentAndDelayed];
2242 // Grab the user icon and set it as their serverside icon.
2243 [self updateUserIcon:[info objectForKey:TWITTER_INFO_ICON] forContact:listContact];
2245 // Set the user as available.
2246 [listContact setStatusWithName:nil
2247 statusType:AIAvailableStatusType
2248 notify:NotifyLater];
2250 // Set the user's status message to their current twitter status text
2251 NSString *statusText = [[info objectForKey:TWITTER_INFO_STATUS] objectForKey:TWITTER_INFO_STATUS_TEXT];
2252 if (!statusText) //nil if they've never tweeted
2254 [listContact setStatusMessage:[NSAttributedString stringWithString:[statusText stringByUnescapingFromXMLWithEntities:nil]] notify:NotifyLater];
2256 // Set the user as online.
2257 [listContact setOnline:YES notify:NotifyLater silently:silentAndDelayed];
2259 [listContact notifyOfChangedPropertiesSilently:silentAndDelayed];
2262 [[AIContactObserverManager sharedManager] endListObjectNotificationsDelay];
2264 if (nextPageNecessary) {
2265 NSInteger nextPage = [[[self dictionaryForRequestID:identifier] objectForKey:@"Page"] intValue] + 1;
2266 NSString *requestID = [twitterEngine getRecentlyUpdatedFriendsFor:self.UID startingAtPage:nextPage];
2268 AILogWithSignature(@"%@ Pulling additional user info page %d", self, nextPage);
2271 [self setRequestType:AITwitterInitialUserInfo
2272 forRequestID:requestID
2273 withDictionary:[NSDictionary dictionaryWithObject:[NSNumber numberWithInt:nextPage]
2276 [self setLastDisconnectionError:AILocalizedString(@"Unable to retrieve user list [additional fail]", "Message when a (vital) twitter request to retrieve the follow list fails")];
2277 [self didDisconnect];
2280 } else if ([self valueForProperty:@"Connecting"]) {
2281 // Trigger our normal update routine.
2284 } else if ([self requestTypeForRequestID:identifier] == AITwitterProfileUserInfo) {
2285 NSDictionary *thisUserInfo = [userInfo objectAtIndex:0];
2288 AIListContact *listContact = [[self dictionaryForRequestID:identifier] objectForKey:@"ListContact"];
2290 NSArray *keyNames = [NSArray arrayWithObjects:@"name", @"location", @"description", @"url", @"friends_count", @"followers_count", @"statuses_count", nil];
2291 NSArray *readableNames = [NSArray arrayWithObjects:AILocalizedString(@"Name", nil), AILocalizedString(@"Location", nil),
2292 AILocalizedString(@"Biography", nil), AILocalizedString(@"Website", nil), AILocalizedString(@"Following", nil),
2293 AILocalizedString(@"Followers", nil), AILocalizedString(@"Updates", nil), nil];
2295 NSMutableArray *profileArray = [NSMutableArray array];
2297 for (NSUInteger index = 0; index < keyNames.count; index++) {
2298 NSString *keyName = [keyNames objectAtIndex:index];
2299 NSString *unattributedValue = [thisUserInfo objectForKey:keyName];
2301 if(![unattributedValue isEqualToString:@""]) {
2302 NSString *readableName = [readableNames objectAtIndex:index];
2303 NSAttributedString *value;
2305 if([keyName isEqualToString:@"friends_count"]) {
2306 value = [NSAttributedString attributedStringWithLinkLabel:unattributedValue
2307 linkDestination:[self addressForLinkType:AITwitterLinkFriends userID:listContact.UID statusID:nil context:nil]];
2308 } else if ([keyName isEqualToString:@"followers_count"]) {
2309 value = [NSAttributedString attributedStringWithLinkLabel:unattributedValue
2310 linkDestination:[self addressForLinkType:AITwitterLinkFollowers userID:listContact.UID statusID:nil context:nil]];
2311 } else if ([keyName isEqualToString:@"statuses_count"]) {
2312 value = [NSAttributedString attributedStringWithLinkLabel:unattributedValue
2313 linkDestination:[self addressForLinkType:AITwitterLinkUserPage userID:listContact.UID statusID:nil context:nil]];
2315 value = [NSAttributedString stringWithString:unattributedValue];
2318 [profileArray addObject:[NSDictionary dictionaryWithObjectsAndKeys:readableName, KEY_KEY, value, KEY_VALUE, nil]];
2322 AILogWithSignature(@"%@ Updating profileArray for user %@", self, listContact);
2324 [listContact setProfileArray:profileArray notify:NotifyNow];
2326 // Grab their statuses.
2327 NSString *requestID = [twitterEngine getUserTimelineFor:listContact.UID since:nil startingAtPage:0 count:TWITTER_UPDATE_USER_INFO_COUNT];
2330 [self setRequestType:AITwitterProfileStatusUpdates
2331 forRequestID:requestID
2332 withDictionary:[NSDictionary dictionaryWithObject:listContact forKey:@"ListContact"]];
2335 } else if ([self requestTypeForRequestID:identifier] == AITwitterValidateCredentials ||
2336 [self requestTypeForRequestID:identifier] == AITwitterProfileSelf) {
2337 for (NSDictionary *info in userInfo) {
2338 NSString *requestID = [twitterEngine getImageAtURL:[info objectForKey:TWITTER_INFO_ICON]];
2341 [self setRequestType:AITwitterSelfUserIconPull
2342 forRequestID:requestID
2343 withDictionary:nil];
2346 [self filterAndSetUID:[info objectForKey:TWITTER_INFO_UID]];
2348 if ([info objectForKey:@"name"]) {
2349 [self setPreference:[[NSAttributedString stringWithString:[info objectForKey:@"name"]] dataRepresentation]
2350 forKey:KEY_ACCOUNT_DISPLAY_NAME
2351 group:GROUP_ACCOUNT_STATUS];
2354 [self setValue:[info objectForKey:@"name"] forProperty:@"Profile Name" notify:NotifyLater];
2355 [self setValue:[info objectForKey:@"url"] forProperty:@"Profile URL" notify:NotifyLater];
2356 [self setValue:[info objectForKey:@"location"] forProperty:@"Profile Location" notify:NotifyLater];
2357 [self setValue:[info objectForKey:@"description"] forProperty:@"Profile Description" notify:NotifyLater];
2358 [self notifyOfChangedPropertiesSilently:NO];
2362 if([self requestTypeForRequestID:identifier] == AITwitterValidateCredentials) {
2363 // Our UID is definitely set; grab our friends.
2365 if ([[self preferenceForKey:TWITTER_PREFERENCE_LOAD_CONTACTS group:TWITTER_PREFERENCE_GROUP_UPDATES] boolValue]) {
2366 // If we load our follows as contacts, do so now.
2368 // Delay updates on initial login.
2369 [self silenceAllContactUpdatesForInterval:18.0];
2370 // Grab our user list.
2371 NSString *requestID = [twitterEngine getRecentlyUpdatedFriendsFor:self.UID startingAtPage:1];
2374 [self setRequestType:AITwitterInitialUserInfo
2375 forRequestID:requestID
2376 withDictionary:[NSDictionary dictionaryWithObject:[NSNumber numberWithInt:1] forKey:@"Page"]];
2378 [self setLastDisconnectionError:AILocalizedString(@"Unable to retrieve user list", nil)];
2379 [self didDisconnect];
2382 // If we don't load follows as contacts, we've finished connecting (fast, wasn't it?)
2386 } else if ([self requestTypeForRequestID:identifier] == AITwitterNotificationEnable ||
2387 [self requestTypeForRequestID:identifier] == AITwitterNotificationDisable) {
2388 BOOL enableNotification = ([self requestTypeForRequestID:identifier] == AITwitterNotificationEnable);
2389 AIListContact *listContact = [[self dictionaryForRequestID:identifier] objectForKey:@"ListContact"];
2391 for (NSDictionary *info in userInfo) {
2392 [adium.interfaceController handleMessage:(enableNotification ?
2393 AILocalizedString(@"Notifications Enabled", nil) :
2394 AILocalizedString(@"Notifications Disabled", nil))
2395 withDescription:[NSString stringWithFormat:(enableNotification ?
2396 AILocalizedString(@"You will now receive device notifications for %@.", nil) :
2397 AILocalizedString(@"You will no longer receive device notifications for %@.", nil)),
2399 withWindowTitle:(enableNotification ?
2400 AILocalizedString(@"Notifications Enabled", nil) :
2401 AILocalizedString(@"Notifications Disabled", nil))];
2405 [self clearRequestTypeForRequestID:identifier];
2409 * @brief Miscellaneous information received
2411 - (void)miscInfoReceived:(NSArray *)miscInfo forRequest:(NSString *)identifier
2413 if([self requestTypeForRequestID:identifier] == AITwitterRateLimitStatus) {
2414 NSDictionary *rateLimit = [miscInfo objectAtIndex:0];
2415 NSDate *resetDate = [NSDate dateWithTimeIntervalSince1970:[[rateLimit objectForKey:TWITTER_RATE_LIMIT_RESET_SECONDS] intValue]];
2417 [adium.interfaceController handleMessage:AILocalizedString(@"Current Twitter rate limit", "Message in the rate limit status window")
2418 withDescription:[NSString stringWithFormat:AILocalizedString(@"You have %d/%d more requests for %@.", "The first %d is the number of requests, the second is the total number of requests per hour. The %@ is the duration of time until the count resets."),
2419 [[rateLimit objectForKey:TWITTER_RATE_LIMIT_REMAINING] intValue],
2420 [[rateLimit objectForKey:TWITTER_RATE_LIMIT_HOURLY_LIMIT] intValue],
2421 [NSDateFormatter stringForTimeInterval:[resetDate timeIntervalSinceNow]
2425 withWindowTitle:AILocalizedString(@"Rate Limit Status", nil)];
2428 [self clearRequestTypeForRequestID:identifier];
2432 * @brief Requested image received
2434 - (void)imageReceived:(NSImage *)image forRequest:(NSString *)identifier
2436 if([self requestTypeForRequestID:identifier] == AITwitterUserIconPull) {
2437 AIListContact *listContact = [[self dictionaryForRequestID:identifier] objectForKey:@"ListContact"];
2439 AILogWithSignature(@"%@ Updated user icon for %@", self, listContact);
2441 [listContact setServersideIconData:[image TIFFRepresentation]
2442 notify:NotifyLater];
2444 [listContact setValue:nil forProperty:TWITTER_PROPERTY_REQUESTED_USER_ICON notify:NotifyNever];
2445 } else if([self requestTypeForRequestID:identifier] == AITwitterSelfUserIconPull) {
2446 AILogWithSignature(@"Updated self icon for %@", self);
2448 // Set a property so we don't re-send thie image we're just now downloading.
2449 [self setValue:[NSNumber numberWithBool:YES] forProperty:TWITTER_PROPERTY_REQUESTED_USER_ICON notify:NotifyNever];
2451 [self setPreference:[NSNumber numberWithBool:YES]
2452 forKey:KEY_USE_USER_ICON
2453 group:GROUP_ACCOUNT_STATUS];
2456 [self setPreference:[image TIFFRepresentation]
2457 forKey:KEY_USER_ICON
2458 group:GROUP_ACCOUNT_STATUS];
2461 [self clearRequestTypeForRequestID:identifier];