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 <Adium/AIAccountControllerProtocol.h>
18 #import <Adium/AIChatControllerProtocol.h>
19 #import <Adium/AIPreferenceControllerProtocol.h>
21 #import <Adium/AIContactControllerProtocol.h>
22 #import <Adium/AIContentControllerProtocol.h>
23 #import <Adium/AIInterfaceControllerProtocol.h>
25 #import "AdiumURLHandling.h"
26 #import "XtrasInstaller.h"
27 #import "ESTextAndButtonsWindowController.h"
28 #import "AINewContactWindowController.h"
29 #import <AIUtilities/AIStringAdditions.h>
30 #import <AIUtilities/AIURLAdditions.h>
31 #import <Adium/AIAccount.h>
32 #import <Adium/AIContentMessage.h>
33 #import <Adium/AIService.h>
35 #define GROUP_URL_HANDLING @"URL Handling Group"
36 #define KEY_DONT_PROMPT_FOR_URL @"Don't Prompt for URL"
37 #define KEY_COMPLETED_FIRST_LAUNCH @"AdiumURLHandling:CompletedFirstLaunch"
39 @interface AdiumURLHandling(PRIVATE)
40 + (void)registerAsDefaultIMClient;
41 + (void)_setHelperAppForKey:(ConstStr255Param)key withInstance:(ICInstance)ICInst;
42 + (BOOL)_checkHelperAppForKey:(ConstStr255Param)key withInstance:(ICInstance)ICInst;
43 + (void)_openChatToContactWithName:(NSString *)name onService:(NSString *)serviceIdentifier withMessage:(NSString *)body;
44 + (void)_openAIMGroupChat:(NSString *)roomname onExchange:(int)exchange;
48 @implementation AdiumURLHandling
50 + (void)registerURLTypes
53 * Prompt the user to change Adium to be the protocol handler for aim:// and/or yahoo:// if we aren't already. Give them the option to agree, disagree, or disagree and never be asked again.
58 //Start Internet Config, passing it Adium's creator code
59 Err = ICStart(&ICInst, 'AdiM');
61 //Bracket multiple calls with ICBegin() for efficiency as per documentation
62 ICBegin(ICInst, icReadWritePerm);
63 BOOL alreadySet = YES;
65 //Configure the protocols we want.
66 alreadySet &= [self _checkHelperAppForKey:(kICHelper "aim") withInstance:ICInst]; //AIM, official
67 alreadySet &= [self _checkHelperAppForKey:(kICHelper "ymsgr") withInstance:ICInst]; //Yahoo!, official
68 alreadySet &= [self _checkHelperAppForKey:(kICHelper "yahoo") withInstance:ICInst]; //Yahoo!, unofficial
69 alreadySet &= [self _checkHelperAppForKey:(kICHelper "xmpp") withInstance:ICInst]; //Jabber, official
70 alreadySet &= [self _checkHelperAppForKey:(kICHelper "jabber") withInstance:ICInst]; //Jabber, unofficial
71 alreadySet &= [self _checkHelperAppForKey:(kICHelper "msn") withInstance:ICInst]; //MSN, unofficial
76 AdiumURLHandling *instance = [[AdiumURLHandling alloc] init];
77 [instance promptUser];
81 [self _setHelperAppForKey:(kICHelper "adiumxtra") withInstance:ICInst];
83 //End whatever it was that ICBegin() began
86 //We're done with Internet Config, so stop it
89 //How there could be an error stopping Internet Config, I don't know.
91 NSLog(@"Error stopping InternetConfig. Error code: %d", Err);
94 NSLog(@"Error starting InternetConfig. Error code: %d", Err);
98 + (void)registerAsDefaultIMClient
103 //Start Internet Config, passing it Adium's creator code
104 Err = ICStart(&ICInst, 'AdiM');
106 //Bracket multiple calls with ICBegin() for efficiency as per documentation
107 ICBegin(ICInst, icReadWritePerm);
109 //Configure the protocols we want.
110 [AdiumURLHandling _setHelperAppForKey:(kICHelper "aim") withInstance:ICInst]; //AIM, official
111 [AdiumURLHandling _setHelperAppForKey:(kICHelper "ymsgr") withInstance:ICInst]; //Yahoo!, official
112 [AdiumURLHandling _setHelperAppForKey:(kICHelper "yahoo") withInstance:ICInst]; //Yahoo!, unofficial
113 [AdiumURLHandling _setHelperAppForKey:(kICHelper "xmpp") withInstance:ICInst]; //Jabber, official
114 [AdiumURLHandling _setHelperAppForKey:(kICHelper "jabber") withInstance:ICInst]; //Jabber, unofficial
115 [AdiumURLHandling _setHelperAppForKey:(kICHelper "msn") withInstance:ICInst]; //MSN, unofficial
116 [AdiumURLHandling _setHelperAppForKey:(kICHelper "gtalk") withInstance:ICInst]; //Google Talk, official?
119 [AdiumURLHandling _setHelperAppForKey:(kICHelper "adiumxtra") withInstance:ICInst];
121 //End whatever it was that ICBegin() began
124 //We're done with Internet Config, so stop it
125 Err = ICStop(ICInst);
127 //How there could be an error stopping Internet Config, I don't know.
129 NSLog(@"Error stopping InternetConfig. Error code: %d", Err);
132 NSLog(@"Error starting InternetConfig. Error code: %d", Err);
136 + (void)handleURLEvent:(NSString *)eventString
138 NSURL *url = [NSURL URLWithString:eventString];
139 NSObject<AIAdium> *sharedAdium = [AIObject sharedAdiumInstance];
142 NSString *scheme, *newScheme;
145 //make sure we have the // in ://, as it simplifies later processing.
146 if (![[url resourceSpecifier] hasPrefix:@"//"]) {
147 eventString = [NSString stringWithFormat:@"%@://%@", [url scheme], [url resourceSpecifier]];
148 url = [NSURL URLWithString:eventString];
151 scheme = [url scheme];
153 //map schemes to common aliases (like jabber: for xmpp:).
154 static NSDictionary *schemeMappingDict = nil;
155 if (!schemeMappingDict) {
156 schemeMappingDict = [[NSDictionary alloc] initWithObjectsAndKeys:
161 newScheme = [schemeMappingDict objectForKey:scheme];
164 eventString = [NSString stringWithFormat:@"%@:%@", scheme, [url resourceSpecifier]];
165 url = [NSURL URLWithString:eventString];
168 static NSDictionary *schemeToServiceDict = nil;
169 if (!schemeToServiceDict) {
170 schemeToServiceDict = [[NSDictionary alloc] initWithObjectsAndKeys:
179 if ((serviceID = [schemeToServiceDict objectForKey:scheme])) {
180 NSString *host = [url host];
181 NSString *query = [url query];
182 if ([host caseInsensitiveCompare:@"goim"] == NSOrderedSame) {
183 // aim://goim?screenname=tekjew
184 NSString *name = [[[url queryArgumentForKey:@"screenname"] stringByDecodingURLEscapes] compactedString];
187 [self _openChatToContactWithName:name
189 withMessage:[[url queryArgumentForKey:@"message"] stringByDecodingURLEscapes]];
192 } else if ([host caseInsensitiveCompare:@"addbuddy"] == NSOrderedSame) {
193 // aim://addbuddy?screenname=tekjew
194 // aim://addbuddy?listofscreennames=screen+name1,screen+name+2&groupname=buddies
195 NSString *name = [[[url queryArgumentForKey:@"screenname"] stringByDecodingURLEscapes] compactedString];
196 AIService *service = [[sharedAdium accountController] firstServiceWithServiceID:serviceID];
199 [[sharedAdium contactController] requestAddContactWithUID:name
204 NSString *listOfNames = [url queryArgumentForKey:@"listofscreennames"];
205 NSArray *names = [listOfNames componentsSeparatedByString:@","];
206 NSEnumerator *enumerator;
208 enumerator = [names objectEnumerator];
209 while ((name = [enumerator nextObject])) {
210 NSString *decodedName = [[name stringByDecodingURLEscapes] compactedString];
211 [[sharedAdium contactController] requestAddContactWithUID:decodedName
217 } else if ([host caseInsensitiveCompare:@"sendim"] == NSOrderedSame) {
218 // ymsgr://sendim?tekjew
219 NSString *name = [[[url query] stringByDecodingURLEscapes] compactedString];
222 [self _openChatToContactWithName:name
227 } else if ([host caseInsensitiveCompare:@"im"] == NSOrderedSame) {
228 // ymsgr://im?to=tekjew
229 NSString *name = [[[url queryArgumentForKey:@"to"] stringByDecodingURLEscapes] compactedString];
232 [self _openChatToContactWithName:name
237 } else if ([host caseInsensitiveCompare:@"gochat"] == NSOrderedSame) {
238 // aim://gochat?RoomName=AdiumRocks
239 NSString *roomname = [[url queryArgumentForKey:@"roomname"] stringByDecodingURLEscapes];
240 NSString *exchangeString = [url queryArgumentForKey:@"exchange"];
243 if (exchangeString) {
244 exchange = [exchangeString intValue];
247 [self _openAIMGroupChat:roomname onExchange:(exchange ? exchange : 4)];
250 } else if ([url queryArgumentForKey:@"openChatToScreenName"]) {
251 // aim://openChatToScreenname?tekjew [?]
252 NSString *name = [[[url queryArgumentForKey:@"openChatToScreenname"] stringByDecodingURLEscapes] compactedString];
255 [self _openChatToContactWithName:name
260 } else if ([url queryArgumentForKey:@"openChatToScreenName"]) {
261 // gtalk:chat?jid=foo@gmail.com&from_jid=bar@gmail.com
262 NSString *name = [[[url queryArgumentForKey:@"jid"] stringByDecodingURLEscapes] compactedString];
265 [self _openChatToContactWithName:name
270 } else if ([host caseInsensitiveCompare:@"BuddyIcon"] == NSOrderedSame) {
271 //aim:BuddyIcon?src=http://www.nbc.com//Heroes/images/wallpapers/heroes-downloads-icon-single-48x48-07.gif
272 NSString *urlString = [url queryArgumentForKey:@"src"];
273 if ([urlString length]) {
274 NSURL *urlToDownload = [[NSURL alloc] initWithString:urlString];
275 NSData *imageData = (urlToDownload ? [NSData dataWithContentsOfURL:urlToDownload] : nil);
276 [urlToDownload release];
278 //Should prompt for where to apply the icon?
280 [[[NSImage alloc] initWithData:imageData] autorelease]) {
281 //If we successfully got image data, and that data makes a valid NSImage, set it as our global buddy icon
282 [[[AIObject sharedAdiumInstance] preferenceController] setPreference:imageData
284 group:GROUP_ACCOUNT_STATUS];
289 } else if ([query rangeOfString:@"message"].location == 0) {
290 //xmpp:johndoe@jabber.org?message;subject=Subject;body=Body
291 NSString *msg = [[url queryArgumentForKey:@"body"] stringByDecodingURLEscapes];
293 [self _openChatToContactWithName:[NSString stringWithFormat:@"%@@%@", [url user], [url host]]
296 } else if ([query rangeOfString:@"roster"].location == 0
297 || [query rangeOfString:@"subscribe"].location == 0) {
298 //xmpp:johndoe@jabber.org?roster;name=John%20Doe;group=Friends
299 //xmpp:johndoe@jabber.org?subscribe
301 //Group specification and name specification is currently ignored,
302 //due to limitations in the AINewContactWindowController API.
304 AIService *jabberService;
306 jabberService = [[[AIObject sharedAdiumInstance] accountController] firstServiceWithServiceID:@"Jabber"];
308 [AINewContactWindowController promptForNewContactOnWindow:nil
309 name:[NSString stringWithFormat:@"%@@%@", [url user], [url host]]
310 service:jabberService
312 } else if ([query rangeOfString:@"remove"].location == 0
313 || [query rangeOfString:@"unsubscribe"].location == 0) {
314 // xmpp:johndoe@jabber.org?remove
315 // xmpp:johndoe@jabber.org?unsubscribe
318 //Default to opening the host as a name.
319 NSString *user = [url user];
320 NSString *host = [url host];
322 if (user && [user length]) {
323 //jabber://tekjew@jabber.org
324 //msn://jdoe@hotmail.com
325 name = [NSString stringWithFormat:@"%@@%@",[url user],[url host]];
331 [self _openChatToContactWithName:[name compactedString]
336 } else if ([scheme isEqualToString:@"adiumxtra"]) {
337 //Installs an adium extra
338 // adiumxtra://www.adiumxtras.com/path/to/xtra.zip
340 [[XtrasInstaller installer] installXtraAtURL:url];
345 + (void)_setHelperAppForKey:(ConstStr255Param)key withInstance:(ICInstance)ICInst
352 TheSize = sizeof(Spec);
354 // Get the current aim helper app, to fill the Spec and TheSize variables
355 Err = ICGetPref(ICInst, key, &Junk, &Spec, &TheSize);
357 //Set the name and creator codes
358 if (Spec.fCreator != 'AdIM') {
359 Spec.name[0] = sprintf((char *) &Spec.name[1], "Adium.app");
360 Spec.fCreator = 'AdIM';
362 //Set the helper app to Adium
363 Err = ICSetPref(ICInst, key, kICAttrNoChange, &Spec, TheSize);
367 + (BOOL)_checkHelperAppForKey:(ConstStr255Param)key withInstance:(ICInstance)ICInst
374 TheSize = sizeof(Spec);
376 // Get the current aim helper app, to fill the Spec and TheSize variables
377 Err = ICGetPref(ICInst, key, &Junk, &Spec, &TheSize);
379 //Set the name and creator codes
380 return Spec.fCreator == 'AdIM';
383 + (void)_openChatToContactWithName:(NSString *)UID onService:(NSString *)serviceID withMessage:(NSString *)message
385 AIListContact *contact;
386 NSObject<AIAdium> *sharedAdium = [AIObject sharedAdiumInstance];
388 contact = [[sharedAdium contactController] preferredContactWithUID:UID
389 andServiceID:serviceID
390 forSendingContentType:CONTENT_MESSAGE_TYPE];
392 //Open the chat and set it as active
393 [[sharedAdium interfaceController] setActiveChat:[[sharedAdium chatController] openChatWithContact:contact
394 onPreferredAccount:YES]];
396 //Insert the message text as if the user had typed it after opening the chat
397 NSResponder *responder = [[[NSApplication sharedApplication] keyWindow] firstResponder];
398 if (message && [responder isKindOfClass:[NSTextView class]] && [(NSTextView *)responder isEditable]) {
399 [responder insertText:message];
407 + (void)_openAIMGroupChat:(NSString *)roomname onExchange:(int)exchange
410 NSEnumerator *enumerator;
412 //Find an AIM-compatible online account which can create group chats
413 enumerator = [[[[AIObject sharedAdiumInstance] accountController] accounts] objectEnumerator];
414 while ((account = [enumerator nextObject])) {
415 if ([account online] &&
416 [[account serviceClass] isEqualToString:@"AIM-compatible"] &&
417 [[account service] canCreateGroupChats]) {
422 if (roomname && account) {
423 [[[AIObject sharedAdiumInstance] chatController] chatWithName:roomname
426 chatCreationInfo:[NSDictionary dictionaryWithObjectsAndKeys:
428 [NSNumber numberWithInt:exchange], @"exchange",
435 - (void)URLQuestion:(NSNumber *)number info:(id)info
437 AITextAndButtonsReturnCode ret = [number intValue];
440 case AITextAndButtonsOtherReturn:
441 [[adium preferenceController] setPreference:[NSNumber numberWithBool:YES] forKey:KEY_DONT_PROMPT_FOR_URL group:GROUP_URL_HANDLING];
443 case AITextAndButtonsDefaultReturn:
444 [AdiumURLHandling registerAsDefaultIMClient];
446 case AITextAndButtonsAlternateReturn:
454 if ([[[adium preferenceController] preferenceForKey:KEY_COMPLETED_FIRST_LAUNCH group:GROUP_URL_HANDLING] boolValue]) {
455 if(![[adium preferenceController] preferenceForKey:KEY_DONT_PROMPT_FOR_URL group:GROUP_URL_HANDLING])
456 [[adium interfaceController] displayQuestion:AILocalizedString(@"Change default messaging client?", nil)
457 withDescription:AILocalizedString(@"Adium is not your default Instant Messaging client. The default client is loaded when you click messaging URLs in web pages. Would you like Adium to become the default?", nil)
459 defaultButton:AILocalizedString(@"Yes", nil)
460 alternateButton:AILocalizedString(@"No", nil)
461 otherButton:AILocalizedString(@"Never", nil)
463 selector:@selector(URLQuestion:info:)
466 //On the first launch, simply register. If the user uses another IM client which takes control of the protocols again, we'll prompt for what to do.
467 [AdiumURLHandling registerAsDefaultIMClient];
469 [[adium preferenceController] setPreference:[NSNumber numberWithBool:YES]
470 forKey:KEY_COMPLETED_FIRST_LAUNCH
471 group:GROUP_URL_HANDLING];