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