Plugins/Purple Service/ESPurpleJabberAccount.m
author Zachary West <zacw@adium.im>
Fri Oct 16 10:54:20 2009 -0400 (2009-10-16)
changeset 2615 bcbb03bada1a
parent 2386 b76c84a95828
child 3042 c6ef8efaf14f
permissions -rw-r--r--
Add a "BOSH Server" option for Jabber accounts.
     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 "ESPurpleJabberAccount.h"
    18 #import <AdiumLibpurple/SLPurpleCocoaAdapter.h>
    19 #import <Adium/AIAccountControllerProtocol.h>
    20 #import <Adium/AIInterfaceControllerProtocol.h>
    21 #import <Adium/AIStatusControllerProtocol.h>
    22 #import <Adium/AIContactControllerProtocol.h>
    23 #import <Adium/AIChat.h>
    24 #import <Adium/AIHTMLDecoder.h>
    25 #import <Adium/AIListContact.h>
    26 #import <Adium/AIStatus.h>
    27 #import <Adium/AIStatusIcons.h>
    28 #import <Adium/ESFileTransfer.h>
    29 #import <Adium/AIService.h>
    30 #import <AIUtilities/AIApplicationAdditions.h>
    31 #import <AIUtilities/AIAttributedStringAdditions.h>
    32 #import <AIUtilities/AIDictionaryAdditions.h>
    33 #import <AIUtilities/AIStringAdditions.h>
    34 #import <libpurple/presence.h>
    35 #import <libpurple/si.h>
    36 #import <SystemConfiguration/SystemConfiguration.h>
    37 #import "AMXMLConsoleController.h"
    38 #import "AMPurpleJabberServiceDiscoveryBrowsing.h"
    39 #import "ESPurpleJabberAccountViewController.h"
    40 #import "AMPurpleJabberAdHocServer.h"
    41 #import "AMPurpleJabberAdHocPing.h"
    42 
    43 #define DEFAULT_JABBER_HOST @"@jabber.org"
    44 
    45 @interface ESPurpleJabberAccount ()
    46 - (BOOL)enableXMLConsole;
    47 @end
    48 
    49 @implementation ESPurpleJabberAccount
    50 
    51 /*!
    52  * @brief The UID will be changed. The account has a chance to perform modifications
    53  *
    54  * Upgrade old Jabber accounts stored with the host in a separate key to have the right UID, in the form
    55  * name@server.org
    56  *
    57  * Append @jabber.org to a proposed UID which has no domain name and does not need to be updated.
    58  *
    59  * @param proposedUID The proposed, pre-filtered UID (filtered means it has no characters invalid for this servce)
    60  * @result The UID to use; the default implementation just returns proposedUID.
    61  */
    62 - (NSString *)accountWillSetUID:(NSString *)proposedUID
    63 {
    64 	proposedUID = [proposedUID lowercaseString];
    65 	NSString	*correctUID;
    66 	
    67 	if ((proposedUID && ([proposedUID length] > 0)) && 
    68 	   ([proposedUID rangeOfString:@"@"].location == NSNotFound)) {
    69 		
    70 		NSString	*host;
    71 		//Upgrade code: grab a previously specified Jabber host
    72 		if ((host = [self preferenceForKey:@"Jabber:Host" group:GROUP_ACCOUNT_STATUS])) {
    73 			//Determine our new, full UID
    74 			correctUID = [NSString stringWithFormat:@"%@@%@",proposedUID, host];
    75 
    76 			//Clear the preference and then set the UID so we don't perform this upgrade again
    77 			[self setPreference:nil forKey:@"Jabber:Host" group:GROUP_ACCOUNT_STATUS];
    78 			[self setPreference:correctUID forKey:@"FormattedUID" group:GROUP_ACCOUNT_STATUS];
    79 
    80 		} else {
    81 			//Append [self serverSuffix] (e.g. @jabber.org) to a Jabber account with no server
    82 			correctUID = [proposedUID stringByAppendingString:[self serverSuffix]];
    83 		}
    84 	} else {
    85 		correctUID = proposedUID;
    86 	}
    87 
    88 	return correctUID;
    89 }
    90 
    91 - (const char*)protocolPlugin
    92 {
    93    return "prpl-jabber";
    94 }
    95 
    96 - (void)dealloc
    97 {
    98 	[xmlConsoleController close];
    99 	[xmlConsoleController release];
   100 
   101 	[super dealloc];
   102 }
   103 
   104 - (NSSet *)supportedPropertyKeys
   105 {
   106 	static NSMutableSet *supportedPropertyKeys = nil;
   107 	
   108 	if (!supportedPropertyKeys) {
   109 		supportedPropertyKeys = [[NSMutableSet alloc] initWithObjects:
   110 			@"AvailableMessage",
   111 			@"Invisible",
   112 			nil];
   113 		[supportedPropertyKeys unionSet:[super supportedPropertyKeys]];
   114 	}
   115 	
   116 	return supportedPropertyKeys;
   117 }
   118 
   119 - (void)configurePurpleAccount
   120 {
   121 	[super configurePurpleAccount];
   122 	
   123 	NSString	*connectServer;
   124 	BOOL		forceOldSSL, allowPlaintext, requireTLS;
   125 
   126 	purple_account_set_username(account, self.purpleAccountName);
   127 
   128 	//'Connect via' server (nil by default)
   129 	connectServer = [self preferenceForKey:KEY_JABBER_CONNECT_SERVER group:GROUP_ACCOUNT_STATUS];
   130 	//XXX - As of libpurple 2.0.0, 'localhost' doesn't work properly by 127.0.0.1 does. Hack!
   131 	if (connectServer && [connectServer isEqualToString:@"localhost"])
   132 		connectServer = @"127.0.0.1";
   133 	
   134 	purple_account_set_string(account, "connect_server", (connectServer ?
   135 														[connectServer UTF8String] :
   136 														""));
   137 	
   138 	NSString *boshServer = [self preferenceForKey:KEY_JABBER_BOSH_SERVER group:GROUP_ACCOUNT_STATUS];
   139 	
   140 	purple_account_set_string(account, "bosh_url", (boshServer ? [boshServer UTF8String] : ""));
   141 	
   142 	// FT proxies
   143 	NSString *ftProxies = [self preferenceForKey:KEY_JABBER_FT_PROXIES group:GROUP_ACCOUNT_STATUS];
   144 	if (ftProxies.length) {
   145 		purple_account_set_string(account, "ft_proxies", [ftProxies UTF8String]);
   146 	}
   147 	
   148 	//Force old SSL usage? (off by default)
   149 	forceOldSSL = [[self preferenceForKey:KEY_JABBER_FORCE_OLD_SSL group:GROUP_ACCOUNT_STATUS] boolValue];
   150 	purple_account_set_bool(account, "old_ssl", forceOldSSL);
   151 
   152 	//Require SSL or TLS? (off by default)
   153 	requireTLS = [[self preferenceForKey:KEY_JABBER_REQUIRE_TLS group:GROUP_ACCOUNT_STATUS] boolValue];
   154 	purple_account_set_bool(account, "require_tls", requireTLS);
   155 
   156 	//Allow plaintext authorization over an unencrypted connection? Purple will prompt if this is NO and is needed.
   157 	allowPlaintext = [[self preferenceForKey:KEY_JABBER_ALLOW_PLAINTEXT group:GROUP_ACCOUNT_STATUS] boolValue];
   158 	purple_account_set_bool(account, "auth_plain_in_clear", allowPlaintext);
   159 	
   160 	/* Mac OS X 10.4's cyrus-sasl's PLAIN mech gives us problems.  Is it a bug in the installed library, a bug in its compilation, or a bug
   161 	 * in our linkage against it? I don't know. The result is that the username gets included twice before the base64 encoding is performed.
   162 	 *
   163 	 * Furthermore, on any version, using the cyrus-sasl PLAIN mech prevents us from following Google Talk best practices for handling of domain names.
   164 	 * This is because we can't add to the <auth> response's attributes:
   165 	 *		xmlns:ga='http://www.google.com/talk/protocol/auth' ga:client-uses-full-bind-result='true'
   166 	 * as per http://code.google.com/apis/talk/jep_extensions/jid_domain_change.html and therefore we won't automatically resolve changing an
   167 	 * "@gmail.com" to "@googlemail.com" or some other domain name.
   168 	 *
   169 	 * We therefore use the PLAIN implementation in libpurple itself. Libpurple's own DIGEST-MD5 is always used for compatibility with old OpenFire
   170 	 * servers.
   171 	 *
   172 	 * This preference and the changes for it are added via the "libpurple_jabber_avoid_sasl_option_hack.diff" patch we apply during the build process.
   173 	 */
   174 	purple_prefs_set_bool("/plugins/prpl/jabber/avoid_sasl_for_plain_auth", YES);
   175 }
   176 
   177 - (NSString *)serverSuffix
   178 {
   179 	NSString *host = [self preferenceForKey:KEY_JABBER_CONNECT_SERVER group:GROUP_ACCOUNT_STATUS];
   180 	
   181 	return (host ? host : DEFAULT_JABBER_HOST);
   182 }
   183 
   184 /*!	@brief	Obtain the resource name for this Jabber account.
   185  *
   186  *	This could be extended in the future to perform keyword substitution (e.g. s/%computerName%/CSCopyMachineName()/).
   187  *
   188  *	@return	The resource name for the account.
   189  */
   190 - (NSString *)resourceName
   191 {
   192     NSString *resource = [self preferenceForKey:KEY_JABBER_RESOURCE group:GROUP_ACCOUNT_STATUS];
   193     
   194     if(resource == nil || [resource length] == 0)
   195         resource = [(NSString*)SCDynamicStoreCopyLocalHostName(NULL) autorelease];
   196     
   197 	return resource;
   198 }
   199 
   200 - (const char *)purpleAccountName
   201 {
   202 	NSString	*userNameWithHost = nil, *completeUserName = nil;
   203 	BOOL		serverAppendedToUID;
   204 	
   205 	/*
   206 	 * Purple stores the username in the format username@server/resource.  We need to pass it a username in this format
   207 	 *
   208 	 * The user should put the username in username@server format, which is common for Jabber. If the user does
   209 	 * not specify the server, use jabber.org.
   210 	 */
   211 	
   212 	serverAppendedToUID = ([UID rangeOfString:@"@"].location != NSNotFound);
   213 	
   214 	if (serverAppendedToUID) {
   215 		userNameWithHost = UID;
   216 	} else {
   217 		userNameWithHost = [UID stringByAppendingString:[self serverSuffix]];
   218 	}
   219 
   220 	completeUserName = [NSString stringWithFormat:@"%@/%@" ,userNameWithHost, [self resourceName]];
   221 
   222 	return [completeUserName UTF8String];
   223 }
   224 
   225 /*!
   226  * @brief Connect Host
   227  *
   228  * Convenience method for retrieving the connect host for this account
   229  *
   230  * Rather than having a separate server field, Jabber uses the servername after the user name.
   231  * username@server.org
   232  *
   233  * The connect server, stored in KEY_JABBER_CONNECT_SERVER, overrides this to provide the connect host. It will
   234  * not be set in most cases.
   235  */
   236 - (NSString *)host
   237 {
   238 	NSString	*host;
   239 	
   240 	if (!(host = [self preferenceForKey:KEY_JABBER_CONNECT_SERVER group:GROUP_ACCOUNT_STATUS])) {
   241 		NSUInteger location = [UID rangeOfString:@"@"].location;
   242 
   243 		if ((location != NSNotFound) && (location + 1 < [UID length])) {
   244 			host = [UID substringFromIndex:(location + 1)];
   245 
   246 		} else {
   247 			host = [self serverSuffix];
   248 		}
   249 	}
   250 	
   251 	return host;
   252 }
   253 
   254 /*!
   255  * @brief Should set aliases serverside?
   256  *
   257  * Jabber supports serverside aliases.
   258  */
   259 - (BOOL)shouldSetAliasesServerside
   260 {
   261 	return YES;
   262 }
   263 
   264 - (AIListContact *)contactWithUID:(NSString *)sourceUID
   265 {
   266 	AIListContact	*contact;
   267 	
   268 	contact = [adium.contactController existingContactWithService:service
   269 															account:self
   270 																UID:sourceUID];
   271 	if (!contact) {		
   272 		contact = [adium.contactController contactWithService:[self _serviceForUID:sourceUID]
   273 														account:self
   274 															UID:sourceUID];
   275 	}
   276 	
   277 	return contact;
   278 }
   279 
   280 - (AIService *)_serviceForUID:(NSString *)contactUID
   281 {
   282 	AIService	*contactService;
   283 	NSString	*contactServiceID = nil;
   284 
   285 	if ([contactUID hasSuffix:@"@gmail.com"] ||
   286 		[contactUID hasSuffix:@"@googlemail.com"]) {
   287 		contactServiceID = @"libpurple-jabber-gtalk";
   288 
   289 	} else if([contactUID hasSuffix:@"@livejournal.com"]){
   290 		contactServiceID = @"libpurple-jabber-livejournal";
   291 		
   292 	} else {
   293 		contactServiceID = @"libpurple-Jabber";
   294 	}
   295 
   296 	contactService = [adium.accountController serviceWithUniqueID:contactServiceID];
   297 	
   298 	return contactService;
   299 }
   300 
   301 - (id)authorizationRequestWithDict:(NSDictionary*)dict {
   302 	switch ([[self preferenceForKey:KEY_JABBER_SUBSCRIPTION_BEHAVIOR group:GROUP_ACCOUNT_STATUS] intValue]) {
   303 		case 2: // always accept + add
   304 			// add
   305 			{
   306 				NSString *groupname = [self preferenceForKey:KEY_JABBER_SUBSCRIPTION_GROUP group:GROUP_ACCOUNT_STATUS];
   307 				if ([groupname length] > 0) {
   308 					AIListContact *contact = [adium.contactController contactWithService:self.service account:self UID:[dict objectForKey:@"Remote Name"]];
   309 					AIListGroup *group = [adium.contactController groupWithUID:groupname];
   310 					[contact.account addContact:contact toGroup:group];
   311 				}
   312 			}
   313 			// fallthrough
   314 		case 1: // always accept
   315 			[[self purpleAdapter] doAuthRequestCbValue:[[[dict objectForKey:@"authorizeCB"] retain] autorelease] withUserDataValue:[[[dict objectForKey:@"userData"] retain] autorelease]];
   316 			break;
   317 		case 3: // always deny
   318 			[[self purpleAdapter] doAuthRequestCbValue:[[[dict objectForKey:@"denyCB"] retain] autorelease] withUserDataValue:[[[dict objectForKey:@"userData"] retain] autorelease]];
   319 			break;
   320 		default: // ask (should be 0)
   321 			return [super authorizationRequestWithDict:dict];
   322 	}
   323 
   324 	return NULL;
   325 }
   326 
   327 - (void)purpleAccountRegistered:(BOOL)success
   328 {
   329 	if(success && [self.service accountViewController]) {
   330 		const char *usernamestr = purple_account_get_username(account);
   331 		NSString *username;
   332 		if (usernamestr) {
   333 			NSString *userWithResource = [NSString stringWithUTF8String:usernamestr];
   334 			NSRange slashrange = [userWithResource rangeOfString:@"/"];
   335 			if(slashrange.location != NSNotFound)
   336 				username = [userWithResource substringToIndex:slashrange.location];
   337 			else
   338 				username = userWithResource;
   339 		} else
   340 			username = (id)[NSNull null];
   341 
   342 		NSString *pw = (purple_account_get_password(account) ? [NSString stringWithUTF8String:purple_account_get_password(account)] : [NSNull null]);
   343 		
   344 		[[NSNotificationCenter defaultCenter] postNotificationName:AIAccountUsernameAndPasswordRegisteredNotification
   345 												  object:self
   346 												userInfo:[NSDictionary dictionaryWithObjectsAndKeys:
   347 													username, @"username",
   348 													pw, @"password",
   349 													nil]];
   350 	}
   351 }
   352 
   353 #pragma mark Status
   354 
   355 - (NSString *)encodedAttributedString:(NSAttributedString *)inAttributedString forListObject:(AIListObject *)inListObject
   356 {
   357 	static AIHTMLDecoder *jabberHtmlEncoder = nil;
   358 	if (!jabberHtmlEncoder) {
   359 		jabberHtmlEncoder = [[AIHTMLDecoder alloc] init];
   360 		[jabberHtmlEncoder setIncludesHeaders:NO];
   361 		[jabberHtmlEncoder setIncludesFontTags:YES];
   362 		[jabberHtmlEncoder setClosesFontTags:YES];
   363 		[jabberHtmlEncoder setIncludesStyleTags:YES];
   364 		[jabberHtmlEncoder setIncludesColorTags:YES];
   365 		[jabberHtmlEncoder setEncodesNonASCII:NO];
   366 		[jabberHtmlEncoder setPreservesAllSpaces:NO];
   367 		[jabberHtmlEncoder setUsesAttachmentTextEquivalents:YES];
   368 	}
   369 	
   370 	return [jabberHtmlEncoder encodeHTML:inAttributedString imagesPath:nil];
   371 }
   372 
   373 - (NSString *)_UIDForAddingObject:(AIListContact *)object
   374 {
   375 	NSString	*objectUID = object.UID;
   376 	NSString	*properUID;
   377 	
   378 	if ([objectUID rangeOfString:@"@"].location != NSNotFound) {
   379 		properUID = objectUID;
   380 	} else {
   381 		properUID = [NSString stringWithFormat:@"%@@%@",objectUID,self.host];
   382 	}
   383 	
   384 	return [properUID lowercaseString];
   385 }
   386 
   387 - (NSString *)unknownGroupName {
   388     return (AILocalizedString(@"Roster","Roster - the Jabber default group"));
   389 }
   390 
   391 - (NSString *)connectionStringForStep:(int)step
   392 {
   393 	switch (step) {
   394 		case 0:
   395 			return AILocalizedString(@"Connecting",nil);
   396 			break;
   397 		case 1:
   398 			return AILocalizedString(@"Initializing Stream",nil);
   399 			break;
   400 		case 2:
   401 			return AILocalizedString(@"Reading data",nil);
   402 			break;			
   403 		case 3:
   404 			return AILocalizedString(@"Authenticating",nil);
   405 			break;
   406 		case 5:
   407 			return AILocalizedString(@"Initializing Stream",nil);
   408 			break;
   409 		case 6:
   410 			return AILocalizedString(@"Authenticating",nil);
   411 			break;
   412 	}
   413 	return nil;
   414 }
   415 
   416 - (AIReconnectDelayType)shouldAttemptReconnectAfterDisconnectionError:(NSString **)disconnectionError
   417 {
   418 	AIReconnectDelayType shouldAttemptReconnect = [super shouldAttemptReconnectAfterDisconnectionError:disconnectionError];
   419 
   420 	if (([self lastDisconnectionReason] == PURPLE_CONNECTION_ERROR_CERT_OTHER_ERROR) &&
   421 		([self shouldVerifyCertificates])) {
   422 		shouldAttemptReconnect = AIReconnectNever;
   423 	} else if (!finishedConnectProcess && ![password length] &&
   424 			   (disconnectionError &&
   425 			   ([*disconnectionError isEqualToString:[NSString stringWithUTF8String:_("Read Error")]] ||
   426 				[*disconnectionError isEqualToString:[NSString stringWithUTF8String:_("Service Unavailable")]] ||
   427 				[*disconnectionError isEqualToString:[NSString stringWithUTF8String:_("Forbidden")]]))) {
   428 		//No password specified + above error while we're connecting = behavior of various broken servers. Prompt for a password.
   429 		[self serverReportedInvalidPassword];
   430 		shouldAttemptReconnect = AIReconnectImmediately;
   431 	}
   432  
   433 	return shouldAttemptReconnect;
   434 }
   435 
   436 - (void)disconnectFromDroppedNetworkConnection
   437 {
   438 	/* Before we disconnect from a dropped network connection, set gc->disconnect_timeout to a non-0 value.
   439 	 * This will let the prpl know that we are disconnecting with no backing ssl connection and that therefore
   440 	 * the ssl connection is has should not be messaged in the process of disconnecting.
   441 	 */
   442 	PurpleConnection *gc = purple_account_get_connection(account);
   443 	if (PURPLE_CONNECTION_IS_VALID(gc) &&
   444 		!gc->disconnect_timeout) {
   445 		gc->disconnect_timeout = -1;
   446 		AILog(@"%@: Disconnecting from a dropped network connection", self);
   447 	}
   448 
   449 	[super disconnectFromDroppedNetworkConnection];
   450 }
   451 
   452 #pragma mark File transfer
   453 - (BOOL)canSendFolders
   454 {
   455 	return NO;
   456 }
   457 
   458 - (void)beginSendOfFileTransfer:(ESFileTransfer *)fileTransfer
   459 {
   460 	[super _beginSendOfFileTransfer:fileTransfer];
   461 }
   462 
   463 - (void)acceptFileTransferRequest:(ESFileTransfer *)fileTransfer
   464 {
   465     [super acceptFileTransferRequest:fileTransfer];    
   466 }
   467 
   468 - (void)rejectFileReceiveRequest:(ESFileTransfer *)fileTransfer
   469 {
   470     [super rejectFileReceiveRequest:fileTransfer];    
   471 }
   472 
   473 - (void)cancelFileTransfer:(ESFileTransfer *)fileTransfer
   474 {
   475 	[super cancelFileTransfer:fileTransfer];
   476 }
   477 
   478 #pragma mark Status Messages
   479 - (NSString *)statusNameForPurpleBuddy:(PurpleBuddy *)buddy
   480 {
   481 	NSString		*statusName = nil;
   482 	PurplePresence	*presence = purple_buddy_get_presence(buddy);
   483 	PurpleStatus		*status = purple_presence_get_active_status(presence);
   484 	const char		*purpleStatusID = purple_status_get_id(status);
   485 	
   486 	if (!purpleStatusID) return nil;
   487 
   488 	if (!strcmp(purpleStatusID, jabber_buddy_state_get_status_id(JABBER_BUDDY_STATE_CHAT))) {
   489 		statusName = STATUS_NAME_FREE_FOR_CHAT;
   490 		
   491 	} else if (!strcmp(purpleStatusID, jabber_buddy_state_get_status_id(JABBER_BUDDY_STATE_XA))) {
   492 		statusName = STATUS_NAME_EXTENDED_AWAY;
   493 		
   494 	} else if (!strcmp(purpleStatusID, jabber_buddy_state_get_status_id(JABBER_BUDDY_STATE_DND))) {
   495 		statusName = STATUS_NAME_DND;
   496 		
   497 	}
   498 	
   499 	return statusName;
   500 }
   501 
   502 #pragma mark Menu items
   503 - (NSString *)titleForContactMenuLabel:(const char *)label forContact:(AIListContact *)inContact
   504 {
   505 	if (strcmp(label, "Un-hide From") == 0) {
   506 		return [NSString stringWithFormat:AILocalizedString(@"Un-hide From %@",nil),inContact.formattedUID];
   507 
   508 	} else if (strcmp(label, "Temporarily Hide From") == 0) {
   509 		return [NSString stringWithFormat:AILocalizedString(@"Temporarily Hide From %@",nil),inContact.formattedUID];
   510 
   511 	} else if (strcmp(label, "Unsubscribe") == 0) {
   512 		return [NSString stringWithFormat:AILocalizedString(@"Unsubscribe %@",nil),inContact.formattedUID];
   513 
   514 	} else if (strcmp(label, "(Re-)Request authorization") == 0) {
   515 		return [NSString stringWithFormat:AILocalizedString(@"Re-request Authorization from %@",nil),inContact.formattedUID];
   516 
   517 	} else if (strcmp(label,  "Cancel Presence Notification") == 0) {
   518 		return [NSString stringWithFormat:AILocalizedString(@"Cancel Presence Notification to %@",nil),inContact.formattedUID];	
   519 		
   520 	} else if (strcmp(label,  _("Ping")) == 0) {
   521 		return [NSString stringWithFormat:AILocalizedString(@"Ping %@",nil),inContact.formattedUID];	
   522 		
   523 	}
   524 	
   525 	return [super titleForContactMenuLabel:label forContact:inContact];
   526 }
   527 
   528 - (NSString *)titleForAccountActionMenuLabel:(const char *)label
   529 {	
   530 	if (strcmp(label, "Set User Info...") == 0) {
   531 		return [AILocalizedString(@"Set User Info", nil) stringByAppendingEllipsis];
   532 		
   533 	} else 	if (strcmp(label, "Search for Users...") == 0) {
   534 		return [AILocalizedString(@"Search for Users", nil) stringByAppendingEllipsis];
   535 		
   536 	} else 	if (strcmp(label, "Set Mood...") == 0) {
   537 		return [AILocalizedString(@"Set Mood", nil) stringByAppendingEllipsis];
   538 		
   539 	} else 	if (strcmp(label, "Set Nickname...") == 0) {
   540 		return [AILocalizedString(@"Set Nickname", nil) stringByAppendingEllipsis];
   541 	} 
   542 	
   543 	return [super titleForAccountActionMenuLabel:label];
   544 }
   545 
   546 #pragma mark Multiuser chat
   547 /*!
   548  * @brief A chat will be joined
   549  *
   550  * This gives the account a chance to update any information in the chat's creation dictionary if desired.
   551  *
   552  * @result The final chat creation dictionary to use.
   553  */
   554 - (NSDictionary *)willJoinChatUsingDictionary:(NSDictionary *)chatCreationDictionary
   555 {
   556 	if (![[chatCreationDictionary objectForKey:@"handle"] length]) {
   557 		NSMutableDictionary *dict = [[chatCreationDictionary mutableCopy] autorelease];
   558 		
   559 		[dict setObject:self.displayName
   560 				 forKey:@"handle"];
   561 
   562 		chatCreationDictionary = dict;
   563 	}
   564 	
   565 	return chatCreationDictionary;
   566 }
   567 
   568 - (BOOL)chatCreationDictionary:(NSDictionary *)chatCreationDict isEqualToDictionary:(NSDictionary *)baseDict
   569 {
   570 	/* If the chat isn't keeping track of a handle, it's because we added it in
   571 	 * willJoinChatUsingDictionary: above. Remove it from baseDict so the comparison is accurate.
   572 	 */
   573 	if (![chatCreationDict objectForKey:@"handle"])
   574 		baseDict = [baseDict dictionaryWithDifferenceWithSetOfKeys:[NSSet setWithObject:@"handle"]];
   575 
   576 	return [chatCreationDict isEqualToDictionary:baseDict];
   577 }
   578 
   579 /*!
   580  * @brief Do group chats support topics?
   581  */
   582 - (BOOL)groupChatsSupportTopic
   583 {
   584 	return YES;
   585 }
   586 
   587 /*!
   588  * @brief Return the "nickname" part of a MUC JID
   589  *
   590  * @param contact The AIListContact
   591  * @param chat the AIChat
   592  * @return The nickname for a chat participant
   593  */
   594 - (NSString *)fallbackAliasForContact:(AIListContact *)contact inChat:(AIChat *)chat
   595 {
   596 	if (contact.isStranger && [contact.UID.lowercaseString rangeOfString:chat.name.lowercaseString].location != NSNotFound) {
   597 		return [contact.UID substringFromIndex:[contact.UID rangeOfString:@"/"].location + 1];		
   598 	} else {
   599 		return [super fallbackAliasForContact:contact inChat:chat];
   600 	}
   601 }
   602 
   603 #pragma mark Status
   604 /*!
   605  * @brief Return the purple status type to be used for a status
   606  *
   607  * Most subclasses should override this method; these generic values may be appropriate for others.
   608  *
   609  * Active services provided nonlocalized status names.  An AIStatus is passed to this method along with a pointer
   610  * to the status message.  This method should handle any status whose statusNname this service set as well as any statusName
   611  * defined in  AIStatusController.h (which will correspond to the services handled by Adium by default).
   612  * It should also handle a status name not specified in either of these places with a sane default, most likely by loooking at
   613  * statusState.statusType for a general idea of the status's type.
   614  *
   615  * @param statusState The status for which to find the purple status ID
   616  * @param arguments Prpl-specific arguments which will be passed with the state. Message is handled automatically.
   617  *
   618  * @result The purple status ID
   619  */
   620 - (const char *)purpleStatusIDForStatus:(AIStatus *)statusState
   621 							arguments:(NSMutableDictionary *)arguments
   622 {
   623 	const char		*statusID = NULL;
   624 	NSString		*statusName = statusState.statusName;
   625 	NSString		*statusMessageString = [statusState statusMessageString];
   626 	NSNumber		*priority = nil;
   627 	
   628 	if (!statusMessageString) statusMessageString = @"";
   629 
   630 	switch (statusState.statusType) {
   631 		case AIAvailableStatusType:
   632 		{
   633 			if (([statusName isEqualToString:STATUS_NAME_FREE_FOR_CHAT]) ||
   634 			   ([statusMessageString caseInsensitiveCompare:[adium.statusController localizedDescriptionForCoreStatusName:STATUS_NAME_FREE_FOR_CHAT]] == NSOrderedSame))
   635 				statusID = jabber_buddy_state_get_status_id(JABBER_BUDDY_STATE_CHAT);
   636 			priority = [self preferenceForKey:KEY_JABBER_PRIORITY_AVAILABLE group:GROUP_ACCOUNT_STATUS];
   637 			break;
   638 		}
   639 			
   640 		case AIAwayStatusType:
   641 		{
   642 			if (([statusName isEqualToString:STATUS_NAME_DND]) ||
   643 				([statusMessageString caseInsensitiveCompare:[adium.statusController localizedDescriptionForCoreStatusName:STATUS_NAME_DND]] == NSOrderedSame) ||
   644 				[statusName isEqualToString:STATUS_NAME_BUSY]) {
   645 				//Note that Jabber doesn't actually support a 'busy' status; if we have it set because some other service supports it, treat it as DND
   646 				statusID = jabber_buddy_state_get_status_id(JABBER_BUDDY_STATE_DND);
   647 
   648 			} else if (([statusName isEqualToString:STATUS_NAME_EXTENDED_AWAY]) ||
   649 					 ([statusMessageString caseInsensitiveCompare:[adium.statusController localizedDescriptionForCoreStatusName:STATUS_NAME_EXTENDED_AWAY]] == NSOrderedSame))
   650 				statusID = jabber_buddy_state_get_status_id(JABBER_BUDDY_STATE_XA);
   651 			priority = [self preferenceForKey:KEY_JABBER_PRIORITY_AWAY group:GROUP_ACCOUNT_STATUS];
   652 			break;
   653 		}
   654 			
   655 		case AIInvisibleStatusType:
   656 			AILog(@"Warning: Invisibility is not yet supported in libpurple 2.0.0 jabber");
   657 			priority = [self preferenceForKey:KEY_JABBER_PRIORITY_AWAY group:GROUP_ACCOUNT_STATUS];
   658 			statusID = jabber_buddy_state_get_status_id(JABBER_BUDDY_STATE_AWAY);
   659 //			statusID = "Invisible";
   660 			break;
   661 			
   662 		case AIOfflineStatusType:
   663 			break;
   664 	}
   665 
   666 	//Set our priority, which is actually set along with the status...Default is 0.
   667 	[arguments setObject:(priority ? priority : [NSNumber numberWithInt:0])
   668 				  forKey:@"priority"];
   669 	
   670 	//We could potentially set buzz on a per-status basis. We have no UI for this, however.
   671 	[arguments setObject:[NSNumber numberWithBool:YES] forKey:@"buzz"];
   672 
   673 	//If we didn't get a purple status ID, request one from super
   674 	if (statusID == NULL) statusID = [super purpleStatusIDForStatus:statusState arguments:arguments];
   675 	
   676 	return statusID;
   677 }
   678 
   679 #pragma mark Gateway Tracking
   680 
   681 - (void)addContact:(AIListContact *)theContact toGroupName:(NSString *)groupName contactName:(NSString *)contactName {
   682 	NSRange atsign = [theContact.UID rangeOfString:@"@"];
   683 	if(atsign.location != NSNotFound)
   684 		[super addContact:theContact toGroupName:groupName contactName:contactName];
   685 	else {
   686 		NSDictionary *gatewaydict;
   687 		// avoid duplicates!
   688 		for (gatewaydict in gateways) {
   689 			if([[[gatewaydict objectForKey:@"contact"] UID] isEqualToString:theContact.UID])
   690 				break;
   691 		}
   692 		
   693 		if (gatewaydict)
   694 			[gateways removeObjectIdenticalTo:gatewaydict];
   695 
   696 		[gateways addObject:[NSDictionary dictionaryWithObjectsAndKeys:
   697 							 theContact, @"contact",
   698 							 groupName, @"remoteGroup",
   699 							 nil]];
   700 	}
   701 }
   702 
   703 - (void)removeContact:(AIListContact *)theContact {
   704 	NSRange atsign = [theContact.UID rangeOfString:@"@"];
   705 	if(atsign.location != NSNotFound)
   706 		[super removeContact:theContact];
   707 	else {
   708 		for (NSDictionary *gatewaydict in [[gateways copy] autorelease]) {
   709 			if([[[gatewaydict objectForKey:@"contact"] UID] isEqualToString:theContact.UID]) {
   710 				[[self purpleAdapter] removeUID:theContact.UID onAccount:self fromGroup:[gatewaydict objectForKey:@"remoteGroup"]];
   711 				
   712 				[gateways removeObjectIdenticalTo:gatewaydict];
   713 				break;
   714 			}
   715 		}
   716 	}
   717 }
   718 
   719 #pragma mark XML Console, Tooltip, AdHoc Server Integration and Gateway Integration
   720 
   721 /*!
   722 * @brief Returns whether or not this account is connected via an encrypted connection.
   723  */
   724 - (BOOL)encrypted
   725 {
   726 	return (self.online && [self secureConnection]);
   727 }
   728 
   729 - (void)didConnect {
   730 	[gateways release];
   731 	gateways = [[NSMutableArray alloc] init];
   732 
   733 	[adhocServer release];
   734 	adhocServer = [[AMPurpleJabberAdHocServer alloc] initWithAccount:self];
   735 	[adhocServer addCommand:@"ping" delegate:(id<AMPurpleJabberAdHocServerDelegate>)[AMPurpleJabberAdHocPing class] name:@"Ping"];
   736 	
   737     [super didConnect];
   738 	
   739 	if ([self enableXMLConsole]) {
   740 		if (!xmlConsoleController) xmlConsoleController = [[AMXMLConsoleController alloc] init];
   741 		[xmlConsoleController setPurpleConnection:purple_account_get_connection(account)];
   742 	}
   743 
   744 	discoveryBrowserController = [[AMPurpleJabberServiceDiscoveryBrowsing alloc] initWithAccount:self
   745 																				purpleConnection:purple_account_get_connection(account)];
   746 }
   747 
   748 - (void)didDisconnect {
   749 	[xmlConsoleController setPurpleConnection:NULL];
   750 	
   751 	[discoveryBrowserController release]; discoveryBrowserController = nil;
   752 	[adhocServer release]; adhocServer = nil;
   753 
   754 	[super didDisconnect];
   755 
   756 	[gateways release]; gateways = nil;
   757 }
   758 
   759 - (IBAction)showXMLConsole:(id)sender {
   760     if(xmlConsoleController)
   761         [xmlConsoleController showWindow:sender];
   762     else
   763         NSBeep();
   764 }
   765 
   766 - (BOOL)enableXMLConsole
   767 {
   768 	BOOL enableConsole;
   769 #ifdef DEBUG_BUILD
   770 	//Always enable the XML console for debug builds
   771 	enableConsole = YES;
   772 #else
   773 	//For non-debug builds, only enable it if the preference is set
   774 	enableConsole = [[NSUserDefaults standardUserDefaults] boolForKey:@"AMXMPPShowAdvanced"];
   775 #endif
   776 	
   777 	return enableConsole;
   778 }
   779 
   780 - (IBAction)showDiscoveryBrowser:(id)sender {
   781 	[discoveryBrowserController browse:sender];
   782 }
   783 
   784 - (PurpleSslConnection *)secureConnection {
   785 	// this is really ugly
   786 	PurpleConnection *gc = purple_account_get_connection(self.purpleAccount);
   787 
   788 	return ((gc && gc->proto_data) ? ((JabberStream*)purple_account_get_connection(self.purpleAccount)->proto_data)->gsc : NULL);
   789 }
   790 
   791 - (void)setShouldVerifyCertificates:(BOOL)yesOrNo {
   792 	[self setPreference:[NSNumber numberWithBool:yesOrNo] forKey:KEY_JABBER_VERIFY_CERTS group:GROUP_ACCOUNT_STATUS];
   793 }
   794 
   795 - (BOOL)shouldVerifyCertificates {
   796 	return [[self preferenceForKey:KEY_JABBER_VERIFY_CERTS group:GROUP_ACCOUNT_STATUS] boolValue];
   797 }
   798 
   799 - (NSArray *)accountActionMenuItems {
   800 	AILog(@"Getting accountActionMenuItems for %@",self);
   801 	NSMutableArray *menu = [[NSMutableArray alloc] init];
   802 	
   803 	if([gateways count] > 0) {
   804 		NSDictionary *gatewaydict;
   805 		for(gatewaydict in gateways) {
   806 			AIListContact *gateway = [gatewaydict objectForKey:@"contact"];
   807 			NSMenuItem *mitem = [[NSMenuItem alloc] initWithTitle:gateway.UID action:@selector(registerGateway:) keyEquivalent:@""];
   808 			NSMenu *submenu = [[NSMenu alloc] initWithTitle:gateway.UID];
   809 			
   810 			NSArray *menuitemarray = [self menuItemsForContact:gateway];
   811 			for (NSMenuItem *m2item in menuitemarray)
   812 				[submenu addItem:m2item];
   813 			
   814 			if([submenu numberOfItems] > 0)
   815 				[submenu addItem:[NSMenuItem separatorItem]];
   816 
   817 			NSMenuItem *removeItem = [[NSMenuItem alloc] initWithTitle:AILocalizedString(@"Remove gateway","gateway menu item") action:@selector(removeGateway:) keyEquivalent:@""];
   818 			[removeItem setTarget:self];
   819 			[removeItem setRepresentedObject:gateway];
   820 			[submenu addItem:removeItem];
   821 			[removeItem release];
   822 			
   823 			[mitem setSubmenu:submenu];
   824 			[submenu release];
   825 			[mitem setRepresentedObject:gateway];
   826 			[mitem setImage:[AIStatusIcons statusIconForListObject:gateway
   827 															  type:AIStatusIconTab
   828 														 direction:AIIconNormal]];
   829 			[mitem setTarget:self];
   830 			[menu addObject:mitem];
   831 			[mitem release];
   832 		}
   833         [menu addObject:[NSMenuItem separatorItem]];
   834 	}
   835 	
   836     NSArray *supermenu = [super accountActionMenuItems];
   837     if(supermenu) {
   838 		[menu addObjectsFromArray:supermenu];
   839         [menu addObject:[NSMenuItem separatorItem]];
   840 	}
   841 
   842 	if ([self enableXMLConsole]) {
   843 		NSMenuItem *xmlConsoleMenuItem = [[NSMenuItem alloc] initWithTitle:AILocalizedString(@"XML Console",nil)
   844 																	action:@selector(showXMLConsole:) 
   845 															 keyEquivalent:@""];
   846 		[xmlConsoleMenuItem setTarget:self];
   847 		[menu addObject:xmlConsoleMenuItem];
   848 		[xmlConsoleMenuItem release];
   849 	}
   850 
   851 	NSMenuItem *discoveryBrowserMenuItem = [[NSMenuItem alloc] initWithTitle:AILocalizedString(@"Discovery Browser",nil)
   852 																	  action:@selector(showDiscoveryBrowser:) 
   853 															   keyEquivalent:@""];
   854     [discoveryBrowserMenuItem setTarget:self];
   855     [menu addObject:discoveryBrowserMenuItem];
   856     [discoveryBrowserMenuItem release];
   857 	
   858     return [menu autorelease];
   859 }
   860 
   861 - (void)registerGateway:(NSMenuItem*)mitem {
   862 	if(mitem && [mitem representedObject])
   863 		jabber_register_gateway((JabberStream*)purple_account_get_connection(self.purpleAccount)->proto_data, [[[mitem representedObject] UID] UTF8String]);
   864 	else
   865 		NSBeep();
   866 }
   867 
   868 - (void)removeGateway:(NSMenuItem*)mitem {
   869 	AIListContact *gateway = [mitem representedObject];
   870 	if(![gateway isKindOfClass:[AIListContact class]])
   871 		return;
   872 	// since this is a potentially dangerous operation, get a confirmation from the user first
   873 	if([[NSAlert alertWithMessageText:AILocalizedString(@"Really remove gateway?",nil)
   874 					 defaultButton:AILocalizedString(@"Remove","alert default button")
   875 				   alternateButton:AILocalizedString(@"Cancel",nil)
   876 					   otherButton:nil
   877 					 informativeTextWithFormat:AILocalizedString(@"This operation would remove the gateway %@ itself and all contacts belonging to the gateway on your contact list. It cannot be undone.",nil), gateway.UID] runModal] == NSAlertDefaultReturn) {
   878 		// first, locate all contacts on the roster that belong to this gateway
   879 		NSString *jid = gateway.UID;
   880 		NSString *pattern = [@"@" stringByAppendingString:jid];
   881 		NSMutableArray *gatewayContacts = [[NSMutableArray alloc] init];
   882 		NSMutableSet *removeGroups = [NSMutableSet set];
   883 		for (AIListContact *contact in self.contacts) {
   884 			if([contact.UID hasSuffix:pattern]) {
   885 				[gatewayContacts addObject:contact];
   886 				[removeGroups unionSet:contact.groups];
   887 			}
   888 		}
   889 		// now, remove them from the roster
   890 		[self removeContacts:gatewayContacts
   891 				  fromGroups:removeGroups.allObjects];
   892 		
   893 		[gatewayContacts release];
   894 		
   895 		// finally, remove the gateway itself
   896 		[self removeContact:gateway];
   897 	}
   898 }
   899 
   900 - (AMPurpleJabberAdHocServer*)adhocServer {
   901 	return adhocServer;
   902 }
   903 
   904 @end