Add a "BOSH Server" option for Jabber accounts.
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 "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"
43 #define DEFAULT_JABBER_HOST @"@jabber.org"
45 @interface ESPurpleJabberAccount ()
46 - (BOOL)enableXMLConsole;
49 @implementation ESPurpleJabberAccount
52 * @brief The UID will be changed. The account has a chance to perform modifications
54 * Upgrade old Jabber accounts stored with the host in a separate key to have the right UID, in the form
57 * Append @jabber.org to a proposed UID which has no domain name and does not need to be updated.
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.
62 - (NSString *)accountWillSetUID:(NSString *)proposedUID
64 proposedUID = [proposedUID lowercaseString];
67 if ((proposedUID && ([proposedUID length] > 0)) &&
68 ([proposedUID rangeOfString:@"@"].location == NSNotFound)) {
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];
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];
81 //Append [self serverSuffix] (e.g. @jabber.org) to a Jabber account with no server
82 correctUID = [proposedUID stringByAppendingString:[self serverSuffix]];
85 correctUID = proposedUID;
91 - (const char*)protocolPlugin
98 [xmlConsoleController close];
99 [xmlConsoleController release];
104 - (NSSet *)supportedPropertyKeys
106 static NSMutableSet *supportedPropertyKeys = nil;
108 if (!supportedPropertyKeys) {
109 supportedPropertyKeys = [[NSMutableSet alloc] initWithObjects:
113 [supportedPropertyKeys unionSet:[super supportedPropertyKeys]];
116 return supportedPropertyKeys;
119 - (void)configurePurpleAccount
121 [super configurePurpleAccount];
123 NSString *connectServer;
124 BOOL forceOldSSL, allowPlaintext, requireTLS;
126 purple_account_set_username(account, self.purpleAccountName);
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";
134 purple_account_set_string(account, "connect_server", (connectServer ?
135 [connectServer UTF8String] :
138 NSString *boshServer = [self preferenceForKey:KEY_JABBER_BOSH_SERVER group:GROUP_ACCOUNT_STATUS];
140 purple_account_set_string(account, "bosh_url", (boshServer ? [boshServer UTF8String] : ""));
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]);
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);
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);
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);
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.
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.
169 * We therefore use the PLAIN implementation in libpurple itself. Libpurple's own DIGEST-MD5 is always used for compatibility with old OpenFire
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.
174 purple_prefs_set_bool("/plugins/prpl/jabber/avoid_sasl_for_plain_auth", YES);
177 - (NSString *)serverSuffix
179 NSString *host = [self preferenceForKey:KEY_JABBER_CONNECT_SERVER group:GROUP_ACCOUNT_STATUS];
181 return (host ? host : DEFAULT_JABBER_HOST);
184 /*! @brief Obtain the resource name for this Jabber account.
186 * This could be extended in the future to perform keyword substitution (e.g. s/%computerName%/CSCopyMachineName()/).
188 * @return The resource name for the account.
190 - (NSString *)resourceName
192 NSString *resource = [self preferenceForKey:KEY_JABBER_RESOURCE group:GROUP_ACCOUNT_STATUS];
194 if(resource == nil || [resource length] == 0)
195 resource = [(NSString*)SCDynamicStoreCopyLocalHostName(NULL) autorelease];
200 - (const char *)purpleAccountName
202 NSString *userNameWithHost = nil, *completeUserName = nil;
203 BOOL serverAppendedToUID;
206 * Purple stores the username in the format username@server/resource. We need to pass it a username in this format
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.
212 serverAppendedToUID = ([UID rangeOfString:@"@"].location != NSNotFound);
214 if (serverAppendedToUID) {
215 userNameWithHost = UID;
217 userNameWithHost = [UID stringByAppendingString:[self serverSuffix]];
220 completeUserName = [NSString stringWithFormat:@"%@/%@" ,userNameWithHost, [self resourceName]];
222 return [completeUserName UTF8String];
226 * @brief Connect Host
228 * Convenience method for retrieving the connect host for this account
230 * Rather than having a separate server field, Jabber uses the servername after the user name.
231 * username@server.org
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.
240 if (!(host = [self preferenceForKey:KEY_JABBER_CONNECT_SERVER group:GROUP_ACCOUNT_STATUS])) {
241 NSUInteger location = [UID rangeOfString:@"@"].location;
243 if ((location != NSNotFound) && (location + 1 < [UID length])) {
244 host = [UID substringFromIndex:(location + 1)];
247 host = [self serverSuffix];
255 * @brief Should set aliases serverside?
257 * Jabber supports serverside aliases.
259 - (BOOL)shouldSetAliasesServerside
264 - (AIListContact *)contactWithUID:(NSString *)sourceUID
266 AIListContact *contact;
268 contact = [adium.contactController existingContactWithService:service
272 contact = [adium.contactController contactWithService:[self _serviceForUID:sourceUID]
280 - (AIService *)_serviceForUID:(NSString *)contactUID
282 AIService *contactService;
283 NSString *contactServiceID = nil;
285 if ([contactUID hasSuffix:@"@gmail.com"] ||
286 [contactUID hasSuffix:@"@googlemail.com"]) {
287 contactServiceID = @"libpurple-jabber-gtalk";
289 } else if([contactUID hasSuffix:@"@livejournal.com"]){
290 contactServiceID = @"libpurple-jabber-livejournal";
293 contactServiceID = @"libpurple-Jabber";
296 contactService = [adium.accountController serviceWithUniqueID:contactServiceID];
298 return contactService;
301 - (id)authorizationRequestWithDict:(NSDictionary*)dict {
302 switch ([[self preferenceForKey:KEY_JABBER_SUBSCRIPTION_BEHAVIOR group:GROUP_ACCOUNT_STATUS] intValue]) {
303 case 2: // always accept + add
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];
314 case 1: // always accept
315 [[self purpleAdapter] doAuthRequestCbValue:[[[dict objectForKey:@"authorizeCB"] retain] autorelease] withUserDataValue:[[[dict objectForKey:@"userData"] retain] autorelease]];
317 case 3: // always deny
318 [[self purpleAdapter] doAuthRequestCbValue:[[[dict objectForKey:@"denyCB"] retain] autorelease] withUserDataValue:[[[dict objectForKey:@"userData"] retain] autorelease]];
320 default: // ask (should be 0)
321 return [super authorizationRequestWithDict:dict];
327 - (void)purpleAccountRegistered:(BOOL)success
329 if(success && [self.service accountViewController]) {
330 const char *usernamestr = purple_account_get_username(account);
333 NSString *userWithResource = [NSString stringWithUTF8String:usernamestr];
334 NSRange slashrange = [userWithResource rangeOfString:@"/"];
335 if(slashrange.location != NSNotFound)
336 username = [userWithResource substringToIndex:slashrange.location];
338 username = userWithResource;
340 username = (id)[NSNull null];
342 NSString *pw = (purple_account_get_password(account) ? [NSString stringWithUTF8String:purple_account_get_password(account)] : [NSNull null]);
344 [[NSNotificationCenter defaultCenter] postNotificationName:AIAccountUsernameAndPasswordRegisteredNotification
346 userInfo:[NSDictionary dictionaryWithObjectsAndKeys:
347 username, @"username",
355 - (NSString *)encodedAttributedString:(NSAttributedString *)inAttributedString forListObject:(AIListObject *)inListObject
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];
370 return [jabberHtmlEncoder encodeHTML:inAttributedString imagesPath:nil];
373 - (NSString *)_UIDForAddingObject:(AIListContact *)object
375 NSString *objectUID = object.UID;
378 if ([objectUID rangeOfString:@"@"].location != NSNotFound) {
379 properUID = objectUID;
381 properUID = [NSString stringWithFormat:@"%@@%@",objectUID,self.host];
384 return [properUID lowercaseString];
387 - (NSString *)unknownGroupName {
388 return (AILocalizedString(@"Roster","Roster - the Jabber default group"));
391 - (NSString *)connectionStringForStep:(int)step
395 return AILocalizedString(@"Connecting",nil);
398 return AILocalizedString(@"Initializing Stream",nil);
401 return AILocalizedString(@"Reading data",nil);
404 return AILocalizedString(@"Authenticating",nil);
407 return AILocalizedString(@"Initializing Stream",nil);
410 return AILocalizedString(@"Authenticating",nil);
416 - (AIReconnectDelayType)shouldAttemptReconnectAfterDisconnectionError:(NSString **)disconnectionError
418 AIReconnectDelayType shouldAttemptReconnect = [super shouldAttemptReconnectAfterDisconnectionError:disconnectionError];
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;
433 return shouldAttemptReconnect;
436 - (void)disconnectFromDroppedNetworkConnection
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.
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);
449 [super disconnectFromDroppedNetworkConnection];
452 #pragma mark File transfer
453 - (BOOL)canSendFolders
458 - (void)beginSendOfFileTransfer:(ESFileTransfer *)fileTransfer
460 [super _beginSendOfFileTransfer:fileTransfer];
463 - (void)acceptFileTransferRequest:(ESFileTransfer *)fileTransfer
465 [super acceptFileTransferRequest:fileTransfer];
468 - (void)rejectFileReceiveRequest:(ESFileTransfer *)fileTransfer
470 [super rejectFileReceiveRequest:fileTransfer];
473 - (void)cancelFileTransfer:(ESFileTransfer *)fileTransfer
475 [super cancelFileTransfer:fileTransfer];
478 #pragma mark Status Messages
479 - (NSString *)statusNameForPurpleBuddy:(PurpleBuddy *)buddy
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);
486 if (!purpleStatusID) return nil;
488 if (!strcmp(purpleStatusID, jabber_buddy_state_get_status_id(JABBER_BUDDY_STATE_CHAT))) {
489 statusName = STATUS_NAME_FREE_FOR_CHAT;
491 } else if (!strcmp(purpleStatusID, jabber_buddy_state_get_status_id(JABBER_BUDDY_STATE_XA))) {
492 statusName = STATUS_NAME_EXTENDED_AWAY;
494 } else if (!strcmp(purpleStatusID, jabber_buddy_state_get_status_id(JABBER_BUDDY_STATE_DND))) {
495 statusName = STATUS_NAME_DND;
502 #pragma mark Menu items
503 - (NSString *)titleForContactMenuLabel:(const char *)label forContact:(AIListContact *)inContact
505 if (strcmp(label, "Un-hide From") == 0) {
506 return [NSString stringWithFormat:AILocalizedString(@"Un-hide From %@",nil),inContact.formattedUID];
508 } else if (strcmp(label, "Temporarily Hide From") == 0) {
509 return [NSString stringWithFormat:AILocalizedString(@"Temporarily Hide From %@",nil),inContact.formattedUID];
511 } else if (strcmp(label, "Unsubscribe") == 0) {
512 return [NSString stringWithFormat:AILocalizedString(@"Unsubscribe %@",nil),inContact.formattedUID];
514 } else if (strcmp(label, "(Re-)Request authorization") == 0) {
515 return [NSString stringWithFormat:AILocalizedString(@"Re-request Authorization from %@",nil),inContact.formattedUID];
517 } else if (strcmp(label, "Cancel Presence Notification") == 0) {
518 return [NSString stringWithFormat:AILocalizedString(@"Cancel Presence Notification to %@",nil),inContact.formattedUID];
520 } else if (strcmp(label, _("Ping")) == 0) {
521 return [NSString stringWithFormat:AILocalizedString(@"Ping %@",nil),inContact.formattedUID];
525 return [super titleForContactMenuLabel:label forContact:inContact];
528 - (NSString *)titleForAccountActionMenuLabel:(const char *)label
530 if (strcmp(label, "Set User Info...") == 0) {
531 return [AILocalizedString(@"Set User Info", nil) stringByAppendingEllipsis];
533 } else if (strcmp(label, "Search for Users...") == 0) {
534 return [AILocalizedString(@"Search for Users", nil) stringByAppendingEllipsis];
536 } else if (strcmp(label, "Set Mood...") == 0) {
537 return [AILocalizedString(@"Set Mood", nil) stringByAppendingEllipsis];
539 } else if (strcmp(label, "Set Nickname...") == 0) {
540 return [AILocalizedString(@"Set Nickname", nil) stringByAppendingEllipsis];
543 return [super titleForAccountActionMenuLabel:label];
546 #pragma mark Multiuser chat
548 * @brief A chat will be joined
550 * This gives the account a chance to update any information in the chat's creation dictionary if desired.
552 * @result The final chat creation dictionary to use.
554 - (NSDictionary *)willJoinChatUsingDictionary:(NSDictionary *)chatCreationDictionary
556 if (![[chatCreationDictionary objectForKey:@"handle"] length]) {
557 NSMutableDictionary *dict = [[chatCreationDictionary mutableCopy] autorelease];
559 [dict setObject:self.displayName
562 chatCreationDictionary = dict;
565 return chatCreationDictionary;
568 - (BOOL)chatCreationDictionary:(NSDictionary *)chatCreationDict isEqualToDictionary:(NSDictionary *)baseDict
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.
573 if (![chatCreationDict objectForKey:@"handle"])
574 baseDict = [baseDict dictionaryWithDifferenceWithSetOfKeys:[NSSet setWithObject:@"handle"]];
576 return [chatCreationDict isEqualToDictionary:baseDict];
580 * @brief Do group chats support topics?
582 - (BOOL)groupChatsSupportTopic
588 * @brief Return the "nickname" part of a MUC JID
590 * @param contact The AIListContact
591 * @param chat the AIChat
592 * @return The nickname for a chat participant
594 - (NSString *)fallbackAliasForContact:(AIListContact *)contact inChat:(AIChat *)chat
596 if (contact.isStranger && [contact.UID.lowercaseString rangeOfString:chat.name.lowercaseString].location != NSNotFound) {
597 return [contact.UID substringFromIndex:[contact.UID rangeOfString:@"/"].location + 1];
599 return [super fallbackAliasForContact:contact inChat:chat];
605 * @brief Return the purple status type to be used for a status
607 * Most subclasses should override this method; these generic values may be appropriate for others.
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.
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.
618 * @result The purple status ID
620 - (const char *)purpleStatusIDForStatus:(AIStatus *)statusState
621 arguments:(NSMutableDictionary *)arguments
623 const char *statusID = NULL;
624 NSString *statusName = statusState.statusName;
625 NSString *statusMessageString = [statusState statusMessageString];
626 NSNumber *priority = nil;
628 if (!statusMessageString) statusMessageString = @"";
630 switch (statusState.statusType) {
631 case AIAvailableStatusType:
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];
640 case AIAwayStatusType:
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);
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];
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";
662 case AIOfflineStatusType:
666 //Set our priority, which is actually set along with the status...Default is 0.
667 [arguments setObject:(priority ? priority : [NSNumber numberWithInt:0])
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"];
673 //If we didn't get a purple status ID, request one from super
674 if (statusID == NULL) statusID = [super purpleStatusIDForStatus:statusState arguments:arguments];
679 #pragma mark Gateway Tracking
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];
686 NSDictionary *gatewaydict;
688 for (gatewaydict in gateways) {
689 if([[[gatewaydict objectForKey:@"contact"] UID] isEqualToString:theContact.UID])
694 [gateways removeObjectIdenticalTo:gatewaydict];
696 [gateways addObject:[NSDictionary dictionaryWithObjectsAndKeys:
697 theContact, @"contact",
698 groupName, @"remoteGroup",
703 - (void)removeContact:(AIListContact *)theContact {
704 NSRange atsign = [theContact.UID rangeOfString:@"@"];
705 if(atsign.location != NSNotFound)
706 [super removeContact:theContact];
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"]];
712 [gateways removeObjectIdenticalTo:gatewaydict];
719 #pragma mark XML Console, Tooltip, AdHoc Server Integration and Gateway Integration
722 * @brief Returns whether or not this account is connected via an encrypted connection.
726 return (self.online && [self secureConnection]);
731 gateways = [[NSMutableArray alloc] init];
733 [adhocServer release];
734 adhocServer = [[AMPurpleJabberAdHocServer alloc] initWithAccount:self];
735 [adhocServer addCommand:@"ping" delegate:(id<AMPurpleJabberAdHocServerDelegate>)[AMPurpleJabberAdHocPing class] name:@"Ping"];
739 if ([self enableXMLConsole]) {
740 if (!xmlConsoleController) xmlConsoleController = [[AMXMLConsoleController alloc] init];
741 [xmlConsoleController setPurpleConnection:purple_account_get_connection(account)];
744 discoveryBrowserController = [[AMPurpleJabberServiceDiscoveryBrowsing alloc] initWithAccount:self
745 purpleConnection:purple_account_get_connection(account)];
748 - (void)didDisconnect {
749 [xmlConsoleController setPurpleConnection:NULL];
751 [discoveryBrowserController release]; discoveryBrowserController = nil;
752 [adhocServer release]; adhocServer = nil;
754 [super didDisconnect];
756 [gateways release]; gateways = nil;
759 - (IBAction)showXMLConsole:(id)sender {
760 if(xmlConsoleController)
761 [xmlConsoleController showWindow:sender];
766 - (BOOL)enableXMLConsole
770 //Always enable the XML console for debug builds
773 //For non-debug builds, only enable it if the preference is set
774 enableConsole = [[NSUserDefaults standardUserDefaults] boolForKey:@"AMXMPPShowAdvanced"];
777 return enableConsole;
780 - (IBAction)showDiscoveryBrowser:(id)sender {
781 [discoveryBrowserController browse:sender];
784 - (PurpleSslConnection *)secureConnection {
785 // this is really ugly
786 PurpleConnection *gc = purple_account_get_connection(self.purpleAccount);
788 return ((gc && gc->proto_data) ? ((JabberStream*)purple_account_get_connection(self.purpleAccount)->proto_data)->gsc : NULL);
791 - (void)setShouldVerifyCertificates:(BOOL)yesOrNo {
792 [self setPreference:[NSNumber numberWithBool:yesOrNo] forKey:KEY_JABBER_VERIFY_CERTS group:GROUP_ACCOUNT_STATUS];
795 - (BOOL)shouldVerifyCertificates {
796 return [[self preferenceForKey:KEY_JABBER_VERIFY_CERTS group:GROUP_ACCOUNT_STATUS] boolValue];
799 - (NSArray *)accountActionMenuItems {
800 AILog(@"Getting accountActionMenuItems for %@",self);
801 NSMutableArray *menu = [[NSMutableArray alloc] init];
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];
810 NSArray *menuitemarray = [self menuItemsForContact:gateway];
811 for (NSMenuItem *m2item in menuitemarray)
812 [submenu addItem:m2item];
814 if([submenu numberOfItems] > 0)
815 [submenu addItem:[NSMenuItem separatorItem]];
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];
823 [mitem setSubmenu:submenu];
825 [mitem setRepresentedObject:gateway];
826 [mitem setImage:[AIStatusIcons statusIconForListObject:gateway
828 direction:AIIconNormal]];
829 [mitem setTarget:self];
830 [menu addObject:mitem];
833 [menu addObject:[NSMenuItem separatorItem]];
836 NSArray *supermenu = [super accountActionMenuItems];
838 [menu addObjectsFromArray:supermenu];
839 [menu addObject:[NSMenuItem separatorItem]];
842 if ([self enableXMLConsole]) {
843 NSMenuItem *xmlConsoleMenuItem = [[NSMenuItem alloc] initWithTitle:AILocalizedString(@"XML Console",nil)
844 action:@selector(showXMLConsole:)
846 [xmlConsoleMenuItem setTarget:self];
847 [menu addObject:xmlConsoleMenuItem];
848 [xmlConsoleMenuItem release];
851 NSMenuItem *discoveryBrowserMenuItem = [[NSMenuItem alloc] initWithTitle:AILocalizedString(@"Discovery Browser",nil)
852 action:@selector(showDiscoveryBrowser:)
854 [discoveryBrowserMenuItem setTarget:self];
855 [menu addObject:discoveryBrowserMenuItem];
856 [discoveryBrowserMenuItem release];
858 return [menu autorelease];
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]);
868 - (void)removeGateway:(NSMenuItem*)mitem {
869 AIListContact *gateway = [mitem representedObject];
870 if(![gateway isKindOfClass:[AIListContact class]])
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)
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];
889 // now, remove them from the roster
890 [self removeContacts:gatewayContacts
891 fromGroups:removeGroups.allObjects];
893 [gatewayContacts release];
895 // finally, remove the gateway itself
896 [self removeContact:gateway];
900 - (AMPurpleJabberAdHocServer*)adhocServer {