Patch from brion to add an SSL option for StatusNet accounts. Fixes #13077.
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/AIContentMessage.h>
35 #import <Adium/AIListBookmark.h>
36 #import <Adium/AIChat.h>
37 #import <Adium/AIUserIcons.h>
38 #import <Adium/AIService.h>
39 #import <Adium/AIStatus.h>
40 #import <Adium/AIHTMLDecoder.h>
41 #import <Adium/AIContentEvent.h>
43 @interface AITwitterAccount()
44 - (void)updateUserIcon:(NSString *)url forContact:(AIListContact *)listContact;
46 - (void)updateTimelineChat:(AIChat *)timelineChat;
48 - (NSAttributedString *)parseMessage:(NSString *)inMessage
49 tweetID:(NSString *)tweetID
50 userID:(NSString *)userID
51 inReplyToUser:(NSString *)replyUserID
52 inReplyToTweetID:(NSString *)replyTweetID;
53 - (NSAttributedString *)parseDirectMessage:(NSString *)inMessage
54 withID:(NSString *)dmID
55 fromUser:(NSString *)sourceUID;
56 - (NSAttributedString *)attributedStringWithLinkLabel:(NSString *)label
57 linkDestination:(NSString *)destination
58 linkClass:(NSString *)attributeName;
60 - (void)setRequestType:(AITwitterRequestType)type forRequestID:(NSString *)requestID withDictionary:(NSDictionary *)info;
61 - (AITwitterRequestType)requestTypeForRequestID:(NSString *)requestID;
62 - (NSDictionary *)dictionaryForRequestID:(NSString *)requestID;
63 - (void)clearRequestTypeForRequestID:(NSString *)requestID;
65 - (void)periodicUpdate;
66 - (void)displayQueuedUpdatesForRequestType:(AITwitterRequestType)requestType;
68 - (void)getRateLimitAmount;
71 @implementation AITwitterAccount
76 pendingRequests = [[NSMutableDictionary alloc] init];
77 queuedUpdates = [[NSMutableArray alloc] init];
78 queuedDM = [[NSMutableArray alloc] init];
79 queuedOutgoingDM = [[NSMutableArray alloc] init];
81 [[NSNotificationCenter defaultCenter] addObserver:self
82 selector:@selector(chatDidOpen:)
86 [adium.preferenceController registerDefaults:[NSDictionary dictionaryWithObjectsAndKeys:
87 [NSNumber numberWithInt:TWITTER_UPDATE_INTERVAL_MINUTES], TWITTER_PREFERENCE_UPDATE_INTERVAL,
88 [NSNumber numberWithBool:YES], TWITTER_PREFERENCE_UPDATE_AFTER_SEND,
89 [NSNumber numberWithBool:YES], TWITTER_PREFERENCE_LOAD_CONTACTS, nil]
90 forGroup:TWITTER_PREFERENCE_GROUP_UPDATES
93 // If we don't have a server set, set our default (if we have one)
94 if (!self.host && self.defaultServer) {
95 [self setPreference:self.defaultServer forKey:KEY_CONNECT_HOST group:GROUP_ACCOUNT_STATUS];
98 [adium.preferenceController registerPreferenceObserver:self forGroup:TWITTER_PREFERENCE_GROUP_UPDATES];
99 [adium.preferenceController informObserversOfChangedKey:nil inGroup:TWITTER_PREFERENCE_GROUP_UPDATES object:self];
104 [[NSNotificationCenter defaultCenter] removeObserver:self];
105 [adium.preferenceController unregisterPreferenceObserver:self];
107 [twitterEngine release];
108 [pendingRequests release];
109 [queuedUpdates release];
111 [queuedOutgoingDM release];
117 * @brief Our default server if none is provided.
119 - (NSString *)defaultServer
121 return @"twitter.com";
124 #pragma mark AIAccount methods
126 * @brief We've been asked to connect.
128 * Sets our username and password for MGTwitterEngine, and validates credentials.
130 * Our connection procedure:
131 * 1. Validate credentials
132 * 2. Retrieve friends
133 * 3. Trigger "periodic" update - DM, replies, timeline
139 [twitterEngine release];
141 twitterEngine = [[MGTwitterEngine alloc] initWithDelegate:self];
143 [twitterEngine setClientName:@"Adium"
144 version:[NSApp applicationVersion]
145 URL:@"http://www.adiumx.com"
146 token:self.sourceToken];
148 [twitterEngine setAPIDomain:[self.host stringByAppendingPathComponent:self.apiPath]];
150 [twitterEngine setUsesSecureConnection:self.useSSL];
153 if (!self.passwordWhileConnected.length) {
154 [self setLastDisconnectionError:TWITTER_OAUTH_NOT_AUTHORIZED];
156 [[NSNotificationCenter defaultCenter] postNotificationName:@"AIEditAccount"
159 [self didDisconnect];
161 // Don't try and connect.
165 twitterEngine.useOAuth = YES;
167 OAToken *token = [[[OAToken alloc] initWithHTTPResponseBody:self.passwordWhileConnected] autorelease];
168 OAConsumer *consumer = [[[OAConsumer alloc] initWithKey:self.consumerKey secret:self.secretKey] autorelease];
170 twitterEngine.accessToken = token;
171 twitterEngine.consumer = consumer;
174 [twitterEngine setUsername:self.UID password:self.passwordWhileConnected];
177 AILogWithSignature(@"%@ connecting to %@", self, twitterEngine.APIDomain);
179 NSString *requestID = [twitterEngine checkUserCredentials];
182 [self setRequestType:AITwitterValidateCredentials forRequestID:requestID withDictionary:nil];
184 [self setLastDisconnectionError:AILocalizedString(@"Unable to connect to server", nil)];
185 [self didDisconnect];
190 * @brief Connection successful
192 * Our credentials were validated correctly. Set up the timeline chat, and request our friends from the server.
198 //Clear any previous disconnection error
199 [self setLastDisconnectionError:nil];
201 // Creating the fake timeline account.
202 AIListBookmark *timelineBookmark = nil;
204 if(!(timelineBookmark = [adium.contactController existingBookmarkForChatName:self.timelineChatName
206 chatCreationInfo:nil])) {
207 AIChat *newTimelineChat = [adium.chatController chatWithName:self.timelineChatName
210 chatCreationInfo:nil];
212 [newTimelineChat setDisplayName:self.timelineChatName];
214 timelineBookmark = [adium.contactController bookmarkForChat:newTimelineChat inGroup:[adium.contactController groupWithUID:TWITTER_REMOTE_GROUP_NAME]];
218 NSTimeInterval updateInterval = [[self preferenceForKey:TWITTER_PREFERENCE_UPDATE_INTERVAL group:TWITTER_PREFERENCE_GROUP_UPDATES] integerValue] * 60;
220 if(updateInterval > 0) {
221 [updateTimer invalidate];
222 updateTimer = [NSTimer scheduledTimerWithTimeInterval:updateInterval
224 selector:@selector(periodicUpdate)
228 [self periodicUpdate];
233 * @brief We've been asked to disconnect.
241 [twitterEngine release]; twitterEngine = nil;
242 [updateTimer invalidate]; updateTimer = nil;
244 [self didDisconnect];
248 * @brief Account will be deleted
250 - (void)willBeDeleted
252 [updateTimer invalidate]; updateTimer = nil;
254 [super willBeDeleted];
258 * @brief Session ended
260 * Remove all state information.
262 - (void)didDisconnect
264 [updateTimer invalidate]; updateTimer = nil;
265 [pendingRequests removeAllObjects];
266 [queuedDM removeAllObjects];
267 [queuedOutgoingDM removeAllObjects];
268 [queuedUpdates removeAllObjects];
270 [super didDisconnect];
276 * The API path extension for the given host.
278 - (NSString *)apiPath
284 * @brief Our source token
286 * On Twitter, our given source token is "adiumofficial".
288 - (NSString *)sourceToken
290 return @"adiumofficial";
294 * @brief Returns whether or not to connect to Twitter API over HTTPS.
302 * @brief Returns whether or not this account is connected via an encrypted connection.
306 return (self.online && [twitterEngine usesSecureConnection]);
310 * @brief Affirm we can open chats.
312 - (BOOL)openChat:(AIChat *)chat
314 [chat setValue:[NSNumber numberWithBool:YES] forProperty:@"Account Joined" notify:NotifyNow];
320 * @brief Allow all chats to close.
322 - (BOOL)closeChat:(AIChat *)inChat
328 * @brief Rejoin the requested chat.
330 - (BOOL)rejoinChat:(AIChat *)inChat
332 [self displayYouHaveConnectedInChat:inChat];
338 * @brief We always want to autocomplete the UID.
340 - (BOOL)chatShouldAutocompleteUID:(AIChat *)inChat
346 * @brief A chat opened.
348 * If this is a group chat which belongs to us, aka a timeline chat, set it up how we want it.
350 - (void)chatDidOpen:(NSNotification *)notification
352 AIChat *chat = [notification object];
354 if(chat.isGroupChat && chat.account == self) {
355 [self updateTimelineChat:chat];
360 * @brief We support adding and removing follows.
362 - (BOOL)contactListEditable
368 * @brief Move contacts
370 * Move existing contacts to a specific group on this account. The passed contacts should already exist somewhere on
372 * @param objects NSArray of AIListContact objects to remove
373 * @param group AIListGroup destination for contacts
375 - (void)moveListObjects:(NSArray *)objects oldGroups:(NSSet *)oldGroups toGroups:(NSSet *)groups
377 // XXX do twitter grouping
381 * @brief Rename a group
383 * Rename a group on this account.
384 * @param group AIListGroup to rename
385 * @param newName NSString name for the group
387 - (void)renameGroup:(AIListGroup *)group to:(NSString *)newName
389 // XXX do twitter grouping
393 * @brief For an invalid password, fail but don't try and reconnect or report it. We do it ourself.
395 - (AIReconnectDelayType)shouldAttemptReconnectAfterDisconnectionError:(NSString **)disconnectionError
397 AIReconnectDelayType reconnectDelayType = [super shouldAttemptReconnectAfterDisconnectionError:disconnectionError];
399 if ([*disconnectionError isEqualToString:TWITTER_INCORRECT_PASSWORD_MESSAGE]) {
400 reconnectDelayType = AIReconnectImmediately;
401 } else if ([*disconnectionError isEqualToString:TWITTER_OAUTH_NOT_AUTHORIZED]) {
402 reconnectDelayType = AIReconnectNeverNoMessage;
405 return reconnectDelayType;
409 * @brief Don't allow OTR encryption.
411 - (BOOL)allowSecureMessagingTogglingForChat:(AIChat *)inChat
417 * @brief Update our status
419 - (void)setSocialNetworkingStatusMessage:(NSAttributedString *)statusMessage
421 NSString *requestID = [twitterEngine sendUpdate:[statusMessage string]];
424 [self setRequestType:AITwitterSendUpdate
425 forRequestID:requestID
430 - (NSString *)encodedAttributedString:(NSAttributedString *)inAttributedString forListObject:(AIListObject *)inListObject
432 return [[inAttributedString attributedStringByConvertingLinksToURLStrings] string];
436 * @brief Send a message
438 * Sends a direct message to the user requested.
439 * If it fails to send, i.e. the request fails, an unknown error will occur.
440 * This is usually caused by the target not following the user.
442 - (BOOL)sendMessageObject:(AIContentMessage *)inContentMessage
446 if(inContentMessage.chat.isGroupChat) {
447 requestID = [twitterEngine sendUpdate:inContentMessage.encodedMessage
448 inReplyTo:[inContentMessage.chat valueForProperty:@"TweetInReplyToStatusID"]];
451 [self setRequestType:AITwitterSendUpdate
452 forRequestID:requestID
453 withDictionary:[NSDictionary dictionaryWithObject:inContentMessage.chat
456 inContentMessage.displayContent = NO;
458 AILogWithSignature(@"%@ Sending update [in reply to %@]: %@", self, [inContentMessage.chat valueForProperty:@"TweetInReplyToStatusID"], inContentMessage.encodedMessage);
462 requestID = [twitterEngine sendDirectMessage:inContentMessage.encodedMessage
463 to:inContentMessage.destination.UID];
466 [self setRequestType:AITwitterDirectMessageSend
467 forRequestID:requestID
468 withDictionary:[NSDictionary dictionaryWithObject:inContentMessage.chat
471 inContentMessage.displayContent = NO;
473 AILogWithSignature(@"%@ Sending DM to %@: %@", self, inContentMessage.destination.UID, inContentMessage.encodedMessage);
478 AILogWithSignature(@"%@ Message immediate fail.", self);
481 return (requestID != nil);
485 * @brief Trigger an info update
487 * This is called when the info inspector wants more information on a contact.
488 * Grab the user's profile information, set everything up accordingly in the user info method.
490 - (void)delayedUpdateContactStatus:(AIListContact *)inContact
496 NSString *requestID = [twitterEngine getUserInformationFor:inContact.UID];
499 [self setRequestType:AITwitterProfileUserInfo
500 forRequestID:requestID
501 withDictionary:[NSDictionary dictionaryWithObject:inContact forKey:@"ListContact"]];
506 * @brief Should an autoreply be sent to this message?
508 - (BOOL)shouldSendAutoreplyToMessage:(AIContentMessage *)message
514 * @brief Update the Twitter profile
516 - (void)setProfileName:(NSString *)name
518 location:(NSString *)location
519 description:(NSString *)description
521 NSString *requestID = [twitterEngine updateProfileName:name
525 description:description];
528 [self setRequestType:AITwitterProfileSelf
529 forRequestID:requestID
536 * @brief Should we store our password based on internal object ID?
538 * We only need to if we're using OAuth.
540 - (BOOL)useInternalObjectIDForPasswordName
542 return self.useOAuth;
546 * @brief Should we connect using OAuth?
548 * If enabled, the account view will display the OAuth setup. Basic authentication will not be used.
556 * @brief OAuth consumer key
558 - (NSString *)consumerKey
560 return @"amjYVOrzKpKkkHAsdEaClA";
564 * @brief OAuth secret key
566 - (NSString *)secretKey
568 return @"kvqM2CQsUO3J6NHctJVhTOzlKZ0k7FsTaR5NwakYU";
572 * @brief Token request URL
574 - (NSString *)tokenRequestURL
576 return @"https://twitter.com/oauth/request_token";
580 * @brief Token access URL
582 - (NSString *)tokenAccessURL
584 return @"https://twitter.com/oauth/access_token";
588 * @brief Token authorize URL
590 - (NSString *)tokenAuthorizeURL
592 return @"https://twitter.com/oauth/authorize";
595 #pragma mark Menu Items
597 * @brief Menu items for contact
599 * Returns an array of menu items for a contact on this account. This is the best place to add protocol-specific
600 * actions that aren't otherwise supported by Adium.
601 * @param inContact AIListContact for menu items
602 * @return NSArray of NSMenuItem instances for the passed contact
604 - (NSArray *)menuItemsForContact:(AIListContact *)inContact
606 NSMutableArray *menuItemArray = [NSMutableArray array];
608 NSMenuItem *menuItem;
610 NSImage *serviceIcon = [AIServiceIcons serviceIconForService:self.service
611 type:AIServiceIconSmall
612 direction:AIIconNormal];
614 menuItem = [[[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:[NSString stringWithFormat:AILocalizedString(@"Open %@'s user page",nil), inContact.UID]
616 action:@selector(openUserPage:)
617 keyEquivalent:@""] autorelease];
618 [menuItem setImage:serviceIcon];
619 [menuItem setRepresentedObject:inContact];
620 [menuItemArray addObject:menuItem];
622 menuItem = [[[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:[NSString stringWithFormat:AILocalizedString(@"Enable device notifications for %@",nil), inContact.UID]
624 action:@selector(enableOrDisableNotifications:)
625 keyEquivalent:@""] autorelease];
626 [menuItem setTag:YES];
627 [menuItem setImage:serviceIcon];
628 [menuItem setRepresentedObject:inContact];
629 [menuItemArray addObject:menuItem];
631 menuItem = [[[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:[NSString stringWithFormat:AILocalizedString(@"Disable device notifications for %@",nil), inContact.UID]
633 action:@selector(enableOrDisableNotifications:)
634 keyEquivalent:@""] autorelease];
635 [menuItem setTag:NO];
636 [menuItem setImage:serviceIcon];
637 [menuItem setRepresentedObject:inContact];
638 [menuItemArray addObject:menuItem];
640 return menuItemArray;
644 * @brief Open the represented objec'ts user page
646 - (void)openUserPage:(NSMenuItem *)menuItem
648 [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:[self addressForLinkType:AITwitterLinkUserPage
649 userID:((AIListContact *)menuItem.representedObject).UID
655 * @brief Enable or disable notifications for a contact.
657 * If the menuItem's tag is YES, we're adding. Otherwise we're removing.
659 - (void)enableOrDisableNotifications:(NSMenuItem *)menuItem
661 if(![menuItem.representedObject isKindOfClass:[AIListContact class]]) {
665 BOOL enableNotification = menuItem.tag;
666 AIListContact *contact = menuItem.representedObject;
668 NSString *requestID = nil;
670 BOOL initialFailure = NO;
672 if (enableNotification) {
673 requestID = [twitterEngine enableNotificationsFor:contact.UID];
676 [self setRequestType:AITwitterNotificationEnable
677 forRequestID:requestID
678 withDictionary:[NSDictionary dictionaryWithObjectsAndKeys:contact, @"ListContact", nil]];
680 initialFailure = YES;
684 requestID = [twitterEngine disableNotificationsFor:contact.UID];
687 [self setRequestType:AITwitterNotificationDisable
688 forRequestID:requestID
689 withDictionary:[NSDictionary dictionaryWithObjectsAndKeys:contact, @"ListContact", nil]];
691 initialFailure = YES;
695 if (initialFailure) {
696 [adium.interfaceController handleErrorMessage:(enableNotification ?
697 AILocalizedString(@"Unable to Enable Notifications", nil) :
698 AILocalizedString(@"Unable to Disable Notifications", nil))
699 withDescription:AILocalizedString(@"Unable to connect to the Twitter server.", nil)];
704 * @brief Menu items for chat
706 * Returns an array of menu items for a chat on this account. This is the best place to add protocol-specific
707 * actions that aren't otherwise supported by Adium.
708 * @param inChat AIChat for menu items
709 * @return NSArray of NSMenuItem instances for the passed contact
711 - (NSArray *)menuItemsForChat:(AIChat *)inChat
713 NSMutableArray *menuItemArray = [NSMutableArray array];
715 NSMenuItem *menuItem;
717 NSImage *serviceIcon = [AIServiceIcons serviceIconForService:self.service
718 type:AIServiceIconSmall
719 direction:AIIconNormal];
721 menuItem = [[[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:AILocalizedString(@"Update Tweets",nil)
723 action:@selector(periodicUpdate)
724 keyEquivalent:@""] autorelease];
725 [menuItem setImage:serviceIcon];
726 [menuItemArray addObject:menuItem];
728 menuItem = [[[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:AILocalizedString(@"Reply to a Tweet",nil)
730 action:@selector(replyToTweet)
731 keyEquivalent:@""] autorelease];
732 [menuItem setImage:serviceIcon];
733 [menuItemArray addObject:menuItem];
735 return menuItemArray;
739 * @brief Menu items for the account's actions
741 * Returns an array of menu items for account-specific actions. This is the best place to add protocol-specific
742 * actions that aren't otherwise supported by Adium. It will only be queried if the account is online.
743 * @return NSArray of NSMenuItem instances for this account
745 - (NSArray *)accountActionMenuItems
747 NSMutableArray *menuItemArray = [NSMutableArray array];
749 NSMenuItem *menuItem;
751 menuItem = [[[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:AILocalizedString(@"Update Tweets",nil)
753 action:@selector(periodicUpdate)
754 keyEquivalent:@""] autorelease];
755 [menuItemArray addObject:menuItem];
757 menuItem = [[[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:AILocalizedString(@"Reply to a Tweet",nil)
759 action:@selector(replyToTweet)
760 keyEquivalent:@""] autorelease];
761 [menuItemArray addObject:menuItem];
763 menuItem = [[[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:AILocalizedString(@"Get Rate Limit Amount",nil)
765 action:@selector(getRateLimitAmount)
766 keyEquivalent:@""] autorelease];
767 [menuItemArray addObject:menuItem];
769 return menuItemArray;
773 * @brief Open the reply to tweet window
775 * Opens a window in which the user can create a reply featuring in_reply_to_status_id being set.
779 [AITwitterReplyWindowController showReplyWindowForAccount:self];
783 * @brief Gets the current rate limit amount.
785 - (void)getRateLimitAmount
787 NSString *requestID = [twitterEngine getRateLimitStatus];
790 [self setRequestType:AITwitterRateLimitStatus
791 forRequestID:requestID
797 #pragma mark Contact handling
799 * @brief The name of our timeline chat
801 - (NSString *)timelineChatName
803 return [NSString stringWithFormat:TWITTER_TIMELINE_NAME, self.UID];
807 * @brief Our timeline chat
809 * If the timeline chat is not already active, it is created.
811 - (AIChat *)timelineChat
813 AIChat *timelineChat = [adium.chatController existingChatWithName:self.timelineChatName
817 timelineChat = [adium.chatController chatWithName:self.timelineChatName
820 chatCreationInfo:nil];
827 * @brief Update the timeline chat
829 * Remove the userlist
831 - (void)updateTimelineChat:(AIChat *)timelineChat
833 // Disable the user list on the chat.
834 if (timelineChat.chatContainer.chatViewController.userListVisible) {
835 [timelineChat.chatContainer.chatViewController toggleUserList];
838 // Update the participant list.
839 [timelineChat addParticipatingListObjects:self.contacts notify:NotifyNow];
841 [timelineChat setValue:[NSNumber numberWithInt:140] forProperty:@"Character Counter Max" notify:NotifyNow];
845 * @brief Update serverside icon
847 * This is called by AIUserIcons when it needs an icon update for a contact.
848 * If we already have an icon set (even a cached icon), ignore it.
849 * Otherwise return the Twitter service icon.
851 * This is so that when an unknown contact appears, it has an actual image
852 * to replace in the WKMV when an actual icon update is returned.
854 * This service icon will not remain saved very long, I see no harm in using it.
855 * This only occurs for "strangers".
857 - (NSData *)serversideIconDataForContact:(AIListContact *)listContact
859 if (![AIUserIcons userIconSourceForObject:listContact] &&
860 ![AIUserIcons cachedUserIconDataForObject:listContact]) {
861 return [[self.service defaultServiceIconOfType:AIServiceIconLarge] TIFFRepresentation];
868 * @brief Update a user icon from a URL if necessary
870 - (void)updateUserIcon:(NSString *)url forContact:(AIListContact *)listContact;
872 // If we don't already have an icon for the user...
873 if(![[listContact valueForProperty:TWITTER_PROPERTY_REQUESTED_USER_ICON] boolValue]) {
874 NSString *fileName = [[url lastPathComponent] stringByReplacingOccurrencesOfString:@"_normal." withString:@"_bigger."];
876 url = [[url stringByDeletingLastPathComponent] stringByAppendingPathComponent:fileName];
878 // Grab the user icon and set it as their serverside icon.
879 NSString *requestID = [twitterEngine getImageAtURL:url];
882 [self setRequestType:AITwitterUserIconPull
883 forRequestID:requestID
884 withDictionary:[NSDictionary dictionaryWithObject:listContact forKey:@"ListContact"]];
887 [listContact setValue:[NSNumber numberWithBool:YES] forProperty:TWITTER_PROPERTY_REQUESTED_USER_ICON notify:NotifyNever];
892 * @brief Unfollow the requested contacts.
894 - (void)removeContacts:(NSArray *)objects fromGroups:(NSArray *)groups
896 for (AIListContact *object in objects) {
897 NSString *requestID = [twitterEngine disableUpdatesFor:object.UID];
899 AILogWithSignature(@"%@ Requesting unfollow for: %@", self, object.UID);
902 [self setRequestType:AITwitterRemoveFollow
903 forRequestID:requestID
904 withDictionary:[NSDictionary dictionaryWithObject:object forKey:@"ListContact"]];
910 * @brief Follow the requested contact, trigger an information pull for them.
912 - (void)addContact:(AIListContact *)contact toGroup:(AIListGroup *)group
914 NSString *requestID = [twitterEngine enableUpdatesFor:contact.UID];
916 AILogWithSignature(@"%@ Requesting follow for: %@", self, contact.UID);
919 NSString *updateRequestID = [twitterEngine getUserInformationFor:contact.UID];
921 if (updateRequestID) {
922 [self setRequestType:AITwitterAddFollow
923 forRequestID:updateRequestID
924 withDictionary:[NSDictionary dictionaryWithObjectsAndKeys:contact.UID, @"UID", nil]];
929 #pragma mark Request cataloguing
931 * @brief Set the type and optional dictionary for a request ID
933 * Sets the AITwitterRequestType for a particular request ID, so when the request finishes we can identify what it is for.
934 * Optionally sets a dictionary which can be retrieved in association with the request type.
936 - (void)setRequestType:(AITwitterRequestType)type forRequestID:(NSString *)requestID withDictionary:(NSDictionary *)info
938 [pendingRequests setObject:[NSDictionary dictionaryWithObjectsAndKeys:[NSNumber numberWithInt:type], @"Type",
944 * @brief Get the request type for a request ID
946 - (AITwitterRequestType)requestTypeForRequestID:(NSString *)requestID
948 return [(NSNumber *)[[pendingRequests objectForKey:requestID] objectForKey:@"Type"] intValue];
952 * @brief Get the dictionary associated with a request ID
954 - (NSDictionary *)dictionaryForRequestID:(NSString *)requestID
956 return (NSDictionary *)[[pendingRequests objectForKey:requestID] objectForKey:@"Info"];
960 * @brief Remove a request ID's saved information.
962 - (void)clearRequestTypeForRequestID:(NSString *)requestID
964 [pendingRequests removeObjectForKey:requestID];
967 #pragma mark Preference updating
968 - (void)preferencesChangedForGroup:(NSString *)group key:(NSString *)key object:(AIListObject *)object
969 preferenceDict:(NSDictionary *)prefDict firstTime:(BOOL)firstTime
971 [super preferencesChangedForGroup:group key:key object:object preferenceDict:prefDict firstTime:firstTime];
973 // We only care about our changes.
974 if (object != self) {
978 if([group isEqualToString:GROUP_ACCOUNT_STATUS]) {
979 if([key isEqualToString:KEY_USER_ICON]) {
980 // Avoid pushing an icon update which we just downloaded.
981 if(![self boolValueForProperty:TWITTER_PROPERTY_REQUESTED_USER_ICON]) {
982 NSString *requestID = [twitterEngine updateProfileImage:[prefDict objectForKey:KEY_USER_ICON]];
985 AILogWithSignature(@"%@ Pushing self icon update", self);
987 [self setRequestType:AITwitterProfileSelf
988 forRequestID:requestID
993 [self setValue:nil forProperty:TWITTER_PROPERTY_REQUESTED_USER_ICON notify:NotifyNever];
997 if([group isEqualToString:TWITTER_PREFERENCE_GROUP_UPDATES]) {
998 if(!firstTime && [key isEqualToString:TWITTER_PREFERENCE_UPDATE_INTERVAL]) {
999 NSTimeInterval timeInterval = [updateTimer timeInterval];
1000 NSTimeInterval newTimeInterval = [[prefDict objectForKey:TWITTER_PREFERENCE_UPDATE_INTERVAL] intValue] * 60;
1002 if (timeInterval != newTimeInterval && self.online) {
1003 [updateTimer invalidate]; updateTimer = nil;
1005 if(newTimeInterval > 0) {
1006 updateTimer = [NSTimer scheduledTimerWithTimeInterval:newTimeInterval
1008 selector:@selector(periodicUpdate)
1015 updateAfterSend = [[prefDict objectForKey:TWITTER_PREFERENCE_UPDATE_AFTER_SEND] boolValue];
1016 retweetLink = [[prefDict objectForKey:TWITTER_PREFERENCE_RETWEET_SPAM] boolValue];
1018 if ([key isEqualToString:TWITTER_PREFERENCE_LOAD_CONTACTS] && self.online) {
1019 if ([[prefDict objectForKey:TWITTER_PREFERENCE_LOAD_CONTACTS] boolValue]) {
1020 // Delay updates when loading our contacts list.
1021 [self silenceAllContactUpdatesForInterval:18.0];
1022 // Grab our user list.
1023 NSString *requestID = [twitterEngine getRecentlyUpdatedFriendsFor:self.UID startingAtPage:1];
1026 [self setRequestType:AITwitterInitialUserInfo
1027 forRequestID:requestID
1028 withDictionary:[NSDictionary dictionaryWithObject:[NSNumber numberWithInt:1] forKey:@"Page"]];
1031 [self removeAllContacts];
1037 #pragma mark Periodic update scheduler
1039 * @brief Trigger our periodic updates
1041 - (void)periodicUpdate
1043 if (pendingUpdateCount) {
1044 AILogWithSignature(@"%@ Update already in progress. Count = %d", self, pendingUpdateCount);
1048 NSString *requestID;
1051 // We haven't completed the timeline nor replies. This lets us know if we should display statuses.
1052 followedTimelineCompleted = repliesCompleted = NO;
1053 futureTimelineLastID = futureRepliesLastID = nil;
1055 // Prevent triggering this update routine multiple times.
1056 pendingUpdateCount = 3;
1058 // We haven't printed error messages for this set.
1059 timelineErrorMessagePrinted = NO;
1061 [queuedUpdates removeAllObjects];
1062 [queuedDM removeAllObjects];
1064 AILogWithSignature(@"%@ Periodic update fire", self);
1066 // Pull direct messages
1067 lastID = [self preferenceForKey:TWITTER_PREFERENCE_DM_LAST_ID
1068 group:TWITTER_PREFERENCE_GROUP_UPDATES];
1070 requestID = [twitterEngine getDirectMessagesSinceID:lastID startingAtPage:1];
1073 [self setRequestType:AITwitterUpdateDirectMessage
1074 forRequestID:requestID
1075 withDictionary:[NSDictionary dictionaryWithObjectsAndKeys:[NSNumber numberWithInt:1], @"Page", nil]];
1077 --pendingUpdateCount;
1080 // Pull followed timeline
1081 lastID = [self preferenceForKey:TWITTER_PREFERENCE_TIMELINE_LAST_ID
1082 group:TWITTER_PREFERENCE_GROUP_UPDATES];
1084 requestID = [twitterEngine getFollowedTimelineFor:nil
1087 count:(lastID ? TWITTER_UPDATE_TIMELINE_COUNT : TWITTER_UPDATE_TIMELINE_COUNT_FIRST_RUN)];
1090 [self setRequestType:AITwitterUpdateFollowedTimeline
1091 forRequestID:requestID
1092 withDictionary:[NSDictionary dictionaryWithObjectsAndKeys:[NSNumber numberWithInt:1], @"Page", nil]];
1094 --pendingUpdateCount;
1097 // Pull the replies feed
1098 lastID = [self preferenceForKey:TWITTER_PREFERENCE_REPLIES_LAST_ID
1099 group:TWITTER_PREFERENCE_GROUP_UPDATES];
1101 requestID = [twitterEngine getRepliesSinceID:lastID startingAtPage:1];
1104 [self setRequestType:AITwitterUpdateReplies
1105 forRequestID:requestID
1106 withDictionary:[NSDictionary dictionaryWithObjectsAndKeys:[NSNumber numberWithInt:1], @"Page", nil]];
1108 --pendingUpdateCount;
1112 #pragma mark Message Display
1114 * @brief Returns a user-readable message for an error code.
1116 - (NSString *)errorMessageForError:(NSError *)error
1118 switch (error.code) {
1120 // Bad Request: your request is invalid, and we'll return an error message that tells you why.
1121 // This is the status code returned if you've exceeded the rate limit.
1122 return AILocalizedString(@"You've exceeded the rate limit.", nil);
1126 // Not Authorized: either you need to provide authentication credentials, or the credentials provided aren't valid.
1127 return AILocalizedString(@"Your credentials do not allow you access.", nil);
1131 // Forbidden: we understand your request, but are refusing to fulfill it. An accompanying error message should explain why.
1132 return AILocalizedString(@"Request refused by the server.", nil);
1136 // Not Found: either you're requesting an invalid URI or the resource in question doesn't exist (ex: no such user).
1137 return AILocalizedString(@"Requested resource not found.", nil);
1141 // Internal Server Error: we did something wrong. Please post to the group about it and the Twitter team will investigate.
1142 return AILocalizedString(@"The server reported an internal error.", nil);
1146 // Bad Gateway: returned if Twitter is down or being upgraded.
1147 return AILocalizedString(@"The server is currently down.", nil);
1153 // Service Unavailable: the Twitter servers are up, but are overloaded with requests. Try again later.
1154 return AILocalizedString(@"The server is overloaded with requests.", nil);
1159 return [NSString stringWithFormat:AILocalizedString(@"Unknown error: code %d, %@", nil), error.code, error.localizedDescription];
1163 * @brief Returns the link URL for a specific type of link
1165 - (NSString *)addressForLinkType:(AITwitterLinkType)linkType
1166 userID:(NSString *)userID
1167 statusID:(NSString *)statusID
1168 context:(NSString *)context
1170 NSString *address = nil;
1172 if (linkType == AITwitterLinkStatus) {
1173 address = [NSString stringWithFormat:@"https://twitter.com/%@/status/%@", userID, statusID];
1174 } else if (linkType == AITwitterLinkFriends) {
1175 address = [NSString stringWithFormat:@"https://twitter.com/%@/friends", userID];
1176 } else if (linkType == AITwitterLinkFollowers) {
1177 address = [NSString stringWithFormat:@"https://twitter.com/%@/followers", userID];
1178 } else if (linkType == AITwitterLinkUserPage) {
1179 address = [NSString stringWithFormat:@"https://twitter.com/%@", userID];
1180 } else if (linkType == AITwitterLinkSearchHash) {
1181 address = [NSString stringWithFormat:@"http://search.twitter.com/search?q=%%23%@", context];
1182 } else if (linkType == AITwitterLinkReply) {
1183 address = [NSString stringWithFormat:@"twitterreply://%@@%@?action=reply&status=%@", self.internalObjectID, userID, statusID];
1184 } else if (linkType == AITwitterLinkRetweet) {
1185 address = [NSString stringWithFormat:@"twitterreply://%@@%@?action=retweet&status=%@&message=%@", self.internalObjectID, userID, statusID, context];
1186 } else if (linkType == AITwitterLinkFavorite) {
1187 address = [NSString stringWithFormat:@"twitterreply://%@@%@?action=favorite&status=%@", self.internalObjectID, userID, statusID];
1188 } else if (linkType == AITwitterLinkDestroyStatus) {
1189 address = [NSString stringWithFormat:@"twitterreply://%@@%@?action=destroy&status=%@&message=%@", self.internalObjectID, userID, statusID, context];
1190 } else if (linkType == AITwitterLinkDestroyDM) {
1191 address = [NSString stringWithFormat:@"twitterreply://%@@%@?action=destroy&dm=%@&message=%@", self.internalObjectID, userID, statusID, context];
1198 * @brief Toggle the favorite status for a tweet.
1200 * Attempts to favorite a tweet. If that fails, it removes favorite status.
1201 * Prints a status message in the chat on success/failure, since it's otherwise not obvious.
1203 - (void)toggleFavoriteTweet:(NSString *)tweetID
1205 NSString *requestID = [twitterEngine markUpdate:tweetID asFavorite:YES];
1208 [self setRequestType:AITwitterFavoriteYes
1209 forRequestID:requestID
1210 withDictionary:[NSDictionary dictionaryWithObjectsAndKeys:tweetID, @"tweetID", nil]];
1212 AIChat *timelineChat = self.timelineChat;
1214 [adium.contentController displayEvent:AILocalizedString(@"Attempt to favorite tweet failed to connect.", nil)
1216 inChat:timelineChat];
1221 * @brief Destroy the tweet.
1223 * The user has already confirmed they want to destroy it; send the message.
1225 - (void)destroyTweet:(NSString *)tweetID
1227 NSString *requestID = [twitterEngine deleteUpdate:tweetID];
1230 [self setRequestType:AITwitterDestroyStatus
1231 forRequestID:requestID
1232 withDictionary:nil];
1234 AIChat *timelineChat = self.timelineChat;
1236 [adium.contentController displayEvent:AILocalizedString(@"Attempt to delete tweet failed to connect.", nil)
1238 inChat:timelineChat];
1243 * @brief Destroy the DM.
1245 * The user has already confirmed they want to destroy it; send the message.
1247 - (void)destroyDirectMessage:(NSString *)messageID
1248 forUser:(NSString *)userID
1250 NSString *requestID = [twitterEngine deleteDirectMessage:messageID];
1251 AIListContact *contact = [self contactWithUID:userID];
1254 [self setRequestType:AITwitterDestroyDM
1255 forRequestID:requestID
1256 withDictionary:[NSDictionary dictionaryWithObject:contact forKey:@"ListContact"]];
1258 AIChat *chat = [adium.chatController chatWithContact:contact];
1260 [adium.contentController displayEvent:AILocalizedString(@"Attempt to delete tweet failed to connect.", nil)
1267 * @brief Convert a link URL and name into an attributed link
1269 * @param label The text to display for the link.
1270 * @param destination The destination address for the link.
1271 * @param attributeName The name of the twitter link attribute for HTML processing.
1273 - (NSAttributedString *)attributedStringWithLinkLabel:(NSString *)label
1274 linkDestination:(NSString *)destination
1275 linkClass:(NSString *)className
1277 NSURL *url = [NSURL URLWithString:destination];
1278 NSDictionary *attributes = [NSDictionary dictionaryWithObjectsAndKeys:
1279 url, NSLinkAttributeName,
1280 className, AIElementClassAttributeName, nil];
1282 return [[[NSAttributedString alloc] initWithString:label attributes:attributes] autorelease];
1286 * @brief Parse an attributed string into a linkified version.
1288 - (NSAttributedString *)linkifiedAttributedStringFromString:(NSAttributedString *)inString
1290 NSAttributedString *attributedString;
1292 static NSCharacterSet *usernameCharacters = nil;
1293 static NSCharacterSet *hashCharacters = nil;
1295 if (!usernameCharacters) {
1296 usernameCharacters = [[NSCharacterSet characterSetWithCharactersInString:@"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_"] retain];
1299 if (!hashCharacters) {
1300 NSMutableCharacterSet *disallowedCharacters = [[NSCharacterSet punctuationCharacterSet] mutableCopy];
1301 [disallowedCharacters formUnionWithCharacterSet:[NSCharacterSet whitespaceCharacterSet]];
1303 hashCharacters = [[disallowedCharacters invertedSet] retain];
1305 [disallowedCharacters release];
1308 attributedString = [AITwitterURLParser linkifiedStringFromAttributedString:inString
1309 forPrefixCharacter:@"@"
1310 forLinkType:AITwitterLinkUserPage
1312 validCharacterSet:usernameCharacters];
1314 attributedString = [AITwitterURLParser linkifiedStringFromAttributedString:attributedString
1315 forPrefixCharacter:@"#"
1316 forLinkType:AITwitterLinkSearchHash
1318 validCharacterSet:hashCharacters];
1320 return attributedString;
1324 * @brief Parses a Twitter message into an attributed string
1326 - (NSAttributedString *)parseMessage:(NSString *)inMessage
1327 tweetID:(NSString *)tweetID
1328 userID:(NSString *)userID
1329 inReplyToUser:(NSString *)replyUserID
1330 inReplyToTweetID:(NSString *)replyTweetID
1332 NSAttributedString *message;
1334 message = [NSAttributedString stringWithString:[inMessage stringByUnescapingFromXMLWithEntities:nil]];
1336 message = [self linkifiedAttributedStringFromString:message];
1338 BOOL replyTweet = (replyTweetID.length);
1339 BOOL tweetLink = (tweetID.length && userID.length);
1341 if (replyTweet || tweetLink) {
1342 NSMutableAttributedString *mutableMessage = [[message mutableCopy] autorelease];
1344 NSUInteger startIndex = message.length;
1346 [mutableMessage appendString:@" (" withAttributes:nil];
1348 BOOL commaNeeded = NO;
1350 // Append a link to the tweet this is in reply to
1352 NSString *linkAddress = [self addressForLinkType:AITwitterLinkStatus
1354 statusID:replyTweetID
1357 if([inMessage hasPrefix:@"@"] &&
1358 inMessage.length >= replyUserID.length + 1 &&
1359 [replyUserID isCaseInsensitivelyEqualToString:[inMessage substringWithRange:NSMakeRange(1, replyUserID.length)]]) {
1360 // If the message has a "@" prefix, it's a proper in_reply_to_status_id if the usernames match. Set a link appropriately.
1361 [mutableMessage setAttributes:[NSDictionary dictionaryWithObjectsAndKeys:linkAddress, NSLinkAttributeName, nil]
1362 range:NSMakeRange(0, replyUserID.length + 1)];
1364 // This happens for mentions which are in_reply_to_status_id but the @target isn't the first part of the message.
1366 [mutableMessage appendAttributedString:[self attributedStringWithLinkLabel:AILocalizedString(@"IRT", "An abbreviation for 'in reply to' - placed at the beginning of the tweet tools for those which are directly in reply to another")
1367 linkDestination:linkAddress
1368 linkClass:AITwitterInReplyToClassName]];
1374 // Append a link to reply to this tweet
1376 NSString *linkAddress;
1378 if(![self.UID isCaseInsensitivelyEqualToString:userID]) {
1379 // A message from someone other than ourselves. RT and @ is permissible.
1382 [mutableMessage appendString:@", " withAttributes:nil];
1385 linkAddress = [self addressForLinkType:AITwitterLinkRetweet
1388 context:[inMessage stringByAddingPercentEscapesForAllCharacters]];
1390 [mutableMessage appendAttributedString:[self attributedStringWithLinkLabel:@"RT"
1391 linkDestination:linkAddress
1392 linkClass:AITwitterRetweetClassName]];
1397 [mutableMessage appendString:@", " withAttributes:nil];
1400 linkAddress = [self addressForLinkType:AITwitterLinkReply
1405 [mutableMessage appendAttributedString:[self attributedStringWithLinkLabel:@"@"
1406 linkDestination:linkAddress
1407 linkClass:AITwitterReplyClassName]];
1410 [mutableMessage appendString:@", " withAttributes:nil];
1413 // Our own message. Display a destroy link.
1414 linkAddress = [self addressForLinkType:AITwitterLinkDestroyStatus
1417 context:[inMessage stringByAddingPercentEscapesForAllCharacters]];
1419 [mutableMessage appendAttributedString:[self attributedStringWithLinkLabel:@"\u232B"
1420 linkDestination:linkAddress
1421 linkClass:AITwitterDeleteClassName]];
1424 [mutableMessage appendString:@", " withAttributes:nil];
1426 linkAddress = [self addressForLinkType:AITwitterLinkFavorite
1431 [mutableMessage appendAttributedString:[self attributedStringWithLinkLabel:@"\u2606"
1432 linkDestination:linkAddress
1433 linkClass:AITwitterFavoriteClassName]];
1435 [mutableMessage appendString:@", " withAttributes:nil];
1437 linkAddress = [self addressForLinkType:AITwitterLinkStatus
1442 [mutableMessage appendAttributedString:[self attributedStringWithLinkLabel:@"#"
1443 linkDestination:linkAddress
1444 linkClass:AITwitterStatusLinkClassName]];
1448 [mutableMessage appendString:@")" withAttributes:nil];
1450 [mutableMessage addAttributes:[NSDictionary dictionaryWithObjectsAndKeys:
1451 [NSNumber numberWithBool:YES], AITwitterActionLinksAttributeName,
1452 [NSNumber numberWithBool:YES], AIHiddenMessagePartAttributeName, nil]
1453 range:NSMakeRange(startIndex, mutableMessage.length - startIndex)];
1455 return mutableMessage;
1462 * @brief Parse a direct message
1464 - (NSAttributedString *)parseDirectMessage:(NSString *)inMessage
1465 withID:(NSString *)dmID
1466 fromUser:(NSString *)sourceUID
1468 NSAttributedString *message;
1470 message = [NSAttributedString stringWithString:[inMessage stringByUnescapingFromXMLWithEntities:nil]];
1472 message = [self linkifiedAttributedStringFromString:message];
1474 NSMutableAttributedString *mutableMessage = [[message mutableCopy] autorelease];
1476 NSUInteger startIndex = message.length;
1478 [mutableMessage appendString:@" (" withAttributes:nil];
1480 NSString *linkAddress = [self addressForLinkType:AITwitterLinkDestroyDM
1483 context:[inMessage stringByAddingPercentEscapesForAllCharacters]];
1485 [mutableMessage appendAttributedString:[self attributedStringWithLinkLabel:@"\u232B"
1486 linkDestination:linkAddress
1487 linkClass:AITwitterDeleteClassName]];
1489 [mutableMessage appendString:@")" withAttributes:nil];
1491 [mutableMessage addAttributes:[NSDictionary dictionaryWithObjectsAndKeys:
1492 [NSNumber numberWithBool:YES], AITwitterActionLinksAttributeName,
1493 [NSNumber numberWithBool:YES], AIHiddenMessagePartAttributeName, nil]
1494 range:NSMakeRange(startIndex, mutableMessage.length - startIndex)];
1496 return mutableMessage;
1501 * @brief Sort status updates
1503 NSInteger queuedUpdatesSort(id update1, id update2, void *context)
1505 return [[update1 objectForKey:TWITTER_STATUS_CREATED] compare:[update2 objectForKey:TWITTER_STATUS_CREATED]];
1509 * @brief Sort direct messages
1511 NSInteger queuedDMSort(id dm1, id dm2, void *context)
1513 return [[dm1 objectForKey:TWITTER_DM_CREATED] compare:[dm2 objectForKey:TWITTER_DM_CREATED]];
1517 * @brief Remove duplicate status updates.
1519 * If we're following someone who replies to us, we'll receive a status update in both the
1520 * timeline and the reply feed.
1522 * @param inArray The sorted array of Tweets
1524 - (NSArray *)arrayWithDuplicateTweetsRemoved:(NSArray *)inArray
1526 NSMutableArray *mutableArray = [inArray mutableCopy];
1528 NSDictionary *status = nil, *previousStatus = nil;
1530 // Starting at index 1, checking backwards. We'll never exceed bounds this way.
1531 for(NSUInteger index = 1; index < inArray.count; index++)
1533 status = [inArray objectAtIndex:index];
1534 previousStatus = [inArray objectAtIndex:index-1];
1536 if([[status objectForKey:TWITTER_STATUS_ID] isEqualToString:[previousStatus objectForKey:TWITTER_STATUS_ID]]) {
1537 [mutableArray removeObject:status];
1541 return [mutableArray autorelease];
1545 * @brief Display queued updates or direct messages
1547 * This could potentially be simplified since both DMs and updates have the same format.
1549 - (void)displayQueuedUpdatesForRequestType:(AITwitterRequestType)requestType
1551 if(requestType == AITwitterUpdateReplies || requestType == AITwitterUpdateFollowedTimeline) {
1552 if(!queuedUpdates.count) {
1556 AILogWithSignature(@"%@ Displaying %d updates", self, queuedUpdates.count);
1558 // Sort the queued updates (since we're intermingling pages of data from different souces)
1559 NSArray *sortedQueuedUpdates = [queuedUpdates sortedArrayUsingFunction:queuedUpdatesSort context:nil];
1561 sortedQueuedUpdates = [self arrayWithDuplicateTweetsRemoved:sortedQueuedUpdates];
1563 BOOL trackContent = [[self preferenceForKey:TWITTER_PREFERENCE_EVER_LOADED_TIMELINE group:TWITTER_PREFERENCE_GROUP_UPDATES] boolValue];
1565 AIChat *timelineChat = self.timelineChat;
1567 [[AIContactObserverManager sharedManager] delayListObjectNotifications];
1569 for (NSDictionary *status in sortedQueuedUpdates) {
1570 NSDate *date = [status objectForKey:TWITTER_STATUS_CREATED];
1571 NSString *text = [status objectForKey:TWITTER_STATUS_TEXT];
1573 NSString *contactUID = [[status objectForKey:TWITTER_STATUS_USER] objectForKey:TWITTER_STATUS_UID];
1575 id fromObject = nil;
1577 if(![self.UID isCaseInsensitivelyEqualToString:contactUID]) {
1578 AIListContact *listContact = [self contactWithUID:contactUID];
1580 // Update the user's status message
1581 [listContact setStatusMessage:[NSAttributedString stringWithString:[text stringByUnescapingFromXMLWithEntities:nil]]
1584 [self updateUserIcon:[[status objectForKey:TWITTER_STATUS_USER] objectForKey:TWITTER_INFO_ICON] forContact:listContact];
1586 [timelineChat addParticipatingListObject:listContact notify:NotifyNow];
1588 fromObject = (id)listContact;
1590 fromObject = (id)self;
1593 NSAttributedString *message = [self parseMessage:text
1594 tweetID:[status objectForKey:TWITTER_STATUS_ID]
1596 inReplyToUser:[status objectForKey:TWITTER_STATUS_REPLY_UID]
1597 inReplyToTweetID:[status objectForKey:TWITTER_STATUS_REPLY_ID]];
1599 AIContentMessage *contentMessage = [AIContentMessage messageInChat:timelineChat
1600 withSource:fromObject
1606 contentMessage.trackContent = trackContent;
1608 [adium.contentController receiveContentObject:contentMessage];
1611 [[AIContactObserverManager sharedManager] endListObjectNotificationsDelay];
1613 [queuedUpdates removeAllObjects];
1614 } else if (requestType == AITwitterUpdateDirectMessage || requestType == AITwitterDirectMessageSend) {
1615 NSMutableArray **unsortedArray = (requestType == AITwitterUpdateDirectMessage) ? &queuedDM : &queuedOutgoingDM;
1617 if (!(*unsortedArray).count) {
1621 AILogWithSignature(@"%@ Displaying %d DMs", self, queuedDM.count);
1623 NSArray *sortedQueuedDM = [*unsortedArray sortedArrayUsingFunction:queuedDMSort context:nil];
1625 for (NSDictionary *message in sortedQueuedDM) {
1626 NSDate *date = [message objectForKey:TWITTER_DM_CREATED];
1627 NSString *text = [message objectForKey:TWITTER_DM_TEXT];
1628 NSString *fromUID = [message objectForKey:TWITTER_DM_SENDER_UID];
1629 NSString *toUID = [message objectForKey:TWITTER_DM_RECIPIENT_UID];
1631 AIListObject *source = nil, *destination = nil;
1634 if([self.UID isCaseInsensitivelyEqualToString:fromUID]) {
1635 // This is a message we sent; display as coming from us.
1637 destination = [self contactWithUID:toUID];
1638 chat = [adium.chatController chatWithContact:(AIListContact *)destination];
1640 source = [self contactWithUID:fromUID];
1642 chat = [adium.chatController chatWithContact:(AIListContact *)source];
1645 if(chat && source && destination) {
1646 AIContentMessage *contentMessage = [AIContentMessage messageInChat:chat
1648 destination:destination
1650 message:[self parseDirectMessage:text
1651 withID:[message objectForKey:TWITTER_DM_ID]
1652 fromUser:chat.listObject.UID]
1655 [adium.contentController receiveContentObject:contentMessage];
1659 [*unsortedArray removeAllObjects];
1663 #pragma mark MGTwitterEngine Delegate Methods
1665 * @brief A request was successful
1667 * We only care about requests succeeding if they aren't specifically handled in another location.
1669 - (void)requestSucceeded:(NSString *)identifier
1671 // If a request succeeds and we think we're offline, call ourselves online.
1672 if ([self requestTypeForRequestID:identifier] == AITwitterDisconnect) {
1673 [self didDisconnect];
1674 } else if ([self requestTypeForRequestID:identifier] == AITwitterRemoveFollow) {
1675 AIListContact *listContact = [[self dictionaryForRequestID:identifier] objectForKey:@"ListContact"];
1677 for (NSString *groupName in listContact.remoteGroupNames) {
1678 [listContact removeRemoteGroupName:groupName];
1680 } else if ([self requestTypeForRequestID:identifier] == AITwitterDestroyStatus) {
1681 AIChat *timelineChat = self.timelineChat;
1683 [adium.contentController displayEvent:AILocalizedString(@"Your tweet has been successfully deleted.", nil)
1685 inChat:timelineChat];
1686 } else if ([self requestTypeForRequestID:identifier] == AITwitterDestroyDM) {
1687 AIListContact *contact = [[self dictionaryForRequestID:identifier] objectForKey:@"ListContact"];
1688 AIChat *chat = [adium.chatController chatWithContact:contact];
1690 [adium.contentController displayEvent:AILocalizedString(@"The direct message has been successfully deleted.", nil)
1697 * @brief A request failed
1699 * If it's a fatal error, we need to kill the session and retry. Otherwise, twitter's reliability is
1700 * pretty terrible, so let's ignore errors for the most part.
1702 - (void)requestFailed:(NSString *)identifier withError:(NSError *)error
1704 switch ([self requestTypeForRequestID:identifier]) {
1705 case AITwitterDirectMessageSend:
1706 case AITwitterSendUpdate:
1708 AIChat *chat = [[self dictionaryForRequestID:identifier] objectForKey:@"Chat"];
1711 [chat receivedError:[NSNumber numberWithInt:AIChatUnknownError]];
1713 AILogWithSignature(@"%@ Chat send error on %@", self, chat);
1718 case AITwitterDisconnect:
1719 [self didDisconnect];
1722 case AITwitterInitialUserInfo:
1723 [self setLastDisconnectionError:AILocalizedString(@"Unable to retrieve user list [fail]", "Message when a (vital) twitter request to retrieve the follow list fails")];
1724 [self didDisconnect];
1727 case AITwitterUserIconPull:
1729 AIListContact *listContact = [[self dictionaryForRequestID:identifier] objectForKey:@"ListContact"];
1731 // Image pull failed, flag ourselves as needing to try again.
1732 [listContact setValue:nil forProperty:TWITTER_PROPERTY_REQUESTED_USER_ICON notify:NotifyNever];
1736 case AITwitterUpdateFollowedTimeline:
1737 case AITwitterUpdateReplies:
1739 AIChat *timelineChat = [adium.chatController existingChatWithName:self.timelineChatName
1742 // Only print an error if the user already has the timeline open. Beyond annoying if we pop it open just to say "lol error"
1743 if (timelineChat && !timelineErrorMessagePrinted) {
1744 AIContentEvent *content = [AIContentEvent eventInChat:timelineChat
1748 message:[NSAttributedString stringWithString:[NSString stringWithFormat:AILocalizedString(@"Unable to update timeline: %@", nil),
1749 [self errorMessageForError:error]]]
1752 content.postProcessContent = NO;
1753 content.coalescingKey = @"error";
1755 [adium.contentController receiveContentObject:content];
1757 // This gets reset to NO the next a periodic update fires.
1758 timelineErrorMessagePrinted = YES;
1761 --pendingUpdateCount;
1765 case AITwitterUpdateDirectMessage:
1766 --pendingUpdateCount;
1769 case AITwitterAddFollow:
1770 if(error.code == 404) {
1771 [adium.interfaceController handleErrorMessage:AILocalizedString(@"Unable to Add Contact", nil)
1772 withDescription:[NSString stringWithFormat:AILocalizedString(@"Unable to add %@ to account %@, the user does not exist.", nil),
1773 [[self dictionaryForRequestID:identifier] objectForKey:@"UID"],
1774 self.explicitFormattedUID]];
1776 [adium.interfaceController handleErrorMessage:AILocalizedString(@"Unable to Add Contact", nil)
1777 withDescription:[NSString stringWithFormat:AILocalizedString(@"Unable to add %@ to account %@. %@",nil),
1778 [[self dictionaryForRequestID:identifier] objectForKey:@"UID"],
1779 self.explicitFormattedUID,
1780 [self errorMessageForError:error]]];
1784 case AITwitterRemoveFollow:
1785 [adium.interfaceController handleErrorMessage:AILocalizedString(@"Unable to Remove Contact", nil)
1786 withDescription:[NSString stringWithFormat:AILocalizedString(@"Unable to remove %@ on account %@. %@", nil),
1787 ((AIListContact *)[[self dictionaryForRequestID:identifier] objectForKey:@"ListContact"]).UID,
1788 self.explicitFormattedUID,
1789 [self errorMessageForError:error]]];
1792 case AITwitterValidateCredentials:
1793 if(error.code == 401) {
1795 [self setPasswordTemporarily:nil];
1796 [self setLastDisconnectionError:TWITTER_OAUTH_NOT_AUTHORIZED];
1798 [[NSNotificationCenter defaultCenter] postNotificationName:@"AIEditAccount"
1802 [self setLastDisconnectionError:TWITTER_INCORRECT_PASSWORD_MESSAGE];
1803 [self serverReportedInvalidPassword];
1806 [self didDisconnect];
1808 [self setLastDisconnectionError:AILocalizedString(@"Unable to validate credentials", nil)];
1809 [self didDisconnect];
1813 case AITwitterFavoriteYes:
1814 case AITwitterFavoriteNo:
1816 AIChat *timelineChat = self.timelineChat;
1818 if (error.code == 403) {
1819 // We've attempted to add or remove when we already have it marked as such. Try the opposite.
1820 BOOL addAsFavorite = ([self requestTypeForRequestID:identifier] == AITwitterFavoriteNo);
1821 NSString *tweetID = [[self dictionaryForRequestID:identifier] objectForKey:@"tweetID"];
1823 NSString *requestID = [twitterEngine markUpdate:tweetID
1824 asFavorite:addAsFavorite];
1827 [self setRequestType:(addAsFavorite ? AITwitterFavoriteYes : AITwitterFavoriteNo)
1828 forRequestID:requestID
1829 withDictionary:[NSDictionary dictionaryWithObjectsAndKeys:tweetID, @"tweetID", nil]];
1831 [adium.contentController displayEvent:AILocalizedString(@"Attempt to favorite tweet failed to connect.", nil)
1833 inChat:timelineChat];
1836 [adium.contentController displayEvent:[NSString stringWithFormat:AILocalizedString(@"Attempt to favorite tweet failed. %@", nil), [self errorMessageForError:error]]
1838 inChat:timelineChat];
1844 case AITwitterNotificationEnable:
1845 case AITwitterNotificationDisable:
1847 BOOL enableNotification = ([self requestTypeForRequestID:identifier] == AITwitterNotificationEnable);
1848 AIListContact *listContact = [[self dictionaryForRequestID:identifier] objectForKey:@"ListContact"];
1850 [adium.interfaceController handleErrorMessage:(enableNotification ?
1851 AILocalizedString(@"Unable to Enable Notifications", nil) :
1852 AILocalizedString(@"Unable to Disable Notifications", nil))
1853 withDescription:[NSString stringWithFormat:AILocalizedString(@"Cannot change notification setting for %@. %@", nil), listContact.UID, [self errorMessageForError:error]]];
1857 case AITwitterDestroyStatus:
1859 AIChat *timelineChat = self.timelineChat;
1861 [adium.contentController displayEvent:[NSString stringWithFormat:AILocalizedString(@"Your tweet failed to delete. %@", nil), [self errorMessageForError:error]]
1863 inChat:timelineChat];
1867 case AITwitterDestroyDM:
1869 AIListContact *contact = [[self dictionaryForRequestID:identifier] objectForKey:@"ListContact"];
1870 AIChat *chat = [adium.chatController chatWithContact:contact];
1872 [adium.contentController displayEvent:[NSString stringWithFormat:AILocalizedString(@"The direct message failed to delete. %@", nil), [self errorMessageForError:error]]
1878 case AITwitterUnknownType:
1879 case AITwitterRateLimitStatus:
1880 case AITwitterProfileSelf:
1881 case AITwitterSelfUserIconPull:
1882 case AITwitterProfileUserInfo:
1883 case AITwitterProfileStatusUpdates:
1884 // While we don't handle the errors, it's a good idea to not have a "default" just to prevent accidentally letting something
1885 // we should really handle slip through.
1890 AILogWithSignature(@"%@ Request failed (%@ - %u) - %@", self, identifier, [self requestTypeForRequestID:identifier], error);
1892 [self clearRequestTypeForRequestID:identifier];
1896 * @brief Status updates received
1898 - (void)statusesReceived:(NSArray *)statuses forRequest:(NSString *)identifier
1900 if([self requestTypeForRequestID:identifier] == AITwitterUpdateFollowedTimeline ||
1901 [self requestTypeForRequestID:identifier] == AITwitterUpdateReplies) {
1904 BOOL nextPageNecessary = NO;
1906 if([self requestTypeForRequestID:identifier] == AITwitterUpdateFollowedTimeline) {
1907 lastID = [self preferenceForKey:TWITTER_PREFERENCE_TIMELINE_LAST_ID
1908 group:TWITTER_PREFERENCE_GROUP_UPDATES];
1910 nextPageNecessary = (lastID && statuses.count >= TWITTER_UPDATE_TIMELINE_COUNT - 5);
1912 lastID = [self preferenceForKey:TWITTER_PREFERENCE_REPLIES_LAST_ID
1913 group:TWITTER_PREFERENCE_GROUP_UPDATES];
1915 nextPageNecessary = (lastID && statuses.count >= TWITTER_UPDATE_REPLIES_COUNT - 5);
1918 // Store the largest tweet ID we find; this will be our "last ID" the next time we run.
1919 NSString *largestTweet = [[self dictionaryForRequestID:identifier] objectForKey:@"LargestTweet"];
1921 // The largest ID is first, compare.
1922 if (statuses.count) {
1923 NSString *tweetID = [[statuses objectAtIndex:0] objectForKey:TWITTER_STATUS_ID];
1924 if (!largestTweet || [largestTweet compare:tweetID options:NSNumericSearch] == NSOrderedAscending) {
1925 largestTweet = tweetID;
1929 [queuedUpdates addObjectsFromArray:statuses];
1931 AILogWithSignature(@"%@ Last ID: %@ Largest Tweet: %@ Next Page Necessary: %d", self, lastID, largestTweet, nextPageNecessary);
1933 // See if we need to pull more updates.
1934 if (nextPageNecessary) {
1935 NSInteger nextPage = [[[self dictionaryForRequestID:identifier] objectForKey:@"Page"] intValue] + 1;
1936 NSString *requestID;
1938 if ([self requestTypeForRequestID:identifier] == AITwitterUpdateFollowedTimeline) {
1939 requestID = [twitterEngine getFollowedTimelineFor:nil
1941 startingAtPage:nextPage
1942 count:TWITTER_UPDATE_TIMELINE_COUNT];
1944 AILogWithSignature(@"%@ Pulling additional timeline page %d", self, nextPage);
1947 [self setRequestType:AITwitterUpdateFollowedTimeline
1948 forRequestID:requestID
1949 withDictionary:[NSDictionary dictionaryWithObjectsAndKeys:[NSNumber numberWithInt:nextPage], @"Page",
1950 largestTweet, @"LargestTweet", nil]];
1952 // Gracefully fail: remove all stored objects.
1953 AILogWithSignature(@"%@ Immediate timeline fail", self);
1954 --pendingUpdateCount;
1955 [queuedUpdates removeAllObjects];
1958 } else if ([self requestTypeForRequestID:identifier] == AITwitterUpdateReplies) {
1959 requestID = [twitterEngine getRepliesSinceID:lastID startingAtPage:nextPage];
1961 AILogWithSignature(@"%@ Pulling additional replies page %d", self, nextPage);
1964 [self setRequestType:AITwitterUpdateReplies
1965 forRequestID:requestID
1966 withDictionary:[NSDictionary dictionaryWithObjectsAndKeys:[NSNumber numberWithInt:nextPage], @"Page",
1967 largestTweet, @"LargestTweet", nil]];
1969 // Gracefully fail: remove all stored objects.
1970 AILogWithSignature(@"%@ Immediate reply fail", self);
1971 --pendingUpdateCount;
1972 [queuedUpdates removeAllObjects];
1976 if([self requestTypeForRequestID:identifier] == AITwitterUpdateFollowedTimeline) {
1977 followedTimelineCompleted = YES;
1978 futureTimelineLastID = [largestTweet retain];
1979 } else if ([self requestTypeForRequestID:identifier] == AITwitterUpdateReplies) {
1980 repliesCompleted = YES;
1981 futureRepliesLastID = [largestTweet retain];
1984 --pendingUpdateCount;
1986 AILogWithSignature(@"%@ Followed completed: %d Replies completed: %d", self, followedTimelineCompleted, repliesCompleted);
1988 if (followedTimelineCompleted && repliesCompleted) {
1989 if (queuedUpdates.count) {
1990 // Set the "last pulled" for the timeline and replies, since we've completed both.
1991 if(futureRepliesLastID) {
1992 AILogWithSignature(@"%@ futureRepliesLastID = %@", self, futureRepliesLastID);
1994 [self setPreference:futureRepliesLastID
1995 forKey:TWITTER_PREFERENCE_REPLIES_LAST_ID
1996 group:TWITTER_PREFERENCE_GROUP_UPDATES];
1998 [futureRepliesLastID release]; futureRepliesLastID = nil;
2001 if(futureTimelineLastID) {
2002 AILogWithSignature(@"%@ futureTimelineLastID = %@", self, futureTimelineLastID);
2004 [self setPreference:futureTimelineLastID
2005 forKey:TWITTER_PREFERENCE_TIMELINE_LAST_ID
2006 group:TWITTER_PREFERENCE_GROUP_UPDATES];
2008 [futureTimelineLastID release]; futureTimelineLastID = nil;
2011 [self displayQueuedUpdatesForRequestType:[self requestTypeForRequestID:identifier]];
2014 if (![self preferenceForKey:TWITTER_PREFERENCE_EVER_LOADED_TIMELINE group:TWITTER_PREFERENCE_GROUP_UPDATES]) {
2015 [self setPreference:[NSNumber numberWithBool:YES]
2016 forKey:TWITTER_PREFERENCE_EVER_LOADED_TIMELINE
2017 group:TWITTER_PREFERENCE_GROUP_UPDATES];
2021 } else if ([self requestTypeForRequestID:identifier] == AITwitterProfileStatusUpdates) {
2022 AIListContact *listContact = [[self dictionaryForRequestID:identifier] objectForKey:@"ListContact"];
2024 NSMutableArray *profileArray = [[listContact profileArray] mutableCopy];
2026 AILogWithSignature(@"%@ Updating statuses for profile, user %@", self, listContact);
2028 for (NSDictionary *update in statuses) {
2029 NSAttributedString *message = [self parseMessage:[update objectForKey:TWITTER_STATUS_TEXT]
2030 tweetID:[update objectForKey:TWITTER_STATUS_ID]
2031 userID:listContact.UID
2032 inReplyToUser:[update objectForKey:TWITTER_STATUS_REPLY_UID]
2033 inReplyToTweetID:[update objectForKey:TWITTER_STATUS_REPLY_ID]];
2035 [profileArray addObject:[NSDictionary dictionaryWithObjectsAndKeys:message, KEY_VALUE, nil]];
2038 [listContact setProfileArray:profileArray notify:NotifyNow];
2039 } else if ([self requestTypeForRequestID:identifier] == AITwitterSendUpdate) {
2040 if (updateAfterSend) {
2041 [self periodicUpdate];
2044 if (statuses.count) {
2045 [adium.contentController displayEvent:AILocalizedString(@"Tweet successfully sent.", nil)
2047 inChat:self.timelineChat];
2050 for(NSDictionary *update in statuses) {
2051 [[NSNotificationCenter defaultCenter] postNotificationName:AITwitterNotificationPostedStatus
2053 userInfo:[NSDictionary dictionaryWithObjectsAndKeys:self.timelineChat, @"AIChat", nil]];
2055 NSString *text = [[update objectForKey:TWITTER_STATUS_TEXT] stringByUnescapingFromXMLWithEntities:nil];
2057 if([[self preferenceForKey:TWITTER_PREFERENCE_UPDATE_GLOBAL group:TWITTER_PREFERENCE_GROUP_UPDATES] boolValue] &&
2058 (![text hasPrefix:@"@"] || [[self preferenceForKey:TWITTER_PREFERENCE_UPDATE_GLOBAL_REPLIES group:TWITTER_PREFERENCE_GROUP_UPDATES] boolValue])) {
2059 AIStatus *availableStatus = [AIStatus statusOfType:AIAvailableStatusType];
2061 availableStatus.statusMessage = [NSAttributedString stringWithString:text];
2062 [adium.statusController setActiveStatusState:availableStatus];
2065 } else if ([self requestTypeForRequestID:identifier] == AITwitterFavoriteYes ||
2066 [self requestTypeForRequestID:identifier] == AITwitterFavoriteNo) {
2067 AIChat *timelineChat = self.timelineChat;
2069 for (NSDictionary *status in statuses) {
2072 // Use HTML for the status message since it's just easier to localize that way.
2074 if ([self requestTypeForRequestID:identifier] == AITwitterFavoriteYes) {
2075 message = AILocalizedString(@"The <a href=\"%@\">requested tweet</a> by <a href=\"%@\">%@</a> is now a favorite.", nil);
2077 message = AILocalizedString(@"The <a href=\"%@\">requested tweet</a> by <a href=\"%@\">%@</a> is no longer a favorite.", nil);
2080 NSString *userID = [[status objectForKey:TWITTER_STATUS_USER] objectForKey:TWITTER_STATUS_UID];
2083 message = [NSString stringWithFormat:message,
2084 [self addressForLinkType:AITwitterLinkStatus
2086 statusID:[status objectForKey:TWITTER_STATUS_ID]
2088 [self addressForLinkType:AITwitterLinkUserPage
2094 NSAttributedString *attributedMessage = [[AIHTMLDecoder decoder] decodeHTML:message withDefaultAttributes:nil];
2096 AIContentEvent *content = [AIContentEvent eventInChat:timelineChat
2100 message:attributedMessage
2101 withType:@"favorite"];
2103 content.postProcessContent = NO;
2104 content.coalescingKey = @"favorite";
2106 [adium.contentController receiveContentObject:content];
2110 [self clearRequestTypeForRequestID:identifier];
2114 * @brief Direct messages received
2116 - (void)directMessagesReceived:(NSArray *)messages forRequest:(NSString *)identifier
2118 if ([self requestTypeForRequestID:identifier] == AITwitterUpdateDirectMessage) {
2119 NSString *lastID = [self preferenceForKey:TWITTER_PREFERENCE_DM_LAST_ID
2120 group:TWITTER_PREFERENCE_GROUP_UPDATES];
2122 BOOL nextPageNecessary = (lastID && messages.count >= TWITTER_UPDATE_DM_COUNT);
2124 // Store the largest tweet ID we find; this will be our "last ID" the next time we run.
2125 NSString *largestTweet = [[self dictionaryForRequestID:identifier] objectForKey:@"LargestTweet"];
2127 // The largest ID is first, compare.
2128 if (messages.count) {
2129 NSString *tweetID = [[messages objectAtIndex:0] objectForKey:TWITTER_DM_ID];
2130 if (!largestTweet || [largestTweet compare:tweetID] == NSOrderedAscending) {
2131 largestTweet = tweetID;
2135 [queuedDM addObjectsFromArray:messages];
2137 AILogWithSignature(@"%@ Last ID: %@ Largest Tweet: %@ Next Page Necessary: %d", self, lastID, largestTweet, nextPageNecessary);
2139 if(nextPageNecessary) {
2140 NSInteger nextPage = [[[self dictionaryForRequestID:identifier] objectForKey:@"Page"] intValue] + 1;
2142 NSString *requestID = [twitterEngine getDirectMessagesSinceID:lastID
2143 startingAtPage:nextPage];
2145 AILogWithSignature(@"%@ Pulling additional DM page %d", self, nextPage);
2148 [self setRequestType:AITwitterUpdateDirectMessage
2149 forRequestID:requestID
2150 withDictionary:[NSDictionary dictionaryWithObjectsAndKeys:[NSNumber numberWithInt:nextPage], @"Page",
2151 largestTweet, @"LargestTweet", nil]];
2153 // Gracefully fail: remove all stored objects.
2154 AILogWithSignature(@"%@ Immediate DM pull fail", self);
2155 --pendingUpdateCount;
2156 [queuedDM removeAllObjects];
2159 --pendingUpdateCount;
2162 AILogWithSignature(@"%@ Largest DM pulled = %@", self, largestTweet);
2164 [self setPreference:largestTweet
2165 forKey:TWITTER_PREFERENCE_DM_LAST_ID
2166 group:TWITTER_PREFERENCE_GROUP_UPDATES];
2169 // On first load, don't display any direct messages. Just ge the largest ID.
2170 if (queuedDM.count && lastID) {
2171 [self displayQueuedUpdatesForRequestType:[self requestTypeForRequestID:identifier]];
2173 [queuedDM removeAllObjects];
2176 } else if ([self requestTypeForRequestID:identifier] == AITwitterDirectMessageSend) {
2177 [queuedOutgoingDM addObjectsFromArray:messages];
2178 [self displayQueuedUpdatesForRequestType:AITwitterDirectMessageSend];
2181 [self clearRequestTypeForRequestID:identifier];
2185 * @brief User information received
2187 - (void)userInfoReceived:(NSArray *)userInfo forRequest:(NSString *)identifier
2189 if (([self requestTypeForRequestID:identifier] == AITwitterInitialUserInfo ||
2190 [self requestTypeForRequestID:identifier] == AITwitterAddFollow) &&
2191 [[self preferenceForKey:TWITTER_PREFERENCE_LOAD_CONTACTS group:TWITTER_PREFERENCE_GROUP_UPDATES] boolValue]) {
2192 [[AIContactObserverManager sharedManager] delayListObjectNotifications];
2194 // The current amount of friends per page is 100. Use >= just in case this changes.
2195 BOOL nextPageNecessary = (userInfo.count >= 100);
2197 AILogWithSignature(@"%@ User info pull, Next page necessary: %d Count: %d", self, nextPageNecessary, userInfo.count);
2199 for (NSDictionary *info in userInfo) {
2200 AIListContact *listContact = [self contactWithUID:[info objectForKey:TWITTER_INFO_UID]];
2202 // If the user isn't in a group, set them in the Twitter group.
2203 if(listContact.countOfRemoteGroupNames == 0) {
2204 [listContact addRemoteGroupName:TWITTER_REMOTE_GROUP_NAME];
2207 // Grab the Twitter display name and set it as the remote alias.
2208 if (![[listContact valueForProperty:@"Server Display Name"] isEqualToString:[info objectForKey:TWITTER_INFO_DISPLAY_NAME]]) {
2209 [listContact setServersideAlias:[info objectForKey:TWITTER_INFO_DISPLAY_NAME]
2210 silently:silentAndDelayed];
2213 // Grab the user icon and set it as their serverside icon.
2214 [self updateUserIcon:[info objectForKey:TWITTER_INFO_ICON] forContact:listContact];
2216 // Set the user as available.
2217 [listContact setStatusWithName:nil
2218 statusType:AIAvailableStatusType
2219 notify:NotifyLater];
2221 // Set the user's status message to their current twitter status text
2222 NSString *statusText = [[info objectForKey:TWITTER_INFO_STATUS] objectForKey:TWITTER_INFO_STATUS_TEXT];
2223 if (!statusText) //nil if they've never tweeted
2225 [listContact setStatusMessage:[NSAttributedString stringWithString:[statusText stringByUnescapingFromXMLWithEntities:nil]] notify:NotifyLater];
2227 // Set the user as online.
2228 [listContact setOnline:YES notify:NotifyLater silently:silentAndDelayed];
2230 [listContact notifyOfChangedPropertiesSilently:silentAndDelayed];
2233 [[AIContactObserverManager sharedManager] endListObjectNotificationsDelay];
2235 if (nextPageNecessary) {
2236 NSInteger nextPage = [[[self dictionaryForRequestID:identifier] objectForKey:@"Page"] intValue] + 1;
2237 NSString *requestID = [twitterEngine getRecentlyUpdatedFriendsFor:self.UID startingAtPage:nextPage];
2239 AILogWithSignature(@"%@ Pulling additional user info page %d", self, nextPage);
2242 [self setRequestType:AITwitterInitialUserInfo
2243 forRequestID:requestID
2244 withDictionary:[NSDictionary dictionaryWithObject:[NSNumber numberWithInt:nextPage]
2247 [self setLastDisconnectionError:AILocalizedString(@"Unable to retrieve user list [additional fail]", "Message when a (vital) twitter request to retrieve the follow list fails")];
2248 [self didDisconnect];
2251 } else if ([self valueForProperty:@"Connecting"]) {
2252 // Trigger our normal update routine.
2255 } else if ([self requestTypeForRequestID:identifier] == AITwitterProfileUserInfo) {
2256 NSDictionary *thisUserInfo = [userInfo objectAtIndex:0];
2259 AIListContact *listContact = [[self dictionaryForRequestID:identifier] objectForKey:@"ListContact"];
2261 NSArray *keyNames = [NSArray arrayWithObjects:@"name", @"location", @"description", @"url", @"friends_count", @"followers_count", @"statuses_count", nil];
2262 NSArray *readableNames = [NSArray arrayWithObjects:AILocalizedString(@"Name", nil), AILocalizedString(@"Location", nil),
2263 AILocalizedString(@"Biography", nil), AILocalizedString(@"Website", nil), AILocalizedString(@"Following", nil),
2264 AILocalizedString(@"Followers", nil), AILocalizedString(@"Updates", nil), nil];
2266 NSMutableArray *profileArray = [NSMutableArray array];
2268 for (NSUInteger index = 0; index < keyNames.count; index++) {
2269 NSString *keyName = [keyNames objectAtIndex:index];
2270 NSString *unattributedValue = [thisUserInfo objectForKey:keyName];
2272 if(![unattributedValue isEqualToString:@""]) {
2273 NSString *readableName = [readableNames objectAtIndex:index];
2274 NSAttributedString *value;
2276 if([keyName isEqualToString:@"friends_count"]) {
2277 value = [NSAttributedString attributedStringWithLinkLabel:unattributedValue
2278 linkDestination:[self addressForLinkType:AITwitterLinkFriends userID:listContact.UID statusID:nil context:nil]];
2279 } else if ([keyName isEqualToString:@"followers_count"]) {
2280 value = [NSAttributedString attributedStringWithLinkLabel:unattributedValue
2281 linkDestination:[self addressForLinkType:AITwitterLinkFollowers userID:listContact.UID statusID:nil context:nil]];
2282 } else if ([keyName isEqualToString:@"statuses_count"]) {
2283 value = [NSAttributedString attributedStringWithLinkLabel:unattributedValue
2284 linkDestination:[self addressForLinkType:AITwitterLinkUserPage userID:listContact.UID statusID:nil context:nil]];
2286 value = [NSAttributedString stringWithString:unattributedValue];
2289 [profileArray addObject:[NSDictionary dictionaryWithObjectsAndKeys:readableName, KEY_KEY, value, KEY_VALUE, nil]];
2293 AILogWithSignature(@"%@ Updating profileArray for user %@", self, listContact);
2295 [listContact setProfileArray:profileArray notify:NotifyNow];
2297 // Grab their statuses.
2298 NSString *requestID = [twitterEngine getUserTimelineFor:listContact.UID since:nil startingAtPage:0 count:TWITTER_UPDATE_USER_INFO_COUNT];
2301 [self setRequestType:AITwitterProfileStatusUpdates
2302 forRequestID:requestID
2303 withDictionary:[NSDictionary dictionaryWithObject:listContact forKey:@"ListContact"]];
2306 } else if ([self requestTypeForRequestID:identifier] == AITwitterValidateCredentials ||
2307 [self requestTypeForRequestID:identifier] == AITwitterProfileSelf) {
2308 for (NSDictionary *info in userInfo) {
2309 NSString *requestID = [twitterEngine getImageAtURL:[info objectForKey:TWITTER_INFO_ICON]];
2312 [self setRequestType:AITwitterSelfUserIconPull
2313 forRequestID:requestID
2314 withDictionary:nil];
2317 [self filterAndSetUID:[info objectForKey:TWITTER_INFO_UID]];
2319 if ([info objectForKey:@"name"]) {
2320 [self setPreference:[[NSAttributedString stringWithString:[info objectForKey:@"name"]] dataRepresentation]
2321 forKey:KEY_ACCOUNT_DISPLAY_NAME
2322 group:GROUP_ACCOUNT_STATUS];
2325 [self setValue:[info objectForKey:@"name"] forProperty:@"Profile Name" notify:NotifyLater];
2326 [self setValue:[info objectForKey:@"url"] forProperty:@"Profile URL" notify:NotifyLater];
2327 [self setValue:[info objectForKey:@"location"] forProperty:@"Profile Location" notify:NotifyLater];
2328 [self setValue:[info objectForKey:@"description"] forProperty:@"Profile Description" notify:NotifyLater];
2329 [self notifyOfChangedPropertiesSilently:NO];
2333 if([self requestTypeForRequestID:identifier] == AITwitterValidateCredentials) {
2334 // Our UID is definitely set; grab our friends.
2336 if ([[self preferenceForKey:TWITTER_PREFERENCE_LOAD_CONTACTS group:TWITTER_PREFERENCE_GROUP_UPDATES] boolValue]) {
2337 // If we load our follows as contacts, do so now.
2339 // Delay updates on initial login.
2340 [self silenceAllContactUpdatesForInterval:18.0];
2341 // Grab our user list.
2342 NSString *requestID = [twitterEngine getRecentlyUpdatedFriendsFor:self.UID startingAtPage:1];
2345 [self setRequestType:AITwitterInitialUserInfo
2346 forRequestID:requestID
2347 withDictionary:[NSDictionary dictionaryWithObject:[NSNumber numberWithInt:1] forKey:@"Page"]];
2349 [self setLastDisconnectionError:AILocalizedString(@"Unable to retrieve user list", nil)];
2350 [self didDisconnect];
2353 // If we don't load follows as contacts, we've finished connecting (fast, wasn't it?)
2357 } else if ([self requestTypeForRequestID:identifier] == AITwitterNotificationEnable ||
2358 [self requestTypeForRequestID:identifier] == AITwitterNotificationDisable) {
2359 BOOL enableNotification = ([self requestTypeForRequestID:identifier] == AITwitterNotificationEnable);
2360 AIListContact *listContact = [[self dictionaryForRequestID:identifier] objectForKey:@"ListContact"];
2362 for (NSDictionary *info in userInfo) {
2363 [adium.interfaceController handleMessage:(enableNotification ?
2364 AILocalizedString(@"Notifications Enabled", nil) :
2365 AILocalizedString(@"Notifications Disabled", nil))
2366 withDescription:[NSString stringWithFormat:(enableNotification ?
2367 AILocalizedString(@"You will now receive device notifications for %@.", nil) :
2368 AILocalizedString(@"You will no longer receive device notifications for %@.", nil)),
2370 withWindowTitle:(enableNotification ?
2371 AILocalizedString(@"Notifications Enabled", nil) :
2372 AILocalizedString(@"Notifications Disabled", nil))];
2376 [self clearRequestTypeForRequestID:identifier];
2380 * @brief Miscellaneous information received
2382 - (void)miscInfoReceived:(NSArray *)miscInfo forRequest:(NSString *)identifier
2384 if([self requestTypeForRequestID:identifier] == AITwitterRateLimitStatus) {
2385 NSDictionary *rateLimit = [miscInfo objectAtIndex:0];
2386 NSDate *resetDate = [NSDate dateWithTimeIntervalSince1970:[[rateLimit objectForKey:TWITTER_RATE_LIMIT_RESET_SECONDS] intValue]];
2388 [adium.interfaceController handleMessage:AILocalizedString(@"Current Twitter rate limit", "Message in the rate limit status window")
2389 withDescription:[NSString stringWithFormat:AILocalizedString(@"You have %d/%d more requests for %@.", "The first %d is the number of requests, the second is the total number of requests per hour. The %@ is the duration of time until the count resets."),
2390 [[rateLimit objectForKey:TWITTER_RATE_LIMIT_REMAINING] intValue],
2391 [[rateLimit objectForKey:TWITTER_RATE_LIMIT_HOURLY_LIMIT] intValue],
2392 [NSDateFormatter stringForTimeInterval:[resetDate timeIntervalSinceNow]
2396 withWindowTitle:AILocalizedString(@"Rate Limit Status", nil)];
2399 [self clearRequestTypeForRequestID:identifier];
2403 * @brief Requested image received
2405 - (void)imageReceived:(NSImage *)image forRequest:(NSString *)identifier
2407 if([self requestTypeForRequestID:identifier] == AITwitterUserIconPull) {
2408 AIListContact *listContact = [[self dictionaryForRequestID:identifier] objectForKey:@"ListContact"];
2410 AILogWithSignature(@"%@ Updated user icon for %@", self, listContact);
2412 [listContact setServersideIconData:[image TIFFRepresentation]
2413 notify:NotifyLater];
2415 [listContact setValue:nil forProperty:TWITTER_PROPERTY_REQUESTED_USER_ICON notify:NotifyNever];
2416 } else if([self requestTypeForRequestID:identifier] == AITwitterSelfUserIconPull) {
2417 AILogWithSignature(@"Updated self icon for %@", self);
2419 // Set a property so we don't re-send thie image we're just now downloading.
2420 [self setValue:[NSNumber numberWithBool:YES] forProperty:TWITTER_PROPERTY_REQUESTED_USER_ICON notify:NotifyNever];
2422 [self setPreference:[NSNumber numberWithBool:YES]
2423 forKey:KEY_USE_USER_ICON
2424 group:GROUP_ACCOUNT_STATUS];
2427 [self setPreference:[image TIFFRepresentation]
2428 forKey:KEY_USER_ICON
2429 group:GROUP_ACCOUNT_STATUS];
2432 [self clearRequestTypeForRequestID:identifier];