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

On an annoying note, home_timeline (despite saying "Returns the 20 most recent statuses, including retweets, posted by the authenticating user and that user's friends") does not include outgoing retweets. This will either be fixed by Twitter quickly, or not. I'm tired of Twitter's inconsistent and buggy API. So, as such, there's currently no way to remove a retweet done by yourself.
     1 /* 
     2  * Adium is the legal property of its developers, whose names are listed in the copyright file included
     3  * with this source distribution.
     4  * 
     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.
     8  * 
     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.
    12  * 
    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.
    15  */
    16 
    17 #import "AIMessageViewController.h"
    18 #import "AIMessageTabViewItem.h"
    19 #import "AITwitterAccount.h"
    20 
    21 #import "AITwitterURLHandler.h"
    22 #import "AIURLHandlerPlugin.h"
    23 #import <AIUtilities/AIURLAdditions.h>
    24 #import <AIUtilities/AIAttributedStringAdditions.h>
    25 #import <AIUtilities/AIStringAdditions.h>
    26 #import <Adium/AIMessageEntryTextView.h>
    27 #import <Adium/AIAccount.h>
    28 #import <Adium/AIChat.h>
    29 #import <Adium/AIService.h>
    30 #import <Adium/AIAccountControllerProtocol.h>
    31 #import <Adium/AIChatControllerProtocol.h>
    32 #import <Adium/AIInterfaceControllerProtocol.h>
    33 #import <Adium/AIContentControllerProtocol.h>
    34 
    35 @implementation AITwitterURLHandler
    36 
    37 /*!
    38  * @brief Install the plugin
    39  *
    40  * This plugin handles links in the format "twitterreply://account@username?status=(sid)" where the account as a provided user is optional.
    41  */
    42 - (void)installPlugin
    43 {
    44 	[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(urlRequest:) name:AIURLHandleNotification object:nil];
    45 }
    46 
    47 /*!
    48  * @brief Uninstall plugin
    49  */
    50 - (void)uninstallPlugin
    51 {
    52 	[[NSNotificationCenter defaultCenter] removeObserver:self];
    53 }
    54 
    55 /*!
    56  * @brief A reply link was licked
    57  *
    58  * Parse the reply link and set up as appropriate.
    59  */
    60 - (void)urlRequest:(NSNotification *)notification
    61 {
    62 	NSString *urlString = notification.object;
    63 	NSURL *url = [NSURL URLWithString:urlString];
    64 	
    65 	if (![url.scheme isEqualToString:@"twitterreply"]) {
    66 		return;
    67 	}
    68 	
    69 	NSString *inUser = [url host];
    70 	NSString *inAction = [url queryArgumentForKey:@"action" withDelimiter:@"&"] ?: @"reply";
    71 	NSString *inTweet = [url queryArgumentForKey:@"status" withDelimiter:@"&"];
    72 	NSString *inDM = [url queryArgumentForKey:@"dm" withDelimiter:@"&"];
    73 	NSString *inMessage = [url queryArgumentForKey:@"message" withDelimiter:@"&"];
    74 	NSString *inAccount = [url user];
    75 	
    76 	AILogWithSignature(@"Twitter Reply requested: %@", url);
    77 	
    78 	NSArray		*accountArray = adium.accountController.accounts;
    79 	
    80 	AITwitterAccount	*account = nil;
    81 	BOOL		exactMatchForInternalID = NO;
    82 	
    83 	// Look for an account with the given internalObjectID
    84 	for(AIAccount *tempAccount in accountArray) {
    85 		if (![tempAccount isKindOfClass:[AITwitterAccount class]]) {
    86 			continue;
    87 		}
    88 		
    89 		account = (AITwitterAccount *)tempAccount;
    90 		
    91 		if([tempAccount.internalObjectID isEqualToString:inAccount]) {
    92 			exactMatchForInternalID = YES;
    93 			break;
    94 		}
    95 	}
    96 
    97 	if(!account) {
    98 		// No exact match. Fail.
    99 		return;
   100 	}
   101 	
   102 	if ([inAction isEqualToString:@"retweet"]) {
   103 		[account retweetTweet:inTweet];
   104 	} else if ([inAction isEqualToString:@"reply"]) {
   105 		AIChat *timelineChat = [adium.chatController existingChatWithName:account.timelineChatName
   106 																onAccount:account];
   107 		
   108 		if (!timelineChat) {
   109 			// Timeline chat isn't already open. Open it.
   110 			timelineChat = [adium.chatController chatWithName:account.timelineChatName
   111 												   identifier:nil
   112 													onAccount:account
   113 											 chatCreationInfo:nil];
   114 		}
   115 		
   116 		if (!timelineChat.isOpen) {
   117 			[adium.interfaceController openChat:timelineChat];
   118 		}
   119 		
   120 		[adium.interfaceController setActiveChat:timelineChat];
   121 		
   122 		AIMessageEntryTextView *textView = ((AIMessageTabViewItem *)[timelineChat valueForProperty:@"MessageTabViewItem"]).messageViewController.textEntryView;
   123 
   124 		// Insert the @reply text
   125 		NSString *prefix = [NSString stringWithFormat:@"@%@ ", inUser];
   126 		
   127 		if (![textView.string hasPrefix:prefix]) {
   128 			NSMutableAttributedString *newString;
   129 			if (textView.attributedString.length > 0){
   130 				newString = [[[textView.attributedString attributedSubstringFromRange:NSMakeRange(0, 1)] mutableCopy] autorelease];
   131 				[newString replaceCharactersInRange:NSMakeRange(0, 1) withString:prefix];
   132 			}
   133 			else
   134 				newString = [[[NSMutableAttributedString alloc] initWithString:prefix attributes:[adium.contentController defaultFormattingAttributes]] autorelease];
   135 			
   136 			[newString appendAttributedString:textView.attributedString];
   137 			[textView setAttributedString:newString];
   138 			
   139 			// Shift the selected range over by the length of our prefix string
   140 			NSRange selectedRange = textView.selectedRange;
   141 			[textView setSelectedRange:NSMakeRange(selectedRange.location + prefix.length, selectedRange.length)];
   142 		}
   143 			
   144 		// Make the text view have focus
   145 		[[adium.interfaceController windowForChat:timelineChat] makeFirstResponder:textView];
   146 		
   147 		[timelineChat setValue:inTweet forProperty:@"TweetInReplyToStatusID" notify:NotifyNow];
   148 		[timelineChat setValue:inUser forProperty:@"TweetInReplyToUserID" notify:NotifyNow];
   149 		[timelineChat setValue:@"@" forProperty:@"Character Counter Prefix" notify:NotifyNow];
   150 		
   151 		AILogWithSignature(@"Flagging chat %@ to in_reply_to_status_id = %@", timelineChat, inTweet);
   152 		
   153 		[[NSNotificationCenter defaultCenter] removeObserver:self name:NSTextDidChangeNotification object:textView];
   154 		
   155 		[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(textDidChange:) name:NSTextDidChangeNotification object:textView];
   156 	} else if ([inAction isEqualToString:@"favorite"]) {
   157 		[account toggleFavoriteTweet:inTweet];
   158 	} else if ([inAction isEqualToString:@"destroy"] && exactMatchForInternalID) {
   159 		if (inTweet && inMessage) {
   160 			// Confirm if the user wants to delete this tweet.
   161 			if (NSRunAlertPanel(AILocalizedString(@"Delete Tweet?", nil),
   162 								AILocalizedString(@"Are you sure you want to delete the tweet:\n\n\"%@\"\n\nThis action cannot be undone.", nil),
   163 								AILocalizedString(@"Delete", nil), AILocalizedString(@"Cancel", nil), nil,
   164 								[inMessage stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding]) == NSAlertDefaultReturn) {
   165 				[account destroyTweet:inTweet];
   166 			}
   167 		} else if (inDM && inMessage) {
   168 			// Confirm if the user wants to delete this DM.
   169 			if (NSRunAlertPanel(AILocalizedString(@"Delete Direct Message?", nil),
   170 								AILocalizedString(@"Are you sure you want to delete the direct message:\n\n\"%@\"\n\nThis action cannot be undone.", nil),
   171 								AILocalizedString(@"Delete", nil), AILocalizedString(@"Cancel", nil), nil,
   172 								[inMessage stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding]) == NSAlertDefaultReturn) {
   173 				[account destroyDirectMessage:inDM forUser:inUser];
   174 			}			
   175 		}
   176 	}
   177 }
   178 
   179 - (void)textDidChange:(NSNotification *)notification
   180 {
   181 	AIMessageEntryTextView *textView = [notification object];
   182 
   183 	AIChat *chat = textView.chat;
   184 	
   185 	if(![chat valueForProperty:@"TweetInReplyToStatusID"] || ![chat valueForProperty:@"TweetInReplyToUserID"]) {
   186 		[[NSNotificationCenter defaultCenter] removeObserver:self name:NSTextDidChangeNotification object:textView];
   187 		return;
   188 	}
   189 	
   190 	NSString *contents = [textView string];
   191 	BOOL keepTweetValues = YES;
   192 	
   193 	NSRange usernameRange = [contents rangeOfString:[NSString stringWithFormat:@"@%@", [chat valueForProperty:@"TweetInReplyToUserID"]]];
   194 	
   195 	if (usernameRange.location == NSNotFound) {
   196 		keepTweetValues = NO;
   197 	}
   198 	
   199 	if (!keepTweetValues) {
   200 		AILogWithSignature(@"Removing in_reply_to_status_id from chat %@", chat);
   201 		
   202 		[chat setValue:nil forProperty:@"TweetInReplyToStatusID" notify:NotifyNow];
   203 		[chat setValue:nil forProperty:@"TweetInReplyToUserID" notify:NotifyNow];
   204 		[chat setValue:nil forProperty:@"Character Counter Prefix" notify:NotifyNow];
   205 		
   206 		[[NSNotificationCenter defaultCenter] removeObserver:self name:NSTextDidChangeNotification object:textView];
   207 	}
   208 }
   209 
   210 @end