Plugins/Purple Service/CBPurpleAccount.m
author Zachary West <zacw@adium.im>
Sun Nov 01 14:04:11 2009 -0500 (2009-11-01)
changeset 2848 d88a4b7a70a8
parent 2693 4bcad311909f
child 2938 3294410d095f
permissions -rw-r--r--
Display unhandled purple conversation writes in the next run loop. Fixes #13190.
     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 "CBPurpleAccount.h"
    18 
    19 #import "PurpleService.h"
    20 
    21 #import <libpurple/notify.h>
    22 #import <libpurple/cmds.h>
    23 #import <AdiumLibpurple/SLPurpleCocoaAdapter.h>
    24 #import <Adium/AIAccount.h>
    25 #import <Adium/AIChat.h>
    26 #import <Adium/AIContentMessage.h>
    27 #import <Adium/AIContentTopic.h>
    28 #import <Adium/AIContentEvent.h>
    29 #import <Adium/AIContentContext.h>
    30 #import <Adium/AIContentNotification.h>
    31 #import <Adium/AIHTMLDecoder.h>
    32 #import <Adium/AIListContact.h>
    33 #import <Adium/AIListGroup.h>
    34 #import <Adium/AIListObject.h>
    35 #import <Adium/AIMetaContact.h>
    36 #import <Adium/AIService.h>
    37 #import <Adium/AIServiceIcons.h>
    38 #import <Adium/AIStatus.h>
    39 #import <Adium/ESFileTransfer.h>
    40 #import <Adium/AIWindowController.h>
    41 #import <Adium/AIEmoticon.h>
    42 #import <Adium/AIAccountControllerProtocol.h>
    43 #import <Adium/AIChatControllerProtocol.h>
    44 #import <Adium/AIContactControllerProtocol.h>
    45 #import <Adium/AIContactObserverManager.h>
    46 #import <Adium/AIContentControllerProtocol.h>
    47 #import <Adium/AIInterfaceControllerProtocol.h>
    48 #import <Adium/AIStatusControllerProtocol.h>
    49 #import <AIUtilities/AIAttributedStringAdditions.h>
    50 #import <AIUtilities/AIDictionaryAdditions.h>
    51 #import <AIUtilities/AIMenuAdditions.h>
    52 #import <AIUtilities/AIMutableOwnerArray.h>
    53 #import <AIUtilities/AIStringAdditions.h>
    54 #import <AIUtilities/AIApplicationAdditions.h>
    55 #import <AIUtilities/AIObjectAdditions.h>
    56 #import <AIUtilities/AIImageAdditions.h>
    57 #import <AIUtilities/AIImageDrawingAdditions.h>
    58 #import <AIUtilities/AIMutableStringAdditions.h>
    59 #import <AIUtilities/AISystemNetworkDefaults.h>
    60 #import <Adium/AdiumAuthorization.h>
    61 
    62 #import "ESiTunesPlugin.h"
    63 #import "AMPurpleTuneTooltip.h"
    64 #import "adiumPurpleRequest.h"
    65 #import "AIDualWindowInterfacePlugin.h"
    66 
    67 #ifdef HAVE_CDSA
    68 #import "AIPurpleCertificateViewer.h"
    69 #endif
    70 
    71 #define NO_GROUP						@"__NoGroup__"
    72 
    73 #define	PREF_GROUP_ALIASES			@"Aliases"		//Preference group to store aliases in
    74 #define NEW_ACCOUNT_DISPLAY_TEXT		AILocalizedString(@"<New Account>", "Placeholder displayed as the name of a new account")
    75 
    76 #define	KEY_PRIVACY_OPTION	@"Privacy Option"
    77 
    78 @interface CBPurpleAccount ()
    79 - (NSString *)_mapIncomingGroupName:(NSString *)name;
    80 - (NSString *)_mapOutgoingGroupName:(NSString *)name;
    81 - (void)setTypingFlagOfChat:(AIChat *)inChat to:(NSNumber *)typingState;
    82 - (void)_receivedMessage:(NSAttributedString *)attributedMessage inChat:(AIChat *)chat fromListContact:(AIListContact *)sourceContact flags:(PurpleMessageFlags)flags date:(NSDate *)date;
    83 - (NSNumber *)shouldCheckMail;
    84 - (void)configurePurpleAccountNotifyingTarget:(id)target selector:(SEL)selector;
    85 - (void)continueConnectWithConfiguredPurpleAccount;
    86 - (void)continueConnectWithConfiguredProxy;
    87 - (void)continueRegisterWithConfiguredPurpleAccount;
    88 - (void)promptForHostBeforeConnecting;
    89 - (void)setAccountProfileTo:(NSAttributedString *)profile configurePurpleAccountContext:(NSInvocation *)inInvocation;
    90 - (void)performAccountMenuAction:(NSMenuItem *)sender;
    91 
    92 - (void)showServerCertificate;
    93 @end
    94 
    95 @implementation CBPurpleAccount
    96 
    97 static SLPurpleCocoaAdapter *purpleAdapter = nil;
    98 
    99 // The PurpleAccount currently associated with this Adium account
   100 - (PurpleAccount*)purpleAccount
   101 {
   102 	//Create a purple account if one does not already exist
   103 	if (!account) {
   104 		[self createNewPurpleAccount];
   105 		AILog(@"Created PurpleAccount 0x%x with UID %@, protocolPlugin %s", account, self.UID, [self protocolPlugin]);
   106 	}
   107 	
   108     return account;
   109 }
   110 
   111 - (SLPurpleCocoaAdapter *)purpleAdapter
   112 {
   113 	if (!purpleAdapter) {
   114 		purpleAdapter = [[SLPurpleCocoaAdapter sharedInstance] retain];	
   115 	}	
   116 	return purpleAdapter;
   117 }
   118 
   119 // Subclasses must override this
   120 - (const char*)protocolPlugin { return NULL; }
   121 
   122 - (PurplePluginProtocolInfo *)protocolInfo
   123 {
   124 	PurplePlugin				*prpl;
   125 	
   126 	if ((prpl = purple_find_prpl(purple_account_get_protocol_id(account)))) {
   127 		return PURPLE_PLUGIN_PROTOCOL_INFO(prpl);
   128 	}
   129 	
   130 	return NULL;
   131 }
   132 
   133 // Contacts ------------------------------------------------------------------------------------------------
   134 #pragma mark Contacts
   135 - (void)newContact:(AIListContact *)theContact withName:(NSString *)inName
   136 {
   137 
   138 }
   139 
   140 - (void)addContact:(AIListContact *)theContact toGroupName:(NSString *)groupName contactName:(NSString *)contactName
   141 {
   142 	//When a new contact is created, if we aren't already silent and delayed, set it  a second to cover our initial
   143 	//status updates
   144 	if (!silentAndDelayed) {
   145 		[self silenceAllContactUpdatesForInterval:2.0];
   146 		[[AIContactObserverManager sharedManager] delayListObjectNotificationsUntilInactivity];		
   147 	}
   148 	
   149 	//If the name we were passed differs from the current formatted UID of the contact, it's itself a formatted UID
   150 	//This is important since we may get an alias ("Evan Schoenberg") from the server but also want the formatted name
   151 	if (![contactName isEqualToString:theContact.formattedUID] && ![contactName isEqualToString:theContact.UID]) {
   152 		[theContact setValue:contactName
   153 							 forProperty:@"FormattedUID"
   154 							 notify:NotifyLater];
   155 	}
   156 	
   157 	if (groupName && [groupName isEqualToString:@PURPLE_ORPHANS_GROUP_NAME]) {
   158 		[theContact addRemoteGroupName:AILocalizedString(@"Orphans","Name for the orphans group")];
   159 	} else if (groupName && [groupName length] != 0) {
   160 		[theContact addRemoteGroupName:[self _mapIncomingGroupName:groupName]];
   161 	} else {
   162 		AILog(@"Got a nil group for %@",theContact);
   163 	}
   164 	
   165 	[self gotGroupForContact:theContact];
   166 }
   167 
   168 - (void)removeContact:(AIListContact *)theContact fromGroupName:(NSString *)groupName
   169 {
   170 	NSParameterAssert(groupName != nil); //is this always true?
   171 	NSParameterAssert(theContact != nil);
   172 	[theContact removeRemoteGroupName:[self _mapIncomingGroupName:groupName]];
   173 }
   174 
   175 /*!
   176  * @brief Change the UID of a contact
   177  *
   178  * If we're just passed a formatted version of the current UID, don't change the UID but instead use the information
   179  * as the FormattedUID.  For example, we get sent this when an AIM contact's name formatting changes; we always want
   180  * to use a lowercase and space-free version for the UID, however.
   181  */
   182 - (void)renameContact:(AIListContact *)theContact toUID:(NSString *)newUID
   183 {
   184 	//If the name we were passed differs from the current formatted UID of the contact, it's itself a formatted UID
   185 	//This is important since we may get an alias ("Evan Schoenberg") from the server but also want the formatted name
   186 	NSString	*normalizedUID = [self.service normalizeUID:newUID removeIgnoredCharacters:YES];
   187 	
   188 	if ([normalizedUID isEqualToString:theContact.UID]) {
   189 		[theContact setValue:newUID
   190 							 forProperty:@"FormattedUID"
   191 							 notify:NotifyLater];		
   192 	} else {
   193 		[theContact setUID:newUID];		
   194 	}
   195 }
   196 
   197 - (void)updateContact:(AIListContact *)theContact toAlias:(NSString *)purpleAlias
   198 {
   199 	if (![[purpleAlias compactedString] isEqualToString:[theContact.UID compactedString]]) {
   200 		//Store this alias as the serverside display name so long as it isn't identical when unformatted to the UID
   201 		[theContact setServersideAlias:purpleAlias
   202 							  silently:silentAndDelayed];
   203 
   204 	} else {
   205 		//If it's the same characters as the UID, apply it as a formatted UID
   206 		if (![purpleAlias isEqualToString:theContact.formattedUID] && 
   207 			![purpleAlias isEqualToString:theContact.UID]) {
   208 			[theContact setFormattedUID:purpleAlias
   209 								 notify:NotifyLater];
   210 
   211 			//Apply any changes
   212 			[theContact notifyOfChangedPropertiesSilently:silentAndDelayed];
   213 		}
   214 	}
   215 }
   216 
   217 - (void)updateContact:(AIListContact *)theContact forEvent:(NSNumber *)event
   218 {
   219 }		
   220 
   221 
   222 //Signed online
   223 - (void)updateSignon:(AIListContact *)theContact withData:(void *)data
   224 {
   225 	[theContact setOnline:YES
   226 				   notify:NotifyLater
   227 				 silently:silentAndDelayed];
   228 
   229 	[theContact notifyOfChangedPropertiesSilently:silentAndDelayed];
   230 }
   231 
   232 //Signed offline
   233 - (void)updateSignoff:(AIListContact *)theContact withData:(void *)data
   234 {
   235 	[theContact setOnline:NO
   236 				   notify:NotifyLater
   237 				 silently:silentAndDelayed];
   238 	
   239 	[theContact notifyOfChangedPropertiesSilently:silentAndDelayed];
   240 }
   241 
   242 //Signon Time
   243 - (void)updateSignonTime:(AIListContact *)theContact withData:(NSDate *)signonDate
   244 {	
   245 	[theContact setSignonDate:signonDate
   246 					   notify:NotifyLater];
   247 	
   248 	//Apply any changes
   249 	[theContact notifyOfChangedPropertiesSilently:silentAndDelayed];
   250 }
   251 
   252 /*!
   253  * @brief Status name to use for a Purple buddy
   254  */
   255 - (NSString *)statusNameForPurpleBuddy:(PurpleBuddy *)buddy
   256 {
   257 	return nil;
   258 }
   259 
   260 /*!
   261  * @brief Status message for a contact
   262  */
   263 - (NSAttributedString *)statusMessageForPurpleBuddy:(PurpleBuddy *)buddy
   264 {
   265 	PurplePresence		*presence = purple_buddy_get_presence(buddy);
   266 	PurpleStatus		*status = (presence ? purple_presence_get_active_status(presence) : NULL);
   267 	const char			*message = (status ? purple_status_get_attr_string(status, "message") : NULL);
   268 	NSString			*statusMessage = nil;
   269 	
   270 	// Get the plugin's status message for this buddy if they don't have a status message
   271 	if (!message) {
   272 		PurplePluginProtocolInfo  *prpl_info = self.protocolInfo;
   273 		
   274 		if (prpl_info && prpl_info->status_text) {
   275 			char *status_text = (prpl_info->status_text)(buddy);
   276 			
   277 			// Don't display "Offline" as a status message.
   278 			if (status_text && strcmp(status_text, _("Offline")) != 0) {
   279 				statusMessage = [NSString stringWithUTF8String:status_text];				
   280 			}
   281 			
   282 			g_free(status_text);
   283 		}
   284 	} else {
   285 		statusMessage = [NSString stringWithUTF8String:message];
   286 	}
   287 	
   288 	return statusMessage ? [AIHTMLDecoder decodeHTML:statusMessage] : nil;
   289 }
   290 
   291 /*!
   292  * @brief Update the status message and away state of the contact
   293  */
   294 - (void)updateStatusForContact:(AIListContact *)theContact toStatusType:(NSNumber *)statusTypeNumber statusName:(NSString *)statusName statusMessage:(NSAttributedString *)statusMessage isMobile:(BOOL)isMobile
   295 {
   296 	[theContact setStatusWithName:statusName
   297 					   statusType:[statusTypeNumber integerValue]
   298 						   notify:NotifyLater];
   299 	[theContact setStatusMessage:statusMessage
   300 						  notify:NotifyLater];
   301 	[theContact setIsMobile:isMobile notify:NotifyLater];
   302 
   303 	//Apply the change
   304 	[theContact notifyOfChangedPropertiesSilently:silentAndDelayed];
   305 }
   306 
   307 //Idle time
   308 - (void)updateWentIdle:(AIListContact *)theContact withData:(NSDate *)idleSinceDate
   309 {
   310 	[theContact setIdle:YES sinceDate:idleSinceDate notify:NotifyLater];
   311 
   312 	//Apply any changes
   313 	[theContact notifyOfChangedPropertiesSilently:silentAndDelayed];
   314 }
   315 - (void)updateIdleReturn:(AIListContact *)theContact withData:(void *)data
   316 {
   317 	[theContact setIdle:NO
   318 			  sinceDate:nil
   319 				 notify:NotifyLater];
   320 
   321 	//Apply any changes
   322 	[theContact notifyOfChangedPropertiesSilently:silentAndDelayed];
   323 }
   324 	
   325 //Evil level (warning level)
   326 - (void)updateEvil:(AIListContact *)theContact withData:(NSNumber *)evilNumber
   327 {
   328 	[theContact setWarningLevel:[evilNumber integerValue]
   329 						 notify:NotifyLater];
   330 
   331 	//Apply any changes
   332 	[theContact notifyOfChangedPropertiesSilently:silentAndDelayed];
   333 }
   334 
   335 
   336 - (void)clearIconForContact:(AIListContact *)theContact
   337 {
   338 	[theContact setServersideIconData:nil
   339 							   notify:NotifyLater];
   340 	
   341 	//Apply any changes
   342 	[theContact notifyOfChangedPropertiesSilently:silentAndDelayed];	
   343 }
   344 
   345 //Buddy Icon
   346 - (void)updateIcon:(AIListContact *)theContact withData:(NSData *)userIconData
   347 {
   348 	[NSObject cancelPreviousPerformRequestsWithTarget:self
   349 											 selector:@selector(clearIconForContact:)
   350 											   object:theContact];
   351 	if (userIconData) {
   352 		[theContact setServersideIconData:userIconData
   353 								   notify:NotifyLater];
   354 		
   355 		//Apply any changes
   356 		[theContact notifyOfChangedPropertiesSilently:silentAndDelayed];
   357 
   358 	} else {
   359 		/* We may receive an empty icon update just before an actual change. We don't want to flicker through no-icon.
   360 		 * We therefore cancel empty icon updates when we receive a new icon, and we do the actual clearing on a delay in case
   361 		 * this is what is about to happen.
   362 		 */
   363 		[self performSelector:@selector(clearIconForContact:)
   364 				   withObject:theContact
   365 				   afterDelay:10.0];
   366 	}
   367 }
   368 
   369 - (NSString *)processedIncomingUserInfo:(NSString *)inString
   370 {
   371 	NSMutableString *returnString = nil;
   372 	if ([inString rangeOfString:@"Purple could not find any information in the user's profile. The user most likely does not exist."].location != NSNotFound) {
   373 		returnString = [[inString mutableCopy] autorelease];
   374 		[returnString replaceOccurrencesOfString:@"Purple could not find any information in the user's profile. The user most likely does not exist."
   375 									  withString:AILocalizedString(@"Adium could not find any information in the user's profile. This may not be a registered name.", "Message shown when a contact's profile can't be found")
   376 										 options:NSLiteralSearch
   377 										   range:NSMakeRange(0, [returnString length])];
   378 	}
   379 	
   380 	return (returnString ? returnString : inString);
   381 }
   382 
   383 - (NSString *)webProfileStringForContact:(AIListContact *)contact
   384 {
   385 	return [NSString stringWithFormat:NSLocalizedString(@"View %@'s %@ web profile", nil), 
   386 			contact.formattedUID, [contact.service shortDescription]];
   387 }
   388 
   389 - (NSMutableArray *)arrayOfDictionariesFromPurpleNotifyUserInfo:(PurpleNotifyUserInfo *)user_info forContact:(AIListContact *)contact
   390 {
   391 	GList *l;
   392 	NSMutableArray *array = [NSMutableArray array];
   393 	
   394 	for (l = purple_notify_user_info_get_entries(user_info); l != NULL; l = l->next) {
   395 		PurpleNotifyUserInfoEntry *user_info_entry = l->data;
   396 		
   397 		switch (purple_notify_user_info_entry_get_type(user_info_entry)) {
   398 			case PURPLE_NOTIFY_USER_INFO_ENTRY_SECTION_HEADER:
   399 				[array addObject:[NSDictionary dictionaryWithObjectsAndKeys:
   400 								  [NSString stringWithUTF8String:purple_notify_user_info_entry_get_label(user_info_entry)], KEY_KEY,
   401 								  [NSNumber numberWithInteger:AIUserInfoSectionHeader], KEY_TYPE,
   402 								  nil]];
   403 				
   404 				break;
   405 			case PURPLE_NOTIFY_USER_INFO_ENTRY_SECTION_BREAK:
   406 				[array addObject:[NSDictionary dictionaryWithObjectsAndKeys:
   407 								  [NSNumber numberWithInteger:AIUserInfoSectionBreak], KEY_TYPE,
   408 								  nil]];
   409 				break;
   410 				
   411 			case PURPLE_NOTIFY_USER_INFO_ENTRY_PAIR:
   412 			{
   413 				if (purple_notify_user_info_entry_get_label(user_info_entry) && purple_notify_user_info_entry_get_value(user_info_entry)) {
   414 					[array addObject:[NSDictionary dictionaryWithObjectsAndKeys:
   415 									  [NSString stringWithUTF8String:purple_notify_user_info_entry_get_label(user_info_entry)], KEY_KEY,
   416 									  processPurpleImages([NSString stringWithUTF8String:purple_notify_user_info_entry_get_value(user_info_entry)], self), KEY_VALUE,
   417 									  nil]];
   418 					
   419 				} else if (purple_notify_user_info_entry_get_label(user_info_entry)) {
   420 					[array addObject:[NSDictionary dictionaryWithObject:
   421 									  [NSString stringWithUTF8String:purple_notify_user_info_entry_get_label(user_info_entry)]
   422 																 forKey:KEY_KEY]];
   423 				} else if (purple_notify_user_info_entry_get_value(user_info_entry)) {
   424 					NSMutableString	*value = [processPurpleImages([NSString stringWithUTF8String:purple_notify_user_info_entry_get_value(user_info_entry)],
   425 																  self) mutableCopy];
   426 					[value replaceOccurrencesOfString:@"<br>" withString:@"<br/>" options:(NSCaseInsensitiveSearch | NSLiteralSearch)];
   427 					[value replaceOccurrencesOfString:@"<br />" withString:@"<br/>" options:(NSCaseInsensitiveSearch | NSLiteralSearch)];
   428 					[value replaceOccurrencesOfString:@"<B>" withString:@"<b>" options:NSLiteralSearch];
   429 
   430 					for (NSString *valuePair in [value componentsSeparatedByString:@"<br/><b>"]) {
   431 						NSRange	firstStartBold = [valuePair rangeOfString:@"<b>"];
   432 						NSRange	firstEndBold = [valuePair rangeOfString:@"</b>"];
   433 						
   434 						if (firstEndBold.length > 0) {
   435 							// Chop off <b> from the beginning and :</b> from the end. The extra -1 is for the colon.
   436 							[array addObject:[NSDictionary dictionaryWithObjectsAndKeys:
   437 											  [valuePair substringWithRange:NSMakeRange(firstStartBold.length, firstEndBold.location-firstStartBold.length-1)], KEY_KEY,
   438 											  [valuePair substringFromIndex:NSMaxRange(firstEndBold)], KEY_VALUE,
   439 											  nil]];
   440 						} else {
   441 							[array addObject:[NSDictionary dictionaryWithObject:valuePair
   442 																		forKey:KEY_VALUE]];
   443 						}
   444 					}
   445 					[value release];
   446 				}	
   447 				break;
   448 			}
   449 		}
   450 	}
   451 
   452 	NSString *webProfileValue = [NSString stringWithFormat:@"%s</a>", _("View web profile")];
   453 	
   454 	NSInteger i;
   455 	NSUInteger count = [array count];
   456 	for (i = 0; i < count; i++) {
   457 		NSDictionary *dict = [array objectAtIndex:i];
   458 		NSString *value = [dict objectForKey:KEY_VALUE];
   459 		if (value &&
   460 			[value rangeOfString:webProfileValue options:(NSBackwardsSearch | NSAnchoredSearch | NSLiteralSearch)].location != NSNotFound) {
   461 			NSMutableString *newValue = [[value mutableCopy] autorelease];
   462 			[newValue replaceOccurrencesOfString:webProfileValue
   463 									  withString:[self webProfileStringForContact:contact]
   464 										 options:(NSBackwardsSearch | NSAnchoredSearch | NSLiteralSearch)];
   465 			
   466 			NSMutableDictionary *replacementDict = [dict mutableCopy];
   467 			[replacementDict setObject:newValue forKey:KEY_VALUE];
   468 			[array replaceObjectAtIndex:i withObject:replacementDict];
   469 			[replacementDict release];
   470 
   471 			/* There will only be 1 (at most) web profile link */
   472 			break;
   473 		}
   474 	}
   475 	
   476 	return array;
   477 }
   478 
   479 - (void)updateUserInfo:(AIListContact *)theContact withData:(PurpleNotifyUserInfo *)user_info
   480 {
   481 	NSArray		*profileContents = [self arrayOfDictionariesFromPurpleNotifyUserInfo:user_info forContact:theContact];
   482 
   483 	[theContact setProfileArray:profileContents
   484 					notify:NotifyLater];
   485 	
   486 	[self openInspectorForContactInfo:theContact];
   487 	
   488 	//Apply any changes
   489 	[theContact notifyOfChangedPropertiesSilently:silentAndDelayed];
   490 }
   491 
   492 /*!
   493  * @brief Open the info inspector when getting info
   494  */
   495 - (void)openInspectorForContactInfo:(AIListContact *)theContact
   496 {
   497 
   498 }
   499 
   500 /*!
   501  * @brief Purple removed a contact from the local blist
   502  *
   503  * This can happen in many situations:
   504  *	- For every contact on an account when the account signs off
   505  *	- For a contact as it is deleted by the user
   506  *	- For a contact as it is deleted by Purple (e.g. when Sametime refuses an addition because it is known to be invalid)
   507  *	- In the middle of the move process as a contact moves from one group to another
   508  *
   509  * We need not take any action; we'll be notified of changes by Purple as necessary.
   510  */
   511 - (void)removeContact:(AIListContact *)theContact
   512 {
   513 
   514 }
   515 
   516 //To allow root level buddies on protocols which don't support them, we map any buddies in a group
   517 //named after this account's UID to the root group.  These functions handle the mapping.  Group names should
   518 //be filtered through incoming before being sent to Adium - and group names from Adium should be filtered through
   519 //outgoing before being used.
   520 - (NSString *)_mapIncomingGroupName:(NSString *)name
   521 {
   522 	if (!name || ([[name compactedString] caseInsensitiveCompare:self.UID] == NSOrderedSame)) {
   523 		return ADIUM_ROOT_GROUP_NAME;
   524 	} else {
   525 		return name;
   526 	}
   527 }
   528 - (NSString *)_mapOutgoingGroupName:(NSString *)name
   529 {
   530 	if ([[name compactedString] caseInsensitiveCompare:ADIUM_ROOT_GROUP_NAME] == NSOrderedSame) {
   531 		return self.UID;
   532 	} else {
   533 		return name;
   534 	}
   535 }
   536 
   537 //Update the status of a contact (Request their profile)
   538 - (void)delayedUpdateContactStatus:(AIListContact *)inContact
   539 {
   540     //Request profile
   541 	AILogWithSignature(@"");
   542 	[purpleAdapter getInfoFor:inContact.UID onAccount:self];
   543 }
   544 
   545 - (void)requestAddContactWithUID:(NSString *)contactUID
   546 {
   547 	[adium.contactController requestAddContactWithUID:contactUID
   548 												service:[self _serviceForUID:contactUID]
   549 												account:self];
   550 }
   551 
   552 - (AIService *)_serviceForUID:(NSString *)contactUID
   553 {
   554 	return self.service;
   555 }
   556 
   557 - (void)gotGroupForContact:(AIListContact *)listContact {};
   558 
   559 /*!
   560  * @brief Return the serverside icon for a contact
   561  */
   562 - (NSData *)serversideIconDataForContact:(AIListContact *)contact
   563 {
   564 	PurpleBuddy		*buddy;
   565 	NSData			*data = nil;
   566 
   567 	if (self.purpleAccount &&
   568 		(buddy = purple_find_buddy(account, [contact.UID UTF8String]))) {
   569 		PurpleBuddyIcon *buddyIcon;
   570 		BOOL			shouldUnref = NO;
   571 		
   572 		/* First, try to get a current buddy icon from the PurpleBuddy */
   573 		buddyIcon = purple_buddy_get_icon(buddy);
   574 		if (!buddyIcon) {
   575 			/* Failing that, load one from the cache. We'll need to unreference the returned PurpleBuddyIcon
   576 			 * when we're done.
   577 			 */
   578 			buddyIcon = purple_buddy_icons_find(account, [contact.UID UTF8String]);
   579 			shouldUnref = YES;
   580 		}
   581 		
   582 		if (buddyIcon) {
   583 			const guchar	*iconData;
   584 			size_t			len;
   585 			
   586 			iconData = purple_buddy_icon_get_data(buddyIcon, &len);
   587 			
   588 			if (iconData && len) {
   589 				data = [NSData dataWithBytes:iconData length:len];
   590 			}
   591 			
   592 			if (shouldUnref)
   593 				purple_buddy_icon_unref(buddyIcon);
   594 		}
   595 
   596 	} else {
   597 		AILogWithSignature(@"Could not get serverside icon data for %@. account is %p", contact, account);
   598 	}
   599 	
   600 	return data;
   601 }
   602 
   603 /*!
   604  * @brief Libpurple manages a contact icon cache; we don't need to duplicate it.
   605  */
   606 - (BOOL)managesOwnContactIconCache
   607 {
   608 	return YES;
   609 }
   610 
   611 /*********************/
   612 /* AIAccount_Handles */
   613 /*********************/
   614 #pragma mark Contact List Editing
   615 
   616 - (void)removeContacts:(NSArray *)objects fromGroups:(NSArray *)groups
   617 {	
   618 	for (AIListGroup *group in groups) {
   619 		NSString *groupName = [self _mapOutgoingGroupName:group.UID];
   620 	
   621 		for (AIListContact *object in objects) {
   622 			//Have the purple thread perform the serverside actions
   623 			[purpleAdapter removeUID:object.UID onAccount:self fromGroup:groupName];
   624 			
   625 			//Remove it from Adium's list
   626 			[object removeRemoteGroupName:groupName];
   627 		}
   628 	}
   629 }
   630 
   631 - (void)addContact:(AIListContact *)contact toGroup:(AIListGroup *)group
   632 {
   633 	NSString		*groupName = [self _mapOutgoingGroupName:group.UID];
   634 	
   635 	if(![group containsObject:contact]) {
   636 		AILogWithSignature(@"%@ adding %@ to %@", self, [self _UIDForAddingObject:contact], groupName);
   637 		
   638 		NSString *alias = [contact.parentContact preferenceForKey:@"Alias"
   639 						   group:PREF_GROUP_ALIASES];
   640 		
   641 		[purpleAdapter addUID:[self _UIDForAddingObject:contact] onAccount:self toGroup:groupName withAlias:alias];
   642 		
   643 		//Add it to Adium's list
   644 		[contact addRemoteGroupName:group.UID]; //Use the non-mapped group name locally
   645 	}
   646 }
   647 
   648 - (NSString *)_UIDForAddingObject:(AIListContact *)object
   649 {
   650 	return object.UID;
   651 }
   652 
   653 - (NSSet *)mappedGroupNamesFromGroups:(NSSet *)groups
   654 {
   655 	NSMutableSet *mappedNames = [NSMutableSet set];
   656 	
   657 	for (AIListGroup *group in groups) {
   658 		[mappedNames addObject:[self _mapOutgoingGroupName:group.UID]];
   659 	}
   660 	
   661 	return mappedNames;
   662 }
   663 
   664 - (void)moveListObjects:(NSArray *)objects fromGroups:(NSSet *)oldGroups toGroups:(NSSet *)groups
   665 {
   666 	NSSet *sourceMappedNames = [self mappedGroupNamesFromGroups:oldGroups];
   667 	NSSet *destinationMappedNames = [self mappedGroupNamesFromGroups:groups];
   668 
   669 	//Move the objects to it
   670 	for (AIListContact *contact in objects) {
   671 		if (![contact.remoteGroups intersectsSet:oldGroups] && oldGroups.count) {
   672 			continue;
   673 		}
   674 		
   675 		NSString *alias = [contact.parentContact preferenceForKey:@"Alias"
   676 						   group:PREF_GROUP_ALIASES];
   677 		
   678 		//Tell the purple thread to perform the serverside operation
   679 		[purpleAdapter moveUID:contact.UID onAccount:self fromGroups:sourceMappedNames toGroups:destinationMappedNames withAlias:alias];
   680 
   681 		for (AIListGroup *group in oldGroups) {
   682 			[contact removeRemoteGroupName:group.UID];
   683 		}
   684 		
   685 		for (AIListGroup *group in groups) {
   686 			[contact addRemoteGroupName:group.UID];
   687 		}
   688 	}		
   689 }
   690 
   691 - (void)renameGroup:(AIListGroup *)inGroup to:(NSString *)newName
   692 {
   693 	NSString		*groupName = [self _mapOutgoingGroupName:inGroup.UID];
   694 
   695 	//Tell the purple thread to perform the serverside operation	
   696 	[purpleAdapter renameGroup:groupName onAccount:self to:newName];
   697 
   698 	//We must also update the remote grouping of all our contacts in that group
   699 	for (AIListContact *contact in [adium.contactController allContactsInObject:inGroup onAccount:self]) {
   700 		[contact removeRemoteGroupName:groupName];
   701 		//Evan: should we use groupName or newName here?
   702 		[contact addRemoteGroupName:newName];
   703 	}
   704 }
   705 
   706 - (void)deleteGroup:(AIListGroup *)inGroup
   707 {
   708 	NSString		*groupName = [self _mapOutgoingGroupName:inGroup.UID];
   709 
   710 	[purpleAdapter deleteGroup:groupName onAccount:self];
   711 }
   712 
   713 // Return YES if the contact list is editable
   714 - (BOOL)contactListEditable
   715 {
   716     return self.online;
   717 }
   718 
   719 - (id)authorizationRequestWithDict:(NSDictionary*)dict
   720 {
   721 	// We retain this in case libpurple wants to close the request early. It is freed below.
   722 	return [[AdiumAuthorization showAuthorizationRequestWithDict:dict forAccount:self] retain];
   723 }
   724 
   725 - (void)authorizationWithDict:(NSDictionary *)infoDict response:(AIAuthorizationResponse)authorizationResponse
   726 {
   727 	if (account) {
   728 		NSValue	*callback = nil;
   729 
   730 		switch (authorizationResponse) {
   731 			case AIAuthorizationAllowed:
   732 				callback = [[[infoDict objectForKey:@"authorizeCB"] retain] autorelease];
   733 				break;
   734 			case AIAuthorizationDenied:
   735 				callback = [[[infoDict objectForKey:@"denyCB"] retain] autorelease];
   736 				break;
   737 			case AIAuthorizationNoResponse:
   738 				callback = nil;
   739 				break;
   740 		}
   741 		
   742 		//libpurple will remove its reference to the handle for this request, which is inDict, in response to this callback invocation
   743 		if (callback) {
   744 			[purpleAdapter doAuthRequestCbValue:callback withUserDataValue:[[[infoDict objectForKey:@"userData"] retain] autorelease]];
   745 
   746 			/* Retained in -[self authorizationRequestWithDict:].  We kept it around before now in case libpurle wanted us to close it early, such as because the
   747 			 * account disconnected.
   748 			 */
   749 			[infoDict release];
   750 		} else {
   751 			[purpleAdapter closeAuthRequestWithHandle:infoDict];
   752 			
   753 		}
   754 	}
   755 }
   756 
   757 #pragma mark Group chat ignore
   758 - (BOOL)accountManagesGroupChatIgnore
   759 {
   760 	return YES;
   761 }
   762 
   763 - (BOOL)contact:(AIListContact *)inContact isIgnoredInChat:(AIChat *)chat
   764 {
   765 	if (self.online && chat.isGroupChat) {
   766 		return [purpleAdapter contact:inContact isIgnoredInChat:chat];
   767 	} else {
   768 		return NO;
   769 	}
   770 }
   771 
   772 - (void)setContact:(AIListContact *)inContact ignored:(BOOL)inIgnored inChat:(AIChat *)chat
   773 {
   774 	if (self.online && chat.isGroupChat) {
   775 		[purpleAdapter setContact:inContact ignored:inIgnored inChat:chat];
   776 	}
   777 }
   778 
   779 //Chats ------------------------------------------------------------
   780 #pragma mark Chats
   781 - (void)removeUser:(NSString *)contactName fromChat:(AIChat *)chat
   782 {
   783 	if (!chat)
   784 		return;
   785 	
   786 	AIListContact *contact = [self contactWithUID:contactName];
   787 	[chat removeObject:contact];
   788 	
   789 	if (contact.isStranger && 
   790 		![adium.chatController allGroupChatsContainingContact:contact.parentContact].count &&
   791 		[adium.chatController existingChatWithContact:contact.parentContact]) {
   792 		// The contact is a stranger, not in any more group chats, but we have a message with them open.
   793 		// Set their status to unknown.
   794 		
   795 		[contact setStatusWithName:nil
   796 						statusType:AIUnknownStatus
   797 							notify:NotifyLater];
   798 		
   799 		[contact setValue:nil
   800 			  forProperty:@"Online"
   801 				   notify:NotifyLater];
   802 		
   803 		[contact notifyOfChangedPropertiesSilently:NO];
   804 	}
   805 }
   806 
   807 - (void)removeUsersArray:(NSArray *)usersArray fromChat:(AIChat *)chat
   808 {
   809 	for (NSString *contactName in usersArray) {
   810 		[self removeUser:contactName fromChat:chat];
   811 	}
   812 }
   813 
   814 - (void)updateUserListForChat:(AIChat *)chat users:(NSArray *)users newlyAdded:(BOOL)newlyAdded
   815 {
   816 	NSMutableArray *newListObjects = [NSMutableArray array];
   817 	
   818 	for (NSDictionary *user in users) {
   819 		AIListContact *contact = [self contactWithUID:[user objectForKey:@"UID"]];
   820 		
   821 		[contact setOnline:YES notify:NotifyNever silently:YES];
   822 		
   823 		[newListObjects addObject:contact];
   824 	}
   825 	
   826 	[chat addParticipatingListObjects:newListObjects notify:newlyAdded];
   827 	
   828 	for (NSDictionary *user in users) {
   829 		AIListContact *contact = [self contactWithUID:[user objectForKey:@"UID"]];
   830 		
   831 		[chat setFlags:(AIGroupChatFlags)[[user objectForKey:@"Flags"] integerValue] forContact:contact];
   832 		
   833 		if ([user objectForKey:@"Alias"]) {
   834 			[chat setAlias:[user objectForKey:@"Alias"] forContact:contact];
   835 			
   836 			if (contact.isStranger) {
   837 				[contact setServersideAlias:[user objectForKey:@"Alias"] silently:NO];
   838 			}
   839 		}
   840 	}
   841 	
   842 	// Post an update notification now that we've modified the flags and names.
   843 	[[NSNotificationCenter defaultCenter] postNotificationName:Chat_ParticipatingListObjectsChanged
   844 														object:chat];
   845 }
   846 
   847 - (void)renameParticipant:(NSString *)oldUID newName:(NSString *)newUID newAlias:(NSString *)newAlias flags:(AIGroupChatFlags)flags inChat:(AIChat *)chat
   848 {
   849 	[chat removeSavedValuesForContactUID:oldUID];
   850 	
   851 	AIListContact *contact = [adium.contactController existingContactWithService:self.service account:self UID:oldUID];
   852 
   853 	if (contact) {
   854 		[adium.contactController setUID:newUID forContact:contact];
   855 	} else {
   856 		contact = [self contactWithUID:newUID];
   857 	}
   858 
   859 	[chat setFlags:flags forContact:contact];
   860 	[chat setAlias:newAlias forContact:contact];
   861 	
   862 	if (contact.isStranger) {
   863 		[contact setServersideAlias:newAlias silently:NO];
   864 	}
   865 
   866 	// Post an update notification since we modified the user entirely.
   867 	[[NSNotificationCenter defaultCenter] postNotificationName:Chat_ParticipatingListObjectsChanged
   868 														object:chat];
   869 }
   870 
   871 - (void)setAttribute:(NSString *)name value:(NSString *)value forContact:(AIListContact *)contact
   872 {
   873 	NSString *property = nil;
   874 	
   875 	if ([name isEqualToString:@"userhost"]) {
   876 		property = @"User Host";
   877 	} else if ([name isEqualToString:@"realname"]) {
   878 		property = @"Real Name";
   879 	} else {
   880 		AILog(@"Unknown attribute: %@ value %@", name, value);
   881 	}
   882 	
   883 	if (property) {
   884 		// Callsite should notify.
   885 		[contact setValue:value forProperty:property notify:NotifyLater];
   886 	}
   887 }
   888 
   889 
   890 - (void)updateUser:(NSString *)user
   891 		   forChat:(AIChat *)chat
   892 			 flags:(AIGroupChatFlags)flags
   893 			 alias:(NSString *)alias
   894 		attributes:(NSDictionary *)attributes
   895 {
   896 	BOOL triggerUserlistUpdate = NO;
   897 	
   898 	AIListContact *contact = [self contactWithUID:user];
   899 	
   900 	AIGroupChatFlags oldFlags = [chat flagsForContact:contact];
   901 	NSString *oldAlias = [chat aliasForContact:contact];
   902 	
   903 	// Trigger an update if the alias or flags (ignoring away state) changes.
   904 	if ((alias && !oldAlias)
   905 		|| (!alias && oldAlias)
   906 		|| ![[chat aliasForContact:contact] isEqualToString:alias]
   907 		|| (flags & ~AIGroupChatAway) != (oldFlags & ~AIGroupChatAway)) {
   908 		triggerUserlistUpdate = YES;
   909 	}
   910 
   911 	[chat setAlias:alias forContact:contact];
   912 	[chat setFlags:flags forContact:contact];
   913 	
   914 	// Away changes only come in after the initial one, so we're safe in only updating it here.
   915 	if (contact.isStranger) {
   916 		[contact setStatusWithName:nil
   917 						statusType:((flags & AIGroupChatAway) == AIGroupChatAway) ? AIAwayStatusType : AIAvailableStatusType
   918 							notify:NotifyLater];
   919 	}
   920 
   921 	for (NSString *key in attributes.allKeys) {
   922 		[self setAttribute:key value:[attributes objectForKey:key] forContact:contact];
   923 	}
   924 	
   925 	[contact notifyOfChangedPropertiesSilently:YES];
   926 	
   927 	// Post an update notification if we modified the flags; don't resort for away changes.
   928 	if (triggerUserlistUpdate) {
   929 		[[NSNotificationCenter defaultCenter] postNotificationName:Chat_ParticipatingListObjectsChanged
   930 															object:chat];
   931 	}
   932 }
   933 
   934 /*!
   935  * @brief Called by Purple code when a chat should be opened by the interface
   936  *
   937  * If the user sent an initial message, this will be triggered and have no effect.
   938  *
   939  * If a remote user sent an initial message, however, a chat will be created without being opened.  This call is our
   940  * cue to actually open chat.
   941  *
   942  * Another situation in which this is relevant is when we request joining a group chat; the chat should only be actually
   943  * opened once the server notifies us that we are in the room.
   944  *
   945  * This will ultimately call -[CBPurpleAccount openChat:] below if the chat was not previously open.
   946  */
   947 - (void)addChat:(AIChat *)chat
   948 {
   949 	AILogWithSignature(@"");
   950 
   951 	//Open the chat
   952 	if ([chat isOpen]) {
   953 		if ([chat boolValueForProperty:@"Rejoining Chat"]) {
   954 			[self displayYouHaveConnectedInChat:chat];
   955 			
   956 			[chat setValue:nil forProperty:@"Rejoining Chat" notify:NotifyNever];
   957 		}
   958 	}
   959 
   960 	[adium.interfaceController openChat:chat];
   961 	
   962 	[chat setValue:[NSNumber numberWithBool:YES] forProperty:@"Account Joined" notify:NotifyNow];
   963 }
   964 
   965 //Open a chat for Adium
   966 - (BOOL)openChat:(AIChat *)chat
   967 {
   968 	/* The #if 0'd block below causes crashes in msn_tooltip_text() on MSN */
   969 #if 0
   970 	AIListContact	*listContact;
   971 	
   972 	//Obtain the contact's information if it's a stranger
   973 	if ((listContact = chat.listObject) && (listContact.isStranger)) {
   974 		[self delayedUpdateContactStatus:listContact];
   975 	}
   976 #endif
   977 	
   978 	AILog(@"purple openChat:%@ for %@",chat,chat.uniqueChatID);
   979 
   980 	//Inform purple that we have opened this chat
   981 	[purpleAdapter openChat:chat onAccount:self];
   982 	
   983 	//Created the chat successfully
   984 	return YES;
   985 }
   986 
   987 - (BOOL)closeChat:(AIChat*)chat
   988 {
   989 	[purpleAdapter closeChat:chat];
   990 	
   991 	if (!chat.isGroupChat) {
   992 		//Be sure any remaining typing flag is cleared as the chat closes
   993 		[self setTypingFlagOfChat:chat to:nil];
   994 	}
   995 	
   996 	AILog(@"purple closeChat:%@",chat.uniqueChatID);
   997 	
   998     return YES;
   999 }
  1000 
  1001 - (void)chatWasDestroyed:(AIChat *)chat
  1002 {
  1003 	[adium.chatController accountDidCloseChat:chat];
  1004 }
  1005 
  1006 - (void)chatJoinDidFail:(AIChat *)chat
  1007 {
  1008 	[adium.chatController accountDidCloseChat:chat];
  1009 }
  1010 
  1011 /* 
  1012  * @brief Rejoin a chat
  1013  */
  1014 - (BOOL)rejoinChat:(AIChat *)chat
  1015 {
  1016 	[chat retain];
  1017 
  1018 	PurpleConversation *conv = [[chat identifier] pointerValue];
  1019 	if (conv && conv->ui_data) {
  1020 		[(AIChat *)(conv->ui_data) release];
  1021 		conv->ui_data = NULL;
  1022 	}
  1023 
  1024 	/* The identifier is how we associate a PurpleConversation with an AIChat.
  1025 	 * Clear the identifier so a new PurpleConversation will be made. The ChatCreationInfo for the chat is still around, so it can join.
  1026 	 */
  1027 	[chat setIdentifier:nil];
  1028 	
  1029 	[chat setValue:[NSNumber numberWithBool:YES] forProperty:@"Rejoining Chat" notify:NotifyNever];
  1030 	
  1031 	[purpleAdapter openChat:chat onAccount:self];
  1032 
  1033 	[chat autorelease];
  1034 
  1035 	//We don't get any immediate feedback as to our success; just return YES.
  1036 	return YES;
  1037 }
  1038 
  1039 /*!
  1040  * @brief A chat will be joined
  1041  *
  1042  * This gives the account a chance to update any information in the chat's creation dictionary if desired.
  1043  *
  1044  * @result The final chat creation dictionary to use.
  1045  */
  1046 - (NSDictionary *)willJoinChatUsingDictionary:(NSDictionary *)chatCreationDictionary
  1047 {
  1048 	return chatCreationDictionary;
  1049 }
  1050 
  1051 - (BOOL)chatCreationDictionary:(NSDictionary *)chatCreationDict isEqualToDictionary:(NSDictionary *)baseDict
  1052 {
  1053 	return [chatCreationDict isEqualToDictionary:baseDict];
  1054 }
  1055 
  1056 - (NSDictionary *)extractChatCreationDictionaryFromConversation:(PurpleConversation *)conv
  1057 {
  1058 	AILog(@"%@ needs an implementation of extractChatCreationDictionaryFromConversation to handle rejoins, bookmarks, and invitations properly", NSStringFromClass([self class]));
  1059 	return nil;
  1060 }
  1061 
  1062 - (AIChat *)chatWithContact:(AIListContact *)contact identifier:(id)identifier
  1063 {
  1064 	AIChat *chat = [adium.chatController chatWithContact:contact];
  1065 	[chat setIdentifier:identifier];
  1066 
  1067 	return chat;
  1068 }
  1069 
  1070 
  1071 - (AIChat *)chatWithName:(NSString *)name identifier:(id)identifier
  1072 {
  1073 	return [adium.chatController chatWithName:name identifier:identifier onAccount:self chatCreationInfo:nil];
  1074 }
  1075 
  1076 //Typing update in an IM
  1077 - (void)typingUpdateForIMChat:(AIChat *)chat typing:(NSNumber *)typingState
  1078 {
  1079 	[self setTypingFlagOfChat:chat
  1080 						   to:typingState];
  1081 }
  1082 
  1083 //Multiuser chat update
  1084 - (void)convUpdateForChat:(AIChat *)chat type:(NSNumber *)type
  1085 {
  1086 
  1087 }
  1088 
  1089 /*!
  1090  * @brief Called when we are informed that we left a multiuser chat
  1091  */
  1092 - (void)leftChat:(AIChat *)chat
  1093 {
  1094 	[chat setValue:nil forProperty:@"Account Joined" notify:NotifyNow];
  1095 }
  1096 
  1097 - (void)updateTopic:(NSString *)inTopic forChat:(AIChat *)chat withSource:(NSString *)source
  1098 {	
  1099 	// Update (not set) the chat's topic
  1100 	[chat updateTopic:inTopic withSource:[self contactWithUID:source]];
  1101 }
  1102 
  1103 /*!
  1104  * @brief Set a chat's topic
  1105  *
  1106  * This only has an effect on group chats.
  1107  */
  1108 - (void)setTopic:(NSString *)topic forChat:(AIChat *)chat
  1109 {
  1110 	if (!chat.isGroupChat) {
  1111 		return;
  1112 	}
  1113 	
  1114 	PurplePluginProtocolInfo  *prpl_info = self.protocolInfo;
  1115 	
  1116 	if (prpl_info && prpl_info->set_chat_topic) {
  1117 		(prpl_info->set_chat_topic)(purple_account_get_connection(account),
  1118 									purple_conv_chat_get_id(purple_conversation_get_chat_data(convLookupFromChat(chat, self))),
  1119 									[topic UTF8String]);
  1120 	}
  1121 }
  1122 
  1123 
  1124 - (void)updateTitle:(NSString *)inTitle forChat:(AIChat *)chat
  1125 {
  1126 	[[chat displayArrayForKey:@"Display Name"] setObject:inTitle
  1127 											   withOwner:self];
  1128 }
  1129 
  1130 - (void)updateForChat:(AIChat *)chat type:(NSNumber *)type
  1131 {
  1132 	AIChatUpdateType	updateType = [type integerValue];
  1133 	NSString			*key = nil;
  1134 	switch (updateType) {
  1135 		case AIChatTimedOut:
  1136 		case AIChatClosedWindow:
  1137 			break;
  1138 	}
  1139 	
  1140 	if (key) {
  1141 		[chat setValue:[NSNumber numberWithBool:YES] forProperty:key notify:NotifyNow];
  1142 		[chat setValue:nil forProperty:key notify:NotifyNever];
  1143 		
  1144 	}
  1145 }
  1146 
  1147 - (void)errorForChat:(AIChat *)chat type:(NSNumber *)type
  1148 {
  1149 	[chat receivedError:type];
  1150 }
  1151 
  1152 - (void)receivedIMChatMessage:(NSDictionary *)messageDict inChat:(AIChat *)chat
  1153 {
  1154 	PurpleMessageFlags		flags = [[messageDict objectForKey:@"PurpleMessageFlags"] integerValue];
  1155 
  1156 	NSAttributedString		*attributedMessage;
  1157 	AIListContact			*listContact;
  1158 	
  1159 	listContact = chat.listObject;
  1160 
  1161 	attributedMessage = [adium.contentController decodedIncomingMessage:[messageDict objectForKey:@"Message"]
  1162 															  fromContact:listContact
  1163 																onAccount:self];
  1164 	
  1165 	//Clear the typing flag of the chat since a message was just received
  1166 	[self setTypingFlagOfChat:chat to:nil];
  1167 	
  1168 	[self _receivedMessage:attributedMessage
  1169 					inChat:chat 
  1170 		   fromListContact:listContact
  1171 					 flags:flags
  1172 					  date:[messageDict objectForKey:@"Date"]];
  1173 }
  1174 
  1175 - (void)receivedEventForChat:(AIChat *)chat
  1176 					 message:(NSString *)message
  1177 						date:(NSDate *)date
  1178 					   flags:(NSNumber *)flagsNumber
  1179 {
  1180 	PurpleMessageFlags flags = [flagsNumber integerValue];
  1181 	
  1182 	AIContentEvent *event = [AIContentEvent eventInChat:chat
  1183 											 withSource:nil
  1184 											destination:self
  1185 												   date:date
  1186 												message:[AIHTMLDecoder decodeHTML:message]
  1187 											   withType:@"purple"];
  1188 	
  1189 	event.filterContent = (flags & PURPLE_MESSAGE_NO_LINKIFY) != PURPLE_MESSAGE_NO_LINKIFY;
  1190 	
  1191 	[adium.contentController receiveContentObject:event];
  1192 }
  1193 
  1194 - (void)receivedMultiChatMessage:(NSDictionary *)messageDict inChat:(AIChat *)chat
  1195 {	
  1196 	PurpleMessageFlags	flags = [[messageDict objectForKey:@"PurpleMessageFlags"] integerValue];
  1197 	NSAttributedString	*attributedMessage = [messageDict objectForKey:@"AttributedMessage"];;
  1198 	NSString			*source = [messageDict objectForKey:@"Source"];
  1199 	
  1200 	[self _receivedMessage:attributedMessage
  1201 					inChat:chat 
  1202 		   fromListContact:[self contactWithUID:source]
  1203 					 flags:flags
  1204 					  date:[messageDict objectForKey:@"Date"]];
  1205 }
  1206 
  1207 - (void)_receivedMessage:(NSAttributedString *)attributedMessage inChat:(AIChat *)chat fromListContact:(AIListContact *)sourceContact flags:(PurpleMessageFlags)flags date:(NSDate *)date
  1208 {
  1209 	if ((flags & PURPLE_MESSAGE_DELAYED) == PURPLE_MESSAGE_DELAYED) {
  1210 		// Display delayed messages as context.
  1211 
  1212 		AIContentContext *messageObject = [AIContentContext messageInChat:chat
  1213 															   withSource:sourceContact
  1214 															  destination:self
  1215 																	 date:date
  1216 																  message:attributedMessage
  1217 																autoreply:(flags & PURPLE_MESSAGE_AUTO_RESP) != 0];
  1218 		
  1219 		messageObject.trackContent = NO;
  1220 		
  1221 		[adium.contentController receiveContentObject:messageObject];
  1222 		
  1223 	} else {
  1224 		AIContentMessage *messageObject = [AIContentMessage messageInChat:chat
  1225 															   withSource:sourceContact
  1226 															  destination:self
  1227 																	 date:date
  1228 																  message:attributedMessage
  1229 																autoreply:(flags & PURPLE_MESSAGE_AUTO_RESP) != 0];
  1230 		
  1231 		[adium.contentController receiveContentObject:messageObject];	
  1232 	}
  1233 }
  1234 
  1235 /*********************/
  1236 /* AIAccount_Content */
  1237 /*********************/
  1238 #pragma mark Content
  1239 - (void)sendTypingObject:(AIContentTyping *)inContentTyping
  1240 {
  1241 	AIChat *chat = inContentTyping.chat;
  1242 
  1243 	if (!chat.isGroupChat) {
  1244 		[purpleAdapter sendTyping:inContentTyping.typingState inChat:chat];
  1245 	}
  1246 }
  1247 
  1248 - (BOOL)sendMessageObject:(AIContentMessage *)inContentMessage
  1249 {
  1250 	PurpleMessageFlags		flags = PURPLE_MESSAGE_RAW;
  1251 	
  1252 	if ([inContentMessage isAutoreply]) {
  1253 		flags |= PURPLE_MESSAGE_AUTO_RESP;
  1254 	}
  1255 
  1256 	[purpleAdapter sendEncodedMessage:[inContentMessage encodedMessage]
  1257 						 fromAccount:self
  1258 							  inChat:inContentMessage.chat
  1259 						   withFlags:flags];
  1260 
  1261 	return YES;
  1262 }
  1263 
  1264 - (BOOL)supportsSendingNotifications
  1265 {
  1266 	return (account ? ((PURPLE_PLUGIN_PROTOCOL_INFO(purple_find_prpl(purple_account_get_protocol_id(account)))->send_attention) != NULL) : NO);
  1267 }
  1268 
  1269 - (BOOL)sendNotificationObject:(AIContentNotification *)inContentNotification
  1270 {
  1271 	[purpleAdapter sendNotificationOfType:[inContentNotification notificationType]
  1272 							  fromAccount:self
  1273 								   inChat:inContentNotification.chat];	
  1274 	
  1275 	return YES;
  1276 }
  1277 
  1278 /*!
  1279  * @brief Return the string encoded for sending to a remote contact
  1280  *
  1281  * We return nil if the string turns out to have been a / command.
  1282  */
  1283 - (NSString *)encodedAttributedStringForSendingContentMessage:(AIContentMessage *)inContentMessage
  1284 {
  1285 	BOOL		didCommand = [purpleAdapter attemptPurpleCommandOnMessage:[inContentMessage.message string]
  1286 														 fromAccount:(AIAccount *)[inContentMessage source]
  1287 															  inChat:inContentMessage.chat];	
  1288 	
  1289 	return (didCommand ? nil : [super encodedAttributedStringForSendingContentMessage:inContentMessage]);
  1290 }
  1291 
  1292 /*!
  1293  * @brief Libpurple prints file transfer messages to the chat window. The Adium core therefore shouldn't.
  1294  */
  1295 - (BOOL)accountDisplaysFileTransferMessages
  1296 {
  1297 	return YES;
  1298 }
  1299 
  1300 /*!
  1301  * @brief Available for sending content
  1302  *
  1303  * Returns YES if the contact is available for receiving content of the specified type.  If contact is nil, instead
  1304  * check for the availiability to send any content of the given type.
  1305  *
  1306  * We override the default implementation to check -[self allowFileTransferWithListObject:] for file transfers
  1307  *
  1308  * @param inType A string content type
  1309  * @param inContact The destination contact, or nil to check global availability
  1310  */
  1311 - (BOOL)availableForSendingContentType:(NSString *)inType toContact:(AIListContact *)inContact
  1312 {
  1313     if (self.online && [inType isEqualToString:CONTENT_FILE_TRANSFER_TYPE]) {
  1314 		if (inContact) {
  1315 			return ([self conformsToProtocol:@protocol(AIAccount_Files)] &&
  1316 					((inContact.online || inContact.isStranger) && [self allowFileTransferWithListObject:inContact]));
  1317 		} else {
  1318 			return [self conformsToProtocol:@protocol(AIAccount_Files)];
  1319 		}
  1320 	}
  1321 
  1322     return [super availableForSendingContentType:inType toContact:inContact];
  1323 }
  1324 
  1325 - (BOOL)allowFileTransferWithListObject:(AIListObject *)inListObject
  1326 {
  1327 	PurplePluginProtocolInfo *prpl_info = self.protocolInfo;
  1328 
  1329 	if (prpl_info && prpl_info->send_file)
  1330 		return (!prpl_info->can_receive_file || prpl_info->can_receive_file(purple_account_get_connection(account), [inListObject.UID UTF8String]));
  1331 	else
  1332 		return NO;
  1333 }
  1334 
  1335 - (BOOL)supportsAutoReplies
  1336 {
  1337 	if (account && purple_account_get_connection(account)) {
  1338 		return ((purple_account_get_connection(account)->flags & PURPLE_CONNECTION_AUTO_RESP) != 0);
  1339 	}
  1340 	
  1341 	return NO;
  1342 }
  1343 
  1344 - (BOOL)canSendOfflineMessageToContact:(AIListContact *)inContact
  1345 {
  1346 	PurplePluginProtocolInfo *prpl_info = self.protocolInfo;
  1347 
  1348 	if (prpl_info && prpl_info->offline_message) {
  1349 		
  1350 		return (prpl_info->offline_message(purple_find_buddy(account, [inContact.UID UTF8String])));
  1351 
  1352 	} else
  1353 		return NO;
  1354 	
  1355 }
  1356 
  1357 #pragma mark Custom emoticons
  1358 - (void)chat:(AIChat *)inChat isWaitingOnCustomEmoticon:(NSString *)emoticonEquivalent
  1359 {
  1360 	AIEmoticon *emoticon;
  1361 
  1362 	//Look for an existing emoticon with this equivalent
  1363 	for (emoticon in inChat.customEmoticons) {
  1364 		if ([[emoticon textEquivalents] containsObject:emoticonEquivalent]) break;
  1365 	}
  1366 	
  1367 	if (!emoticon) {
  1368 		emoticon = [AIEmoticon emoticonWithIconPath:nil
  1369 										equivalents:[NSArray arrayWithObject:emoticonEquivalent]
  1370 											   name:emoticonEquivalent
  1371 											   pack:nil];
  1372 		[inChat addCustomEmoticon:emoticon];			
  1373 	}
  1374 	
  1375 	if (![emoticon path]) {
  1376 		[emoticon setPath:[[NSBundle bundleForClass:[CBPurpleAccount class]] pathForResource:@"missing_image"
  1377 																					ofType:@"png"]];
  1378 	}
  1379 }
  1380 
  1381 /*!
  1382  * @brief Return the path at which to save an emoticon
  1383  */
  1384 - (NSString *)_emoticonCachePathForEmoticon:(NSString *)emoticonEquivalent type:(AIBitmapImageFileType)fileType inChat:(AIChat *)inChat
  1385 {
  1386 	static unsigned long long emoticonID = 0;
  1387     NSString    *filename = [NSString stringWithFormat:@"TEMP-CustomEmoticon_%@_%@_%qu.%@",
  1388 		[inChat uniqueChatID], emoticonEquivalent, emoticonID++, [NSImage extensionForBitmapImageFileType:fileType]];
  1389     return [[adium cachesPath] stringByAppendingPathComponent:[filename safeFilenameString]];	
  1390 }
  1391 
  1392 
  1393 - (void)chat:(AIChat *)inChat setCustomEmoticon:(NSString *)emoticonEquivalent withImageData:(NSData *)inImageData
  1394 {
  1395 	/* XXX Note: If we can set outgoing emoticons, this method needs to be updated to mark emoticons as incoming
  1396 	 * and AIEmoticonController needs to be able to handle that.
  1397 	 */
  1398 	AIEmoticon	*emoticon;
  1399 
  1400 	//Look for an existing emoticon with this equivalent
  1401 	for (emoticon in inChat.customEmoticons) {
  1402 		if ([[emoticon textEquivalents] containsObject:emoticonEquivalent]) break;
  1403 	}
  1404 	
  1405 	//Write out our image
  1406 	NSString	*path = [self _emoticonCachePathForEmoticon:emoticonEquivalent
  1407 													   type:[NSImage fileTypeOfData:inImageData]
  1408 													 inChat:inChat];
  1409 	[inImageData writeToFile:path
  1410 				  atomically:NO];
  1411 
  1412 	if (emoticon) {
  1413 		//If we already have an emoticon, just update its path
  1414 		[emoticon setPath:path];
  1415 
  1416 	} else {
  1417 		emoticon = [AIEmoticon emoticonWithIconPath:path
  1418 										equivalents:[NSArray arrayWithObject:emoticonEquivalent]
  1419 											   name:emoticonEquivalent
  1420 											   pack:nil];
  1421 		[inChat addCustomEmoticon:emoticon];
  1422 	}
  1423 }
  1424 
  1425 - (void)chat:(AIChat *)inChat closedCustomEmoticon:(NSString *)emoticonEquivalent
  1426 {
  1427 	AIEmoticon	*emoticon;
  1428 
  1429 	//Look for an existing emoticon with this equivalent
  1430 	for (emoticon in inChat.customEmoticons) {
  1431 		if ([[emoticon textEquivalents] containsObject:emoticonEquivalent]) break;
  1432 	}
  1433 	
  1434 	if (emoticon) {
  1435 		[[NSNotificationCenter defaultCenter] postNotificationName:@"AICustomEmoticonUpdated"
  1436 												  object:inChat
  1437 												userInfo:[NSDictionary dictionaryWithObject:emoticon
  1438 																					 forKey:@"AIEmoticon"]];
  1439 	} else {
  1440 		//This shouldn't happen; chat:setCustomEmoticon:withImageData: should have already been called.
  1441 		emoticon = [AIEmoticon emoticonWithIconPath:nil
  1442 										equivalents:[NSArray arrayWithObject:emoticonEquivalent]
  1443 											   name:emoticonEquivalent
  1444 											   pack:nil];
  1445 		NSLog(@"Warning: closed custom emoticon %@ without adding it to the chat", emoticon);
  1446 		AILog(@"Warning: closed custom emoticon %@ without adding it to the chat", emoticon);
  1447 	}
  1448 }
  1449 
  1450 /*********************/
  1451 /* AIAccount_Privacy */
  1452 /*********************/
  1453 #pragma mark Privacy
  1454 - (BOOL)addListObject:(AIListObject *)inObject toPrivacyList:(AIPrivacyType)type
  1455 {
  1456     if (type == AIPrivacyTypePermit)
  1457         return (purple_privacy_permit_add(account,[inObject.UID UTF8String],FALSE));
  1458     else
  1459         return (purple_privacy_deny_add(account,[inObject.UID UTF8String],FALSE));
  1460 }
  1461 
  1462 - (BOOL)removeListObject:(AIListObject *)inObject fromPrivacyList:(AIPrivacyType)type
  1463 {
  1464     if (type == AIPrivacyTypePermit)
  1465         return (purple_privacy_permit_remove(account,[inObject.UID UTF8String],FALSE));
  1466     else
  1467         return (purple_privacy_deny_remove(account,[inObject.UID UTF8String],FALSE));
  1468 }
  1469 
  1470 - (NSArray *)listObjectsOnPrivacyList:(AIPrivacyType)type
  1471 {
  1472 	NSMutableArray	*array = [NSMutableArray array];
  1473 	if (account) {
  1474 		GSList			*list;
  1475 		GSList			*sourceList = ((type == AIPrivacyTypePermit) ? account->permit : account->deny);
  1476 		
  1477 		for (list = sourceList; (list != NULL); list=list->next) {
  1478 			[array addObject:[self contactWithUID:[NSString stringWithUTF8String:(char *)list->data]]];
  1479 		}
  1480 	}
  1481 
  1482 	return array;
  1483 }
  1484 
  1485 - (void)accountPrivacyList:(AIPrivacyType)type added:(NSString *)sourceUID
  1486 {
  1487 	//Can't really trust sourceUID to not be @"" or something silly like that
  1488 	if ([sourceUID length]) {
  1489 		//Get our contact
  1490 		AIListContact   *contact = [self contactWithUID:sourceUID];
  1491 
  1492 		//Update Adium's knowledge of it
  1493 		[contact setIsBlocked:((type == AIPrivacyTypeDeny) ? YES : NO) updateList:NO];
  1494 	}
  1495 }
  1496 
  1497 - (void)privacyPermitListAdded:(NSString *)sourceUID
  1498 {
  1499 	[self accountPrivacyList:AIPrivacyTypePermit added:sourceUID];
  1500 }
  1501 
  1502 - (void)privacyDenyListAdded:(NSString *)sourceUID
  1503 {
  1504 	[self accountPrivacyList:AIPrivacyTypeDeny added:sourceUID];
  1505 }
  1506 
  1507 - (void)accountPrivacyList:(AIPrivacyType)type removed:(NSString *)sourceUID
  1508 {
  1509 	//Can't really trust sourceUID to not be @"" or something silly like that
  1510 	if ([sourceUID length]) {
  1511 		if (!namesAreCaseSensitive) {
  1512 			sourceUID = [sourceUID compactedString];
  1513 		}
  1514 
  1515 		//Get our contact, which must already exist for us to care about its removal
  1516 		AIListContact   *contact = [adium.contactController existingContactWithService:service
  1517 																				 account:self
  1518 																					 UID:sourceUID];
  1519 		
  1520 		if (contact) {			
  1521 			//Update Adium's knowledge of it
  1522 			[contact setIsBlocked:((type == AIPrivacyTypeDeny) ? NO : YES) updateList:NO];
  1523 		}
  1524 	}
  1525 }
  1526 
  1527 - (void)privacyPermitListRemoved:(NSString *)sourceUID
  1528 {
  1529 	[self accountPrivacyList:AIPrivacyTypePermit removed:sourceUID];
  1530 }
  1531 
  1532 - (void)privacyDenyListRemoved:(NSString *)sourceUID
  1533 {
  1534 	[self accountPrivacyList:AIPrivacyTypeDeny removed:sourceUID];
  1535 }
  1536 
  1537 - (void)setPrivacyOptions:(AIPrivacyOption)option
  1538 {
  1539 	if (account && purple_account_get_connection(account)) {
  1540 		PurplePrivacyType privacyType;
  1541 
  1542 		switch (option) {
  1543 			case AIPrivacyOptionAllowAll:
  1544 			default:
  1545 				privacyType = PURPLE_PRIVACY_ALLOW_ALL;
  1546 				break;
  1547 			case AIPrivacyOptionDenyAll:
  1548 				privacyType = PURPLE_PRIVACY_DENY_ALL;
  1549 				break;
  1550 			case AIPrivacyOptionAllowUsers:
  1551 				privacyType = PURPLE_PRIVACY_ALLOW_USERS;
  1552 				break;
  1553 			case AIPrivacyOptionDenyUsers:
  1554 				privacyType = PURPLE_PRIVACY_DENY_USERS;
  1555 				break;
  1556 			case AIPrivacyOptionAllowContactList:
  1557 				privacyType = PURPLE_PRIVACY_ALLOW_BUDDYLIST;
  1558 				break;
  1559 			
  1560 		}
  1561 		
  1562 		if (account->perm_deny != privacyType) {
  1563 			account->perm_deny = privacyType;
  1564 			serv_set_permit_deny(purple_account_get_connection(account));
  1565 			AILog(@"Set privacy options for %@ (%x %x) to %i",
  1566 				  self,account,purple_account_get_connection(account),account->perm_deny);
  1567 
  1568 			[self setPreference:[NSNumber numberWithInteger:option]
  1569 						 forKey:KEY_PRIVACY_OPTION
  1570 						  group:GROUP_ACCOUNT_STATUS];			
  1571 		}
  1572 	} else {
  1573 		AILog(@"Couldn't set privacy options for %@ (%x %x)",self,account,purple_account_get_connection(account));
  1574 	}
  1575 }
  1576 
  1577 - (AIPrivacyOption)privacyOptions
  1578 {
  1579 	AIPrivacyOption privacyOption = -1;
  1580 	
  1581 	if (account) {
  1582 		PurplePrivacyType privacyType = account->perm_deny;
  1583 		
  1584 		switch (privacyType) {
  1585 			case PURPLE_PRIVACY_ALLOW_ALL:
  1586 			default:
  1587 				privacyOption = AIPrivacyOptionAllowAll;
  1588 				break;
  1589 			case PURPLE_PRIVACY_DENY_ALL:
  1590 				privacyOption = AIPrivacyOptionDenyAll;
  1591 				break;
  1592 			case PURPLE_PRIVACY_ALLOW_USERS:
  1593 				privacyOption = AIPrivacyOptionAllowUsers;
  1594 				break;
  1595 			case PURPLE_PRIVACY_DENY_USERS:
  1596 				privacyOption = AIPrivacyOptionDenyUsers;
  1597 				break;
  1598 			case PURPLE_PRIVACY_ALLOW_BUDDYLIST:
  1599 				privacyOption = AIPrivacyOptionAllowContactList;
  1600 				break;
  1601 		}
  1602 	}
  1603 	AILog(@"%@: privacyOptions are %i",self,privacyOption);
  1604 	return privacyOption;
  1605 }
  1606 
  1607 /*****************************************************/
  1608 /* File transfer / AIAccount_Files inherited methods */
  1609 /*****************************************************/
  1610 #pragma mark File Transfer
  1611 - (BOOL)canSendFolders
  1612 {
  1613 	return NO;
  1614 }
  1615 
  1616 //Create a protocol-specific xfer object, set it up as requested, and begin sending
  1617 - (void)_beginSendOfFileTransfer:(ESFileTransfer *)fileTransfer
  1618 {
  1619 	PurpleXfer *xfer = [self newOutgoingXferForFileTransfer:fileTransfer];
  1620 	
  1621 	if (xfer) {
  1622 		//Associate the fileTransfer and the xfer with each other
  1623 		[fileTransfer setAccountData:[NSValue valueWithPointer:xfer]];
  1624 		xfer->ui_data = [fileTransfer retain];
  1625 		
  1626 		//Set the filename
  1627 		purple_xfer_set_local_filename(xfer, [[fileTransfer localFilename] UTF8String]);
  1628 		purple_xfer_set_filename(xfer, [[[fileTransfer localFilename] lastPathComponent] UTF8String]);
  1629 		
  1630 		/*
  1631 		 Request that the transfer begins.
  1632 		 We will be asked to accept it via:
  1633 			- (void)acceptFileTransferRequest:(ESFileTransfer *)fileTransfer
  1634 		 below.
  1635 		 */
  1636 		[purpleAdapter xferRequest:xfer];
  1637 		[fileTransfer setStatus: Waiting_on_Remote_User_FileTransfer];
  1638 	}
  1639 }
  1640 //By default, protocols can not create PurpleXfer objects
  1641 - (PurpleXfer *)newOutgoingXferForFileTransfer:(ESFileTransfer *)fileTransfer
  1642 {
  1643 	PurpleXfer				*newPurpleXfer = NULL;
  1644 
  1645 	if (account && purple_account_get_connection(account)) {
  1646 		PurplePluginProtocolInfo  *prpl_info = self.protocolInfo;
  1647 
  1648 		if (prpl_info && prpl_info->new_xfer) {
  1649 			char *destsn = (char *)[[[fileTransfer contact] UID] UTF8String];
  1650 			newPurpleXfer = (prpl_info->new_xfer)(purple_account_get_connection(account), destsn);
  1651 		}
  1652 	}
  1653 
  1654 	return newPurpleXfer;
  1655 }
  1656 
  1657 /* 
  1658  * @brief The account requested that we received a file.
  1659  *
  1660  * Set up the ESFileTransfer and query the fileTransferController for a save location.
  1661  * 
  1662  */
  1663 - (void)requestReceiveOfFileTransfer:(ESFileTransfer *)fileTransfer
  1664 {
  1665 	AILog(@"File transfer request received: %@",fileTransfer);
  1666 	[adium.fileTransferController receiveRequestForFileTransfer:fileTransfer];
  1667 }
  1668 
  1669 //Create an ESFileTransfer object from an xfer
  1670 - (ESFileTransfer *)newFileTransferObjectWith:(NSString *)destinationUID
  1671 										 size:(unsigned long long)inSize
  1672 							   remoteFilename:(NSString *)remoteFilename
  1673 {
  1674 	AIListContact   *contact = [self contactWithUID:destinationUID];
  1675     ESFileTransfer	*fileTransfer;
  1676 	
  1677 	fileTransfer = [adium.fileTransferController newFileTransferWithContact:contact
  1678 																   forAccount:self
  1679 																		 type:Unknown_FileTransfer]; 
  1680 	[fileTransfer setSize:inSize];
  1681 	[fileTransfer setRemoteFilename:remoteFilename];
  1682 	
  1683     return fileTransfer;
  1684 }
  1685 
  1686 //Update an ESFileTransfer object progress
  1687 - (void)updateProgressForFileTransfer:(ESFileTransfer *)fileTransfer percent:(NSNumber *)percent bytesSent:(NSNumber *)bytesSent
  1688 {
  1689 	CGFloat percentDone = [percent doubleValue];
  1690     [fileTransfer setPercentDone:percentDone bytesSent:[bytesSent unsignedLongValue]];
  1691 }
  1692 
  1693 //The local side cancelled the transfer.  We probably already have this status set, but set it just in case.
  1694 - (void)fileTransferCancelledLocally:(ESFileTransfer *)fileTransfer
  1695 {
  1696 	if (![fileTransfer isStopped]) {
  1697 		[fileTransfer setStatus:Cancelled_Local_FileTransfer];
  1698 	}
  1699 }
  1700 
  1701 //The remote side cancelled the transfer, the fool. Update our status.
  1702 - (void)fileTransferCancelledRemotely:(ESFileTransfer *)fileTransfer
  1703 {
  1704 	if (![fileTransfer isStopped]) {
  1705 		[fileTransfer setStatus:Cancelled_Remote_FileTransfer];
  1706 	}
  1707 }
  1708 
  1709 - (void)destroyFileTransfer:(ESFileTransfer *)fileTransfer
  1710 {
  1711 	AILog(@"Destroy file transfer %@",fileTransfer);
  1712 	[fileTransfer release];
  1713 }
  1714 
  1715 //Accept a send or receive ESFileTransfer object, beginning the transfer.
  1716 //Subsequently inform the fileTransferController that the fun has begun.
  1717 - (void)acceptFileTransferRequest:(ESFileTransfer *)fileTransfer
  1718 {
  1719     AILog(@"Accepted file transfer %@",fileTransfer);
  1720 	
  1721 	PurpleXfer		*xfer;
  1722 	PurpleXferType	xferType;
  1723 	
  1724 	xfer = [[fileTransfer accountData] pointerValue];
  1725 
  1726     xferType = purple_xfer_get_type(xfer);
  1727     if (xferType == PURPLE_XFER_SEND) {
  1728         [fileTransfer setFileTransferType:Outgoing_FileTransfer];
  1729 
  1730     } else if (xferType == PURPLE_XFER_RECEIVE) {
  1731         [fileTransfer setFileTransferType:Incoming_FileTransfer];
  1732 		[fileTransfer setSize:purple_xfer_get_size(xfer)];
  1733     }
  1734     
  1735     //accept the request
  1736 	[purpleAdapter xferRequestAccepted:xfer withFileName:[fileTransfer localFilename]];
  1737 	
  1738 	[fileTransfer setStatus:Accepted_FileTransfer];
  1739 }
  1740 
  1741 //User refused a receive request.  Tell purple; we don't release the ESFileTransfer object
  1742 //since that will happen when the xfer is destroyed.  This will end up calling back on
  1743 //- (void)fileTransfercancelledLocally:(ESFileTransfer *)fileTransfer
  1744 - (void)rejectFileReceiveRequest:(ESFileTransfer *)fileTransfer
  1745 {
  1746 	PurpleXfer	*xfer = [[fileTransfer accountData] pointerValue];
  1747 	if (xfer) {
  1748 		[purpleAdapter xferRequestRejected:xfer];
  1749 	}
  1750 }
  1751 
  1752 //Cancel a file transfer in progress.  Tell purple; we don't release the ESFileTransfer object
  1753 //since that will happen when the xfer is destroyed.  This will end up calling back on
  1754 //- (void)fileTransfercancelledLocally:(ESFileTransfer *)fileTransfer
  1755 - (void)cancelFileTransfer:(ESFileTransfer *)fileTransfer
  1756 {
  1757 	PurpleXfer	*xfer = [[fileTransfer accountData] pointerValue];
  1758 	if (xfer) {
  1759 		[purpleAdapter xferCancel:xfer];
  1760 	}	
  1761 }
  1762 
  1763 //Account Connectivity -------------------------------------------------------------------------------------------------
  1764 #pragma mark Connect
  1765 //Connect this account (Our password should be in the instance variable 'password' all ready for us)
  1766 - (void)connect
  1767 {
  1768 	finishedConnectProcess = NO;
  1769 
  1770 	[super connect];
  1771 
  1772 	//Ensure we have a purple account if one does not already exist
  1773 	[self purpleAccount];
  1774 	
  1775 	//Make sure our settings are correct
  1776 	if ([self connectivityBasedOnNetworkReachability] &&
  1777 		![self.host length]) {
  1778 		//If we use the network for connectivity, and we don't have a host, we need to get ourselves one. Prompt for it!
  1779 		[self promptForHostBeforeConnecting];
  1780 	} else {
  1781 		[self configurePurpleAccountNotifyingTarget:self selector:@selector(continueConnectWithConfiguredPurpleAccount)];
  1782 	}
  1783 }
  1784 
  1785 - (void)unregister
  1786 {
  1787 	finishedConnectProcess = NO;
  1788 
  1789 	[purpleAdapter unregisterAccount:self];
  1790 }
  1791 
  1792 static void prompt_host_cancel_cb(CBPurpleAccount *self) {
  1793 	[self disconnect];
  1794 }
  1795 
  1796 
  1797 static void prompt_host_ok_cb(CBPurpleAccount *self, const char *host) {
  1798 	if(host && *host) {
  1799 		[self setPreference:[NSString stringWithUTF8String:host]
  1800 					 forKey:KEY_CONNECT_HOST
  1801 					  group:GROUP_ACCOUNT_STATUS];	
  1802 
  1803 		[self configurePurpleAccountNotifyingTarget:self selector:@selector(continueConnectWithConfiguredPurpleAccount)];
  1804 	} else {
  1805 		prompt_host_cancel_cb(self);
  1806 	}
  1807 }
  1808 
  1809 - (void)promptForHostBeforeConnecting
  1810 {
  1811 	purple_request_input(NULL, [[NSString stringWithFormat:AILocalizedString(@"%@ (%@) Setup", "first %@ is an account name; second is a service. This is a title for a window"),
  1812 								self.formattedUID, [self.service shortDescription]] UTF8String],
  1813 						 [AILocalizedString(@"No Server Specified", nil) UTF8String],
  1814 						 [[NSString stringWithFormat:AILocalizedString(@"No server has been configured for the %@ account %@. Please enter one below to connect", nil),
  1815 						   [self.service longDescription], self.formattedUID] UTF8String],
  1816 						 /* default value */ "", /* multiline */ FALSE, /* masked */ FALSE, /* hint */ NULL,
  1817 						 [AILocalizedString(@"Connect", "Button title to connect; this is a verb") UTF8String], G_CALLBACK(prompt_host_ok_cb),
  1818 						 [AILocalizedString(@"Cancel", nil) UTF8String], G_CALLBACK(prompt_host_cancel_cb),
  1819 						 /* account */ NULL, /* who */ NULL, /* conv */ NULL,
  1820 						 self);
  1821 						 
  1822 }
  1823 
  1824 
  1825 - (void)continueConnectWithConfiguredPurpleAccount
  1826 {
  1827 	//Configure libpurple's proxy settings; continueConnectWithConfiguredProxy will be called once we are ready
  1828 	[self configureAccountProxyNotifyingTarget:self selector:@selector(continueConnectWithConfiguredProxy)];
  1829 }
  1830 
  1831 - (void)continueConnectWithConfiguredProxy
  1832 {
  1833 	//Set password and connect
  1834 	purple_account_set_password(account, [password UTF8String]);
  1835 
  1836 	//Set our current status state after filtering its statusMessage as appropriate. This will take us online in the process.
  1837 	AIStatus	*statusState = [self valueForProperty:@"StatusState"];
  1838 	if (!statusState || (statusState.statusType == AIOfflineStatusType)) {
  1839 		statusState = [adium.statusController defaultInitialStatusState];
  1840 	}
  1841 
  1842 	AILog(@"Adium: Connect: %@ initiating connection using status state %@ (%@).",self.UID,statusState,
  1843 			  [statusState statusMessageString]);
  1844 
  1845 	[self autoRefreshingOutgoingContentForStatusKey:@"StatusState"
  1846 										   selector:@selector(gotFilteredStatusMessage:forStatusState:)
  1847 											context:statusState];
  1848 }
  1849 
  1850 //Make sure our settings are correct; notify target/selector when we're finished
  1851 - (void)configurePurpleAccountNotifyingTarget:(id)target selector:(SEL)selector
  1852 {
  1853 	NSInvocation	*contextInvocation;
  1854 	
  1855 	//Perform the synchronous configuration activities (subclasses may want to take action in this function)
  1856 	[self configurePurpleAccount];
  1857 	
  1858 	contextInvocation = [NSInvocation invocationWithMethodSignature:[target methodSignatureForSelector:selector]];
  1859 	
  1860 	[contextInvocation setTarget:target];
  1861 	[contextInvocation setSelector:selector];
  1862 	[contextInvocation retainArguments];
  1863 
  1864 	//Set the text profile BEFORE beginning the connect process, to avoid problems with setting it while the
  1865 	//connect occurs. Once that's done, contextInvocation will be invoked, continuing the configurePurpleAccount process.
  1866 	[self autoRefreshingOutgoingContentForStatusKey:@"TextProfile" 
  1867 										   selector:@selector(setAccountProfileTo:configurePurpleAccountContext:)
  1868 											context:contextInvocation];
  1869 }
  1870 
  1871 /*!
  1872  * @brief The server name to be passed to libpurple
  1873  * By default, this is the host as seen by the rest of Adium.  Subclasses may choose to override this if
  1874  * some trickery is desired between what is told to libpurple and what the rest of Adium sees.
  1875  */
  1876 - (NSString *)hostForPurple
  1877 {
  1878 	return self.host;
  1879 }
  1880 
  1881 //Synchronous purple account configuration activites, always performed after an account is created.
  1882 //This is a definite subclassing point so prpls can apply their own account settings.
  1883 - (void)configurePurpleAccount
  1884 {
  1885 	NSString	*hostName;
  1886 	NSInteger			portNumber;
  1887 
  1888 	//Host (server)
  1889 	hostName = [self hostForPurple];
  1890 	if (hostName && [hostName length]) {
  1891 		purple_account_set_string(account, "server", [hostName UTF8String]);
  1892 	}
  1893 	
  1894 	//Port
  1895 	portNumber = [self port];
  1896 	if (portNumber) {
  1897 		purple_account_set_int(account, "port", portNumber);
  1898 	}
  1899 	
  1900 	//E-mail checking
  1901 	purple_account_set_check_mail(account, [[self shouldCheckMail] boolValue]);
  1902 	
  1903 	//Custom Emoticons
  1904 	BOOL customEmoticons = [[self preferenceForKey:KEY_DISPLAY_CUSTOM_EMOTICONS group:GROUP_ACCOUNT_STATUS] boolValue];
  1905 	purple_account_set_bool(account, "custom_smileys", customEmoticons);
  1906 	
  1907 	//Update a few properties before we begin connecting.  Libpurple will send these automatically
  1908     [self updateStatusForKey:KEY_USER_ICON];
  1909 }
  1910 
  1911 /*!
  1912  * @brief Configure libpurple's proxy settings using the current system values
  1913  *
  1914  * target/selector are used rather than a hardcoded callback (or getProxyConfigurationNotifyingTarget: directly) because this allows code reuse
  1915  * between the connect and register processes, which are similar in their need for proxy configuration
  1916  */
  1917 - (void)configureAccountProxyNotifyingTarget:(id)target selector:(SEL)selector
  1918 {
  1919 	NSInvocation		*invocation; 
  1920 
  1921 	//Configure the invocation we will use when we are done configuring
  1922 	invocation = [NSInvocation invocationWithMethodSignature:[target methodSignatureForSelector:selector]];
  1923 	[invocation setSelector:selector];
  1924 	[invocation setTarget:target];
  1925 	
  1926 	[self getProxyConfigurationNotifyingTarget:self
  1927 									  selector:@selector(retrievedProxyConfiguration:context:)
  1928 									   context:invocation];
  1929 }
  1930 
  1931 /*!
  1932  * @brief Callback for -[self getProxyConfigurationNotifyingTarget:selector:context:]
  1933  */
  1934 - (void)retrievedProxyConfiguration:(NSDictionary *)proxyConfig context:(NSInvocation *)invocation
  1935 {
  1936 	PurpleProxyInfo		*proxy_info;
  1937 	
  1938 	AdiumProxyType  	proxyType = [[proxyConfig objectForKey:@"AdiumProxyType"] integerValue];
  1939 	
  1940 	proxy_info = purple_proxy_info_new();
  1941 	purple_account_set_proxy_info(account, proxy_info);
  1942 
  1943 	PurpleProxyType		purpleAccountProxyType;
  1944 	
  1945 	switch (proxyType) {
  1946 		case Adium_Proxy_HTTP:
  1947 		case Adium_Proxy_Default_HTTP:
  1948 			purpleAccountProxyType = PURPLE_PROXY_HTTP;
  1949 			break;
  1950 		case Adium_Proxy_SOCKS4:
  1951 		case Adium_Proxy_Default_SOCKS4:
  1952 			purpleAccountProxyType = PURPLE_PROXY_SOCKS4;
  1953 			break;
  1954 		case Adium_Proxy_SOCKS5:
  1955 		case Adium_Proxy_Default_SOCKS5:
  1956 			purpleAccountProxyType = PURPLE_PROXY_SOCKS5;
  1957 			break;
  1958 		case Adium_Proxy_None:
  1959 		default:
  1960 			purpleAccountProxyType = PURPLE_PROXY_NONE;
  1961 			break;
  1962 	}
  1963 	
  1964 	purple_proxy_info_set_type(proxy_info, purpleAccountProxyType);
  1965 
  1966 	if (proxyType != Adium_Proxy_None) {
  1967 		purple_proxy_info_set_host(proxy_info, (char *)[[proxyConfig objectForKey:@"Host"] UTF8String]);
  1968 		purple_proxy_info_set_port(proxy_info, [[proxyConfig objectForKey:@"Port"] integerValue]);
  1969 
  1970 		purple_proxy_info_set_username(proxy_info, (char *)[[proxyConfig objectForKey:@"Username"] UTF8String]);
  1971 		purple_proxy_info_set_password(proxy_info, (char *)[[proxyConfig objectForKey:@"Password"] UTF8String]);
  1972 		
  1973 		AILog(@"Connecting with proxy type %i and proxy host %@",proxyType, [proxyConfig objectForKey:@"Host"]);
  1974 	}
  1975 
  1976 	[invocation invoke];
  1977 }
  1978 
  1979 //Sublcasses should override to provide a string for each progress step
  1980 - (NSString *)connectionStringForStep:(NSInteger)step { return nil; };
  1981 
  1982 /*!
  1983  * @brief Should the account's status be updated as soon as it is connected?
  1984  *
  1985  * If YES, the StatusState and IdleSince properties will be told to update as soon as the account connects.
  1986  * This will allow the account to send its status information to the server upon connecting.
  1987  *
  1988  * If this information is already known by the account at the time it connects and further prompting to send it is
  1989  * not desired, return NO.
  1990  *
  1991  * libpurple should already have been told of our status before connecting began.
  1992  */
  1993 - (BOOL)updateStatusImmediatelyAfterConnecting
  1994 {
  1995 	return NO;
  1996 }
  1997 
  1998 - (void)didConnect
  1999 {
  2000 	finishedConnectProcess = YES;
  2001 
  2002 	[super didConnect];
  2003 	
  2004 	[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(iTunesDidUpdate:) name:Adium_iTunesTrackChangedNotification object:nil];
  2005 
  2006 	//Silence updates
  2007 	[self silenceAllContactUpdatesForInterval:18.0];
  2008 	[[AIContactObserverManager sharedManager] delayListObjectNotificationsUntilInactivity];
  2009 	
  2010 	//Clear any previous disconnection error
  2011 	[self setLastDisconnectionError:nil];
  2012 
  2013 	if (unregisterAfterConnecting)
  2014 		[self unregister];
  2015 }
  2016 
  2017 //Our account has connected
  2018 - (void)accountConnectionConnected
  2019 {
  2020 	AILog(@"************ %@ CONNECTED ***********",self.UID);
  2021 	[self didConnect];
  2022 }
  2023 
  2024 - (void)accountConnectionProgressStep:(NSNumber *)step percentDone:(NSNumber *)connectionProgressPrecent
  2025 {
  2026 	NSString	*connectionProgressString = [self connectionStringForStep:[step integerValue]];
  2027 
  2028 	[self setValue:connectionProgressString forProperty:@"ConnectionProgressString" notify:NO];
  2029 	[self setValue:connectionProgressPrecent forProperty:@"ConnectionProgressPercent" notify:NO];	
  2030 
  2031 	//Apply any changes
  2032 	[self notifyOfChangedPropertiesSilently:NO];
  2033 	
  2034 	AILog(@"************ %@ --step-- %i",self.UID,[step integerValue]);
  2035 }
  2036 
  2037 /*!
  2038  * @brief Name to use when creating a PurpleAccount for this CBPurpleAccount
  2039  *
  2040  * By default, we just use the formattedUID.  Subclasses can override this to provide other handling,
  2041  * such as appending \@mac.com if necessary for dotMac accounts.
  2042  */
  2043 - (const char *)purpleAccountName
  2044 {
  2045 	return [self.formattedUID UTF8String];
  2046 }
  2047 
  2048 - (void)setPurpleAccount:(PurpleAccount *)inAccount
  2049 {
  2050 	account = inAccount;
  2051 }
  2052 
  2053 - (void)createNewPurpleAccount
  2054 {
  2055 	//Ensure libpurple is loaded and initialized
  2056 	[self purpleAdapter];
  2057 	
  2058 	//If loading libpurple didn't set an account for us, tell it to create one
  2059 	if (!account)
  2060 		[[self purpleAdapter] addAdiumAccount:self];
  2061 
  2062 	//-[SLPurpleCocoaAdapter addAdiumAccount:] should have immediately called back on setPurpleAccount. It's bad if it didn't.
  2063 	if (account) {
  2064 		AILog(@"Created PurpleAccount 0x%x with UID %@ and protocolPlugin %s", account, self.UID, [self protocolPlugin]);
  2065 	} else {
  2066 		AILog(@"Unable to create Libpurple account with name %s and protocol plugin %s",
  2067 			  self.purpleAccountName, [self protocolPlugin]);
  2068 		NSLog(@"Unable to create Libpurple account with name %s and protocol plugin %s",
  2069 			  self.purpleAccountName, [self protocolPlugin]);
  2070 	}
  2071 }
  2072 
  2073 /*!
  2074  * @brief Returns a PurpleSslConnection for a given account.
  2075  */
  2076 - (PurpleSslConnection *)secureConnection
  2077 {
  2078 	return NULL;
  2079 }
  2080 
  2081 #pragma mark Disconnect
  2082 
  2083 /*!
  2084  * @brief Disconnect this account
  2085  */
  2086 - (void)disconnect
  2087 {
  2088 	if (self.online || [self boolValueForProperty:@"Connecting"]) {
  2089 		//As per AIAccount's documentation, call super's implementation
  2090 		[super disconnect];
  2091 
  2092 		[[AIContactObserverManager sharedManager] delayListObjectNotificationsUntilInactivity];
  2093 
  2094 		//Tell libpurple to disconnect
  2095 		[purpleAdapter disconnectAccount:self];
  2096 	}
  2097 }
  2098 
  2099 - (void)setLastDisconnectionReason:(PurpleConnectionError)reason
  2100 {
  2101 	lastDisconnectionReason = reason;
  2102 }
  2103 
  2104 - (PurpleConnectionError)lastDisconnectionReason
  2105 {
  2106 	return lastDisconnectionReason;
  2107 }
  2108 
  2109 /*!
  2110  * @brief Our account was unexpectedly disconnected with an error message
  2111  */
  2112 - (void)accountConnectionReportDisconnect:(NSString *)text withReason:(PurpleConnectionError)reason
  2113 {
  2114 	[self setLastDisconnectionError:text];
  2115 	[self setLastDisconnectionReason:reason];
  2116 
  2117 	if (reason == PURPLE_CONNECTION_ERROR_AUTHENTICATION_FAILED)
  2118 		[self serverReportedInvalidPassword];
  2119 
  2120 	//We are disconnecting
  2121     [self setValue:[NSNumber numberWithBool:YES] forProperty:@"Disconnecting" notify:NotifyNow];
  2122 	
  2123 	AILog(@"%@ accountConnectionReportDisconnect: %@",self,lastDisconnectionError);
  2124 }
  2125 
  2126 - (void)accountConnectionNotice:(NSString *)connectionNotice
  2127 {
  2128     [adium.interfaceController handleErrorMessage:[NSString stringWithFormat:AILocalizedString(@"%@ (%@) : Connection Notice",nil),self.formattedUID,[service description]]
  2129                                     withDescription:connectionNotice];
  2130 }
  2131 
  2132 - (void)didDisconnect
  2133 {
  2134 	//Clear properties which don't make sense for a disconnected account
  2135 	[self setValue:nil forProperty:@"TextProfile" notify:NO];
  2136 	
  2137 	//Apply any changes
  2138 	[self notifyOfChangedPropertiesSilently:NO];
  2139 	
  2140 	[[NSNotificationCenter defaultCenter] removeObserver:self
  2141 										  name:Adium_iTunesTrackChangedNotification
  2142 										object:nil];
  2143 	[tuneinfo release];
  2144 	tuneinfo = nil;
  2145 	
  2146 	if (deletePurpleAccountAfterDisconnecting) {
  2147 		deletePurpleAccountAfterDisconnecting = FALSE;
  2148 
  2149 		[[self purpleAdapter] removeAdiumAccount:self];
  2150 	}
  2151 
  2152 	[super didDisconnect];
  2153 }
  2154 /*!
  2155  * @brief Our account has disconnected
  2156  *
  2157  * This is called after the account disconnects for any reason
  2158  */
  2159 - (void)accountConnectionDisconnected
  2160 {
  2161 	//Report that we disconnected
  2162 	AILog(@"%@: Telling the core we disconnected", self);
  2163 	[self didDisconnect];
  2164 }
  2165 
  2166 - (AIReconnectDelayType)shouldAttemptReconnectAfterDisconnectionError:(NSString **)disconnectionError
  2167 {
  2168 	AIReconnectDelayType reconnectDelayType;
  2169 
  2170 	if ([self lastDisconnectionReason] == PURPLE_CONNECTION_ERROR_AUTHENTICATION_FAILED) {
  2171 		[self setLastDisconnectionError:AILocalizedString(@"Incorrect username or password","Error message displayed when the server reports username or password as being incorrect.")];
  2172 		reconnectDelayType = AIReconnectImmediately;
  2173 
  2174 	} else if ([self lastDisconnectionReason] == PURPLE_CONNECTION_ERROR_INVALID_USERNAME) {
  2175 		[self setLastDisconnectionError:AILocalizedString(@"The name you entered is not registered. Check to ensure you typed it correctly.", nil)];
  2176 		reconnectDelayType = AIReconnectNever;
  2177 
  2178 	} else if (disconnectionError && ([*disconnectionError isEqualToString:[NSString stringWithUTF8String:_("SSL Handshake Failed")]] ||
  2179 									  [*disconnectionError isEqualToString:[NSString stringWithUTF8String:_("SSL Connection Failed")]])) {
  2180 		/* This particular message comes with PURPLE_CONNECTION_ERROR_ENCRYPTION_ERROR, which is a 'fatal' error according to libpurple. Other problems
  2181 		 * with that message may be fatal, but this one isn't.
  2182 		 */
  2183 		reconnectDelayType = AIReconnectNormally;
  2184 
  2185 	} else if (purple_connection_error_is_fatal([self lastDisconnectionReason])) {
  2186 		reconnectDelayType = AIReconnectNever;
  2187 
  2188 	} else {
  2189 		reconnectDelayType = AIReconnectNormally;
  2190 	}
  2191 
  2192 	return reconnectDelayType;
  2193 }
  2194 
  2195 #pragma mark Registering
  2196 - (void)performRegisterWithPassword:(NSString *)inPassword
  2197 {
  2198 	//Save the new password
  2199 	if (inPassword && ![password isEqualToString:inPassword]) {
  2200 		[password release]; password = [inPassword retain];
  2201 	}
  2202 
  2203 	//Ensure we have a purple account if one does not already exist
  2204 	[self purpleAccount];
  2205 	
  2206 	//We are connecting
  2207 	[self setValue:[NSNumber numberWithBool:YES] forProperty:@"Connecting" notify:NotifyNow];
  2208 	
  2209 	//Make sure our settings are correct
  2210 	[self configurePurpleAccountNotifyingTarget:self selector:@selector(continueRegisterWithConfiguredPurpleAccount)];
  2211 }
  2212 
  2213 - (void)continueRegisterWithConfiguredProxy
  2214 {
  2215 	//Set password and connect
  2216 	purple_account_set_password(account, [password UTF8String]);
  2217 	
  2218 	AILog(@"Adium: Register: %@ initiating connection.",self.UID);
  2219 	
  2220 	[purpleAdapter registerAccount:self];
  2221 }
  2222 
  2223 - (void)continueRegisterWithConfiguredPurpleAccount
  2224 {
  2225 	//Configure libpurple's proxy settings; continueConnectWithConfiguredProxy will be called once we are ready
  2226 	[self configureAccountProxyNotifyingTarget:self selector:@selector(continueRegisterWithConfiguredProxy)];
  2227 }
  2228 
  2229 - (void)purpleAccountRegistered:(BOOL)success
  2230 {
  2231 	if (success && [self.service accountViewController]) {
  2232 		NSString *username = (purple_account_get_username(account) ? [NSString stringWithUTF8String:purple_account_get_username(account)] : [NSNull null]);
  2233 		NSString *pw = (purple_account_get_password(account) ? [NSString stringWithUTF8String:purple_account_get_password(account)] : [NSNull null]);
  2234 
  2235 		[[NSNotificationCenter defaultCenter] postNotificationName:AIAccountUsernameAndPasswordRegisteredNotification
  2236 												  object:self
  2237 												userInfo:[NSDictionary dictionaryWithObjectsAndKeys:
  2238 													username, @"username",
  2239 													pw, @"password",
  2240 													nil]];
  2241 	}
  2242 }
  2243 
  2244 //Account Status ------------------------------------------------------------------------------------------------------
  2245 #pragma mark Account Status
  2246 //Properties this account supports
  2247 - (NSSet *)supportedPropertyKeys
  2248 {
  2249 	static NSMutableSet *supportedPropertyKeys = nil;
  2250 	
  2251 	if (!supportedPropertyKeys) {
  2252 		supportedPropertyKeys = [[NSMutableSet alloc] initWithObjects:
  2253 			@"IdleSince",
  2254 			@"IdleManuallySet",
  2255 			@"TextProfile",
  2256 			@"DefaultUserIconFilename",
  2257 			KEY_ACCOUNT_CHECK_MAIL,
  2258 			nil];
  2259 		[supportedPropertyKeys unionSet:[super supportedPropertyKeys]];
  2260 		
  2261 	}
  2262 
  2263 	return supportedPropertyKeys;
  2264 }
  2265 
  2266 //Update our status
  2267 - (void)updateStatusForKey:(NSString *)key
  2268 {    
  2269 	[super updateStatusForKey:key];
  2270 	
  2271     //Now look at keys which only make sense if we have an account
  2272 	if (account) {
  2273 		AILog(@"%@: Updating status for key: %@",self, key);
  2274 
  2275 		if ([key isEqualToString:@"IdleSince"]) {
  2276 			NSDate	*idleSince = [self preferenceForKey:@"IdleSince" group:GROUP_ACCOUNT_STATUS];
  2277 			
  2278 			if (!idleSince) {
  2279 				idleSince = [adium.preferenceController preferenceForKey:@"IdleSince" group:GROUP_ACCOUNT_STATUS];
  2280 			}
  2281 			
  2282 			[self setAccountIdleSinceTo:idleSince];
  2283 							
  2284 		} else if ([key isEqualToString:@"TextProfile"]) {
  2285 			[self autoRefreshingOutgoingContentForStatusKey:key selector:@selector(setAccountProfileTo:) context:nil];
  2286 
  2287 		} else if ([key isEqualToString:KEY_ACCOUNT_CHECK_MAIL]) {
  2288 			//Update the mail checking setting if the account is already made (if it isn't, we'll set it when it is made)
  2289 			if (account) {
  2290 				[purpleAdapter setCheckMail:[self shouldCheckMail]
  2291 							  forAccount:self];
  2292 			}
  2293 		}
  2294 	}
  2295 }
  2296 
  2297 /*!
  2298  * @brief Return the purple status type to be used for a status
  2299  *
  2300  * Most subclasses should override this method; these generic values may be appropriate for others.
  2301  *
  2302  * Active services provided nonlocalized status names.  An AIStatus is passed to this method along with a pointer
  2303  * to the status message.  This method should handle any status whose statusNname this service set as well as any statusName
  2304  * defined in  AIStatusController.h (which will correspond to the services handled by Adium by default).
  2305  * It should also handle a status name not specified in either of these places with a sane default, most likely by loooking at
  2306  * statusState.statusType for a general idea of the status's type.
  2307  *
  2308  * @param statusState The status for which to find the purple status ID
  2309  * @param arguments Prpl-specific arguments which will be passed with the state. Message is handled automatically.
  2310  *
  2311  * @result The purple status ID
  2312  */
  2313 - (const char *)purpleStatusIDForStatus:(AIStatus *)statusState
  2314 							arguments:(NSMutableDictionary *)arguments
  2315 {
  2316 	char	*statusID = NULL;
  2317 	
  2318 	switch (statusState.statusType) {
  2319 		case AIAvailableStatusType:
  2320 			statusID = "available";
  2321 			break;
  2322 		case AIAwayStatusType:
  2323 			statusID = "away";
  2324 			break;
  2325 			
  2326 		case AIInvisibleStatusType:
  2327 			statusID = "invisible";
  2328 			break;
  2329 			
  2330 		case AIOfflineStatusType:
  2331 			statusID = "offline";
  2332 			break;
  2333 	}
  2334 	
  2335 	return statusID;
  2336 }
  2337 
  2338 - (BOOL)shouldAddMusicalNoteToNowPlayingStatus
  2339 {
  2340 	return YES;
  2341 }
  2342 
  2343 - (BOOL)shouldSetITMSLinkForNowPlayingStatus
  2344 {
  2345 	return NO;
  2346 }
  2347 
  2348 - (NSDictionary *)purpleSongInfoDictionary
  2349 {
  2350 	NSMutableDictionary *arguments = nil;
  2351 
  2352 	if (tuneinfo && [[tuneinfo objectForKey:ITUNES_PLAYER_STATE] isEqualToString:@"Playing"]) {
  2353 		arguments = [NSMutableDictionary dictionary];
  2354 		
  2355 		NSString *artist = [tuneinfo objectForKey:ITUNES_ARTIST];
  2356 		NSString *name = [tuneinfo objectForKey:ITUNES_NAME];
  2357 		
  2358 		[arguments setObject:(artist ? artist : @"") forKey:[NSString stringWithUTF8String:PURPLE_TUNE_ARTIST]];
  2359 		[arguments setObject:(name ? name : @"") forKey:[NSString stringWithUTF8String:PURPLE_TUNE_TITLE]];
  2360 		[arguments setObject:([tuneinfo objectForKey:ITUNES_ALBUM] ? [tuneinfo objectForKey:ITUNES_ALBUM] : @"") forKey:[NSString stringWithUTF8String:PURPLE_TUNE_ALBUM]];
  2361 		[arguments setObject:([tuneinfo objectForKey:ITUNES_GENRE] ? [tuneinfo objectForKey:ITUNES_GENRE] : @"") forKey:[NSString stringWithUTF8String:PURPLE_TUNE_GENRE]];
  2362 		[arguments setObject:([tuneinfo objectForKey:ITUNES_TOTAL_TIME] ? [tuneinfo objectForKey:ITUNES_TOTAL_TIME]:[NSNumber numberWithInteger:-1]) forKey:[NSString stringWithUTF8String:PURPLE_TUNE_TIME]];
  2363 		[arguments setObject:([tuneinfo objectForKey:ITUNES_YEAR] ? [tuneinfo objectForKey:ITUNES_YEAR]:[NSNumber numberWithInteger:-1]) forKey:[NSString stringWithUTF8String:PURPLE_TUNE_YEAR]];
  2364 		[arguments setObject:([tuneinfo objectForKey:ITUNES_STORE_URL] ? [tuneinfo objectForKey:ITUNES_STORE_URL] : @"") forKey:[NSString stringWithUTF8String:PURPLE_TUNE_URL]];
  2365 		
  2366 		[arguments setObject:[NSString stringWithFormat:@"%@%@%@", (name ? name : @""), (name && artist ? @" - " : @""), (artist ? artist : @"")]
  2367 					  forKey:[NSString stringWithUTF8String:PURPLE_TUNE_FULL]];
  2368 	}
  2369 
  2370 	return arguments;
  2371 }
  2372 
  2373 - (void)iTunesDidUpdate:(NSNotification*)notification {
  2374 	[tuneinfo release];
  2375 	tuneinfo = [[notification object] retain];
  2376 
  2377 	/* Only if we're including the information in all statuses do we need to do an update;
  2378 	 * if we just have a 'now playing' status, the dynamic stats update will call
  2379 	 * -[self setStatusState:usingStatusMessage:] in a moment.
  2380 	 */	 
  2381 	[purpleAdapter setSongInformation:(shouldIncludeNowPlayingInformationInAllStatuses ? [self purpleSongInfoDictionary] : nil) onAccount:self];
  2382 }
  2383 
  2384 /*!
  2385  * @brief Should a status message be set when using the default "Away" state?
  2386  */
  2387 - (BOOL)shouldSetStatusMessageForDefaultAwayState
  2388 {
  2389 	return YES;
  2390 }
  2391 
  2392 /*!
  2393  * @brief Perform the setting of a status state
  2394  *
  2395  * Sets the account to a passed status state.  The account should set itself to best possible status given the return
  2396  * values of statusState's accessors.  The passed statusMessage has been filtered; it should be used rather than
  2397  * statusState.statusMessage, which returns an unfiltered statusMessage.
  2398  *
  2399  * @param statusState The state to enter
  2400  * @param statusMessage The filtered status message to use.
  2401  */
  2402 - (void)setStatusState:(AIStatus *)statusState usingStatusMessage:(NSAttributedString *)statusMessage
  2403 {
  2404 	NSString			*encodedStatusMessage;
  2405 	NSMutableDictionary	*arguments = [[NSMutableDictionary alloc] init];
  2406 
  2407 	//Get the purple status type from this class or subclasses, which may also potentially modify or nullify our statusMessage
  2408 	const char *statusID = [self purpleStatusIDForStatus:statusState
  2409 											 arguments:arguments];
  2410 
  2411 	if (![statusMessage length] &&
  2412 		(statusState.statusType == AIAwayStatusType) &&
  2413 		statusState.statusName &&
  2414 		(!statusID || ((strcmp(statusID, "away") == 0) && [self shouldSetStatusMessageForDefaultAwayState]))) {
  2415 		/* If we don't have a status message, and the status type is away for a non-default away such as "Do Not Disturb", and we're only setting
  2416 		 * a default away state becuse we don't know a better one for this service, get a default
  2417 		 * description of this away state. This allows, for example, an AIM user to set the "Do Not Disturb" type provided by her ICQ account
  2418 		 * and have the away message be set appropriately.
  2419 		 */
  2420 		statusMessage = [NSAttributedString stringWithString:[adium.statusController descriptionForStateOfStatus:statusState]];
  2421 	}
  2422 
  2423 	BOOL isNowPlayingStatus = ([statusState specialStatusType] == AINowPlayingSpecialStatusType);
  2424 	if (isNowPlayingStatus && [statusMessage length]) {
  2425 		if ([self shouldAddMusicalNoteToNowPlayingStatus]) {
  2426 #define MUSICAL_NOTE_AND_SPACE [NSString stringWithUTF8String:"\xe2\x99\xab "]
  2427 			NSMutableAttributedString *temporaryStatusMessage;
  2428 			temporaryStatusMessage = [[[NSMutableAttributedString alloc] initWithString:MUSICAL_NOTE_AND_SPACE] autorelease];
  2429 			[temporaryStatusMessage appendAttributedString:statusMessage];
  2430 			
  2431 			statusMessage = temporaryStatusMessage;
  2432 		}
  2433 		
  2434 		if ([self shouldSetITMSLinkForNowPlayingStatus]) {
  2435 			//Grab the message's subtext, which is the song link if we're using the Current iTunes Track status
  2436 			NSString *itmsStoreLink	= [statusMessage attribute:@"AIMessageSubtext" atIndex:0 effectiveRange:NULL];
  2437 			if (itmsStoreLink) {
  2438 				[arguments setObject:itmsStoreLink
  2439 							  forKey:@"itmsurl"];
  2440 			}
  2441 		}
  2442 		
  2443 		NSDictionary *purpleSongInfoDictionary = [self purpleSongInfoDictionary];
  2444 		if (purpleSongInfoDictionary)
  2445 			[arguments addEntriesFromDictionary:purpleSongInfoDictionary];
  2446 	}
  2447 
  2448 	//Encode the status message if we have one
  2449 	encodedStatusMessage = (statusMessage ? 
  2450 							[self encodedAttributedString:statusMessage
  2451 										   forStatusState:statusState]  :
  2452 							nil);
  2453 	if (encodedStatusMessage) {
  2454 		[arguments setObject:encodedStatusMessage
  2455 					  forKey:@"message"];
  2456 	}
  2457 
  2458 	[self setStatusState:statusState
  2459 				statusID:statusID
  2460 				isActive:[NSNumber numberWithBool:YES] /* We're only using exclusive states for now... I hope.  */
  2461 			   arguments:arguments];
  2462 	
  2463 	[arguments release];
  2464 }
  2465 
  2466 /*!
  2467  * @brief Perform the actual setting of a state
  2468  *
  2469  * This is called by setStatusState.  It allows subclasses to perform any other behaviors, such as modifying a display
  2470  * name, which are called for by the setting of the state; most of the processing has already been done, however, so
  2471  * most subclasses will not need to implement this.
  2472  *
  2473  * @param statusState The AIStatus which is being set
  2474  * @param statusID The Purple-sepcific statusID we are setting
  2475  * @param isActive An NSNumber with a bool YES if we are activating (going to) the passed state, NO if we are deactivating (going away from) the passed state.
  2476  * @param arguments Purple-specific arguments specified by the account. It must contain only NSString objects and keys.
  2477  */
  2478 - (void)setStatusState:(AIStatus *)statusState statusID:(const char *)statusID isActive:(NSNumber *)isActive arguments:(NSMutableDictionary *)arguments
  2479 {
  2480 	[purpleAdapter setStatusID:statusID
  2481 				   isActive:isActive
  2482 				  arguments:arguments
  2483 				  onAccount:self];
  2484 }
  2485 
  2486 //Set our idle (Pass nil for no idle)
  2487 - (void)setAccountIdleSinceTo:(NSDate *)idleSince
  2488 {
  2489 	[purpleAdapter setIdleSinceTo:idleSince onAccount:self];
  2490 	
  2491 	//We now should update our idle property
  2492 	[self setValue:([idleSince timeIntervalSinceNow] ? idleSince : nil)
  2493 				   forProperty:@"IdleSince"
  2494 				   notify:NotifyNow];
  2495 }
  2496 
  2497 //Set the profile, then invoke the passed invocation to return control to the target/selector specified
  2498 //by a configurePurpleAccountNotifyingTarget:selector: call.
  2499 - (void)setAccountProfileTo:(NSAttributedString *)profile configurePurpleAccountContext:(NSInvocation *)inInvocation
  2500 {
  2501 	[self setAccountProfileTo:profile];
  2502 	
  2503 	[inInvocation invoke];
  2504 }
  2505 
  2506 //Set our profile immediately on the purpleAdapter
  2507 - (void)setAccountProfileTo:(NSAttributedString *)profile
  2508 {
  2509 	if (!profile || ![[profile string] isEqualToString:[[self valueForProperty:@"TextProfile"] string]]) {
  2510 		NSString 	*profileHTML = nil;
  2511 		
  2512 		//Convert the profile to HTML, and pass it to libpurple
  2513 		if (profile) {
  2514 			profileHTML = [self encodedAttributedString:profile forListObject:nil];
  2515 		}
  2516 		
  2517 		[purpleAdapter setInfo:profileHTML onAccount:self];
  2518 		
  2519 		//We now have a profile
  2520 		[self setValue:profile forProperty:@"TextProfile" notify:NotifyNow];
  2521 	}
  2522 }
  2523 
  2524 /*!
  2525  * @brief Set our user image
  2526  *
  2527  * Pass nil for no image. This resizes and converts the image as needed for our protocol.
  2528  * After setting it with purple, it sets it within Adium; if this is not called, the image will
  2529  * show up neither locally nor remotely.
  2530  */
  2531 - (void)setAccountUserImage:(NSImage *)image withData:(NSData *)originalData;
  2532 {
  2533 	if (account) {
  2534 		NSData		*imageData = originalData;
  2535 		NSSize		imageSize = (image ? [image size] : NSZeroSize);
  2536 		NSData		*buddyIconData = nil;
  2537 
  2538 		/* Now pass libpurple the new icon. Check to be sure our image doesn't have an NSZeroSize size,
  2539 		 * which would indicate currupt data */
  2540 		if (image && !NSEqualSizes(NSZeroSize, imageSize)) {
  2541 			PurplePluginProtocolInfo  *prpl_info = self.protocolInfo;
  2542 
  2543 			AILog(@"Original image of size %f %f",imageSize.width,imageSize.height);
  2544 
  2545 			if (prpl_info && (prpl_info->icon_spec.format)) {
  2546 				BOOL		smallEnough, prplScales;
  2547 				NSUInteger	i;
  2548 				
  2549 				/* We need to scale it down if:
  2550 				 *	1) The prpl needs to scale before it sends to the server or other buddies AND
  2551 				 *	2) The image is larger than the maximum size allowed by the protocol
  2552 				 * We ignore the minimum required size, as scaling up just leads to pixellated images.
  2553 				 */
  2554 				smallEnough =  (prpl_info->icon_spec.max_width >= imageSize.width &&
  2555 								prpl_info->icon_spec.max_height >= imageSize.height);
  2556 					
  2557 				prplScales = (prpl_info->icon_spec.scale_rules & PURPLE_ICON_SCALE_SEND) || (prpl_info->icon_spec.scale_rules & PURPLE_ICON_SCALE_DISPLAY);
  2558 
  2559 				if (prplScales && !smallEnough) {
  2560 					gint width = (gint)imageSize.width;
  2561 					gint height = (gint)imageSize.height;
  2562 					
  2563 					purple_buddy_icon_get_scale_size(&prpl_info->icon_spec, &width, &height);
  2564 					//Determine the scaled size.  If it's too big, scale to the largest permissable size
  2565 					image = [image imageByScalingToSize:NSMakeSize(width, height)];
  2566 
  2567 					/* Our original data is no longer valid, since we had to scale to a different size */
  2568 					imageData = nil;
  2569 					AILog(@"%@: Scaled image to size %@", self, NSStringFromSize([image size]));
  2570 				}
  2571 
  2572 				if (!buddyIconData) {
  2573 					char		**prpl_formats =  g_strsplit(prpl_info->icon_spec.format,",",0);
  2574 
  2575 					//Look for gif first if the image is animated
  2576 					NSImageRep	*imageRep = [image bestRepresentationForDevice:nil] ;
  2577 					if ([imageRep isKindOfClass:[NSBitmapImageRep class]] &&
  2578 						[[(NSBitmapImageRep *)imageRep valueForProperty:NSImageFrameCount] integerValue] > 1) {
  2579 						
  2580 						for (i = 0; prpl_formats[i]; i++) {
  2581 							if (strcmp(prpl_formats[i],"gif") == 0) {
  2582 								/* Try to use our original data.  If we had to scale, imageData will have been set
  2583 								* to nil and we'll continue below to convert the image. */
  2584 								AILog(@"l33t script kiddie animated GIF!!111");
  2585 								
  2586 								buddyIconData = imageData;
  2587 								if (buddyIconData)
  2588 									break;
  2589 							}
  2590 						}
  2591 					}
  2592 					
  2593 					if (!buddyIconData) {
  2594 						for (i = 0; prpl_formats[i]; i++) {
  2595 							if (strcmp(prpl_formats[i],"png") == 0) {
  2596 								buddyIconData = [image PNGRepresentation];
  2597 								if (buddyIconData)
  2598 									break;
  2599 								
  2600 							} else if ((strcmp(prpl_formats[i],"jpeg") == 0) || (strcmp(prpl_formats[i],"jpg") == 0)) {								
  2601 								buddyIconData = [image JPEGRepresentationWithCompressionFactor:1.0];
  2602 								if (buddyIconData)
  2603 									break;
  2604 								
  2605 							} else if ((strcmp(prpl_formats[i],"tiff") == 0) || (strcmp(prpl_formats[i],"tif") == 0)) {
  2606 								buddyIconData = [image TIFFRepresentation];
  2607 								if (buddyIconData)
  2608 									break;
  2609 								
  2610 							} else if (strcmp(prpl_formats[i],"gif") == 0) {
  2611 								buddyIconData = [image GIFRepresentation];
  2612 								if (buddyIconData)
  2613 									break;
  2614 								
  2615 							} else if (strcmp(prpl_formats[i],"bmp") == 0) {
  2616 								buddyIconData = [image BMPRepresentation];
  2617 								if (buddyIconData)
  2618 									break;
  2619 								
  2620 							}						
  2621 						}
  2622 						
  2623 						size_t maxSize = prpl_info->icon_spec.max_filesize;
  2624 						if (maxSize > 0 && ([buddyIconData length] > maxSize)) {
  2625 							AILog(@"Image %i is larger than %i!",[buddyIconData length],maxSize);
  2626 							for (i = 0; prpl_formats[i]; i++) {
  2627 								if ((strcmp(prpl_formats[i],"jpeg") == 0) || (strcmp(prpl_formats[i],"jpg") == 0)) {
  2628 									buddyIconData = [image JPEGRepresentationWithMaximumByteSize:maxSize];
  2629 								}
  2630 							}
  2631 						}
  2632 					}	
  2633 					//Cleanup
  2634 					g_strfreev(prpl_formats);
  2635 				}
  2636 			}
  2637 		}
  2638 
  2639 		AILogWithSignature(@"%@ setting icon data of length %i", self, [buddyIconData length]);
  2640 		[purpleAdapter setBuddyIcon:buddyIconData onAccount:self];
  2641 	}
  2642 	
  2643 	[super setAccountUserImage:image withData:originalData];
  2644 }
  2645 
  2646 #pragma mark Group Chat
  2647 - (BOOL)inviteContact:(AIListContact *)inContact toChat:(AIChat *)inChat withMessage:(NSString *)inviteMessage
  2648 {
  2649 	[purpleAdapter inviteContact:inContact toChat:inChat withMessage:inviteMessage];
  2650 	
  2651 	return YES;
  2652 }
  2653 
  2654 #pragma mark Buddy Menu Items
  2655 //Action of a dynamically-generated contact menu item
  2656 - (void)performContactMenuAction:(NSMenuItem *)sender
  2657 {
  2658 	NSDictionary		*dict = [sender representedObject];
  2659 	
  2660 	[purpleAdapter performContactMenuActionFromDict:dict forAccount:self];
  2661 }
  2662 
  2663 /*!
  2664  * @brief Utility method when generating buddy-specific menu items
  2665  *
  2666  * Adds the menu item for act to a growing array of NSMenuItems.  If act has children (a submenu), this method is used recursively
  2667  * to generate the submenu containing each child menu item.
  2668  */
  2669 - (void)addMenuItemForMenuAction:(PurpleMenuAction *)act forListContact:(AIListContact *)inContact purpleBuddy:(PurpleBuddy *)buddy toArray:(NSMutableArray *)menuItemArray withServiceIcon:(NSImage *)serviceIcon
  2670 {
  2671 	NSDictionary	*dict;
  2672 	NSMenuItem		*menuItem;
  2673 	NSString		*title;
  2674 				
  2675 	//If titleForContactMenuLabel:forContact: returns nil, we don't add the menuItem
  2676 	if (act &&
  2677 		act->label &&
  2678 		(title = [self titleForContactMenuLabel:act->label
  2679 									 forContact:inContact])) { 
  2680 		menuItem = [[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:title
  2681 																		target:self
  2682 																		action:@selector(performContactMenuAction:)
  2683 																 keyEquivalent:@""];
  2684 		[menuItem setImage:serviceIcon];
  2685 
  2686 		if (act->data) {
  2687 			dict = [NSDictionary dictionaryWithObjectsAndKeys:
  2688 				[NSValue valueWithPointer:act->callback],@"PurpleMenuActionCallback",
  2689 				/* act->data may be freed by purple_menu_action_free() before we use it, I'm afraid... */
  2690 				[NSValue valueWithPointer:act->data],@"PurpleMenuActionData",
  2691 				[NSValue valueWithPointer:buddy],@"PurpleBuddy",
  2692 				nil];
  2693 		} else {
  2694 			dict = [NSDictionary dictionaryWithObjectsAndKeys:
  2695 				[NSValue valueWithPointer:act->callback],@"PurpleMenuActionCallback",
  2696 				[NSValue valueWithPointer:buddy],@"PurpleBuddy",
  2697 				nil];			
  2698 		}
  2699 		
  2700 		[menuItem setRepresentedObject:dict];
  2701 		
  2702 		//If there is a submenu, generate and set it
  2703 		if (act->children) {
  2704 			NSMutableArray	*childrenArray = [NSMutableArray array];
  2705 			GList			*l, *ll;
  2706 			//Add a NSMenuItem for each child
  2707 			for (l = ll = act->children; l; l = l->next) {
  2708 				[self addMenuItemForMenuAction:(PurpleMenuAction *)l->data
  2709 								forListContact:inContact
  2710 									 purpleBuddy:buddy
  2711 									   toArray:childrenArray
  2712 							   withServiceIcon:serviceIcon];
  2713 			}
  2714 			g_list_free(act->children);
  2715 
  2716 			if ([childrenArray count]) {
  2717 				NSMenu		 *submenu = [[NSMenu alloc] init];
  2718 				
  2719 				for (NSMenuItem *childMenuItem in childrenArray) {
  2720 					[submenu addItem:childMenuItem];
  2721 				}
  2722 				
  2723 				[menuItem setSubmenu:submenu];
  2724 				[submenu release];
  2725 			}
  2726 		}
  2727 
  2728 		[menuItemArray addObject:menuItem];
  2729 		[menuItem release];
  2730 	}
  2731 
  2732 	purple_menu_action_free(act);
  2733 }
  2734 
  2735 //Returns an array of menuItems specific for this contact based on its account and potentially status
  2736 - (NSArray *)menuItemsForContact:(AIListContact *)inContact
  2737 {
  2738 	NSMutableArray			*menuItemArray = nil;
  2739 
  2740 	if (account && purple_account_is_connected(account)) {
  2741 		PurplePluginProtocolInfo  *prpl_info = self.protocolInfo;
  2742 		GList					*l, *ll;
  2743 		PurpleBuddy				*buddy;
  2744 		
  2745 		//Find the PurpleBuddy
  2746 		buddy = purple_find_buddy(account, [inContact.UID UTF8String]);
  2747 		
  2748 		if (prpl_info && prpl_info->blist_node_menu && buddy) {
  2749 			NSImage	*serviceIcon = [AIServiceIcons serviceIconForService:self.service
  2750 																	type:AIServiceIconSmall
  2751 															   direction:AIIconNormal];
  2752 			
  2753 			menuItemArray = [NSMutableArray array];
  2754 
  2755 			//Add a NSMenuItem for each node action specified by the prpl
  2756 			for (l = ll = prpl_info->blist_node_menu((PurpleBlistNode *)buddy); l; l = l->next) {
  2757 				[self addMenuItemForMenuAction:(PurpleMenuAction *)l->data
  2758 								forListContact:inContact
  2759 									 purpleBuddy:buddy
  2760 									   toArray:menuItemArray
  2761 							   withServiceIcon:serviceIcon];
  2762 			}
  2763 			g_list_free(ll);
  2764 			
  2765 			//Don't return an empty array
  2766 			if (![menuItemArray count]) menuItemArray = nil;
  2767 		}
  2768 	}
  2769 	
  2770 	return menuItemArray;
  2771 }
  2772 
  2773 //Subclasses may override to provide a localized label and/or prevent a specified label from being shown
  2774 - (NSString *)titleForContactMenuLabel:(const char *)label forContact:(AIListContact *)inContact
  2775 {
  2776 	return [NSString stringWithUTF8String:label];
  2777 }
  2778 
  2779 /*!
  2780 * @brief Menu items for the account's actions
  2781  *
  2782  * Returns an array of menu items for account-specific actions.  This is the best place to add protocol-specific
  2783  * actions that aren't otherwise supported by Adium.  It will only be queried if the account is online.
  2784  * @return NSArray of NSMenuItem instances for this account
  2785  */
  2786 - (NSArray *)accountActionMenuItems
  2787 {
  2788 	NSMutableArray			*menuItemArray = nil;
  2789 	
  2790 	if (account && purple_account_is_connected(account)) {
  2791 		PurplePlugin *plugin = purple_account_get_connection(account)->prpl;
  2792 		
  2793 		if (PURPLE_PLUGIN_HAS_ACTIONS(plugin)) {
  2794 			GList	*l, *actions;
  2795 			
  2796 			actions = PURPLE_PLUGIN_ACTIONS(plugin, purple_account_get_connection(account));
  2797 
  2798 			//Avoid adding separators between nonexistant items (i.e. items which Purple shows but we don't)
  2799 			BOOL	addedAnAction = NO;
  2800 			for (l = actions; l; l = l->next) {
  2801 				
  2802 				if (l->data) {
  2803 					PurplePluginAction	*action;
  2804 					NSDictionary		*dict;
  2805 					NSMenuItem			*menuItem;
  2806 					NSString			*title;
  2807 					
  2808 					action = (PurplePluginAction *) l->data;
  2809 					
  2810 					//If titleForAccountActionMenuLabel: returns nil, we don't add the menuItem
  2811 					if (action &&
  2812 						action->label &&
  2813 						(title = [self titleForAccountActionMenuLabel:action->label])) {
  2814 
  2815 						action->plugin = plugin;
  2816 						action->context = purple_account_get_connection(account);
  2817 
  2818 						menuItem = [[[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:title
  2819 																						 target:self
  2820 																						 action:@selector(performAccountMenuAction:)
  2821 																				  keyEquivalent:@""] autorelease];
  2822 						dict = [NSDictionary dictionaryWithObjectsAndKeys:
  2823 							[NSValue valueWithPointer:action->callback], @"PurplePluginActionCallback",
  2824 							[NSValue valueWithPointer:action->user_data], @"PurplePluginActionCallbackUserData",
  2825 							nil];
  2826 						
  2827 						[menuItem setRepresentedObject:dict];
  2828 						
  2829 						if (!menuItemArray) menuItemArray = [NSMutableArray array];
  2830 						
  2831 						[menuItemArray addObject:menuItem];
  2832 						addedAnAction = YES;
  2833 					} 
  2834 					
  2835 					purple_plugin_action_free(action);
  2836 					
  2837 				} else {
  2838 					if (addedAnAction) {
  2839 						[menuItemArray addObject:[NSMenuItem separatorItem]];
  2840 						addedAnAction = NO;
  2841 					}
  2842 				}
  2843 			} /* end for */
  2844 			
  2845 			g_list_free(actions);
  2846 		}
  2847 	}
  2848 	
  2849 #ifdef HAVE_CDSA
  2850 	if([self encrypted] && [self secureConnection]) {
  2851 		if (menuItemArray.count) {
  2852 			[menuItemArray addObject:[NSMenuItem separatorItem]];
  2853 		}
  2854 		
  2855 		NSMenuItem *showCertificateMenuItem = [[[NSMenuItem alloc] initWithTitle:AILocalizedString(@"Show Server Certificate",nil)
  2856 																		 target:self
  2857 																		 action:@selector(showServerCertificate) 
  2858 																  keyEquivalent:@""] autorelease];
  2859 		
  2860 		[menuItemArray addObject:showCertificateMenuItem];
  2861 	}
  2862 #endif
  2863 
  2864 	return menuItemArray;
  2865 }
  2866 
  2867 #ifdef HAVE_CDSA
  2868 /*!
  2869  * @brief Shows the SSL certificate for the connection.
  2870  */
  2871 - (void)showServerCertificate
  2872 {
  2873 	CFArrayRef certificates = [[self purpleAdapter] copyServerCertificates:[self secureConnection]];
  2874 	
  2875 	[AIPurpleCertificateViewer displayCertificateChain:certificates forAccount:self];
  2876 	
  2877 	CFRelease(certificates);
  2878 }
  2879 #endif
  2880 
  2881 //Action of a dynamically-generated contact menu item
  2882 - (void)performAccountMenuAction:(NSMenuItem *)sender
  2883 {
  2884 	NSDictionary		*dict = [sender representedObject];
  2885 
  2886 	[purpleAdapter performAccountMenuActionFromDict:dict forAccount:self];
  2887 }
  2888 
  2889 //Subclasses may override to provide a localized label and/or prevent a specified label from being shown
  2890 - (NSString *)titleForAccountActionMenuLabel:(const char *)label
  2891 {
  2892 	if ((strcmp(label, _("Change Password...")) == 0) || (strcmp(label, _("Change Password")) == 0)) {
  2893 		return [[NSString stringWithFormat:AILocalizedString(@"Change Password", "Menu item title for changing the password of an account")] stringByAppendingEllipsis];
  2894 	} else {
  2895 		return [NSString stringWithUTF8String:label];
  2896 	}
  2897 }
  2898 
  2899 /********************************/
  2900 /* AIAccount subclassed methods */
  2901 /********************************/
  2902 #pragma mark AIAccount Subclassed Methods
  2903 - (void)initAccount
  2904 {
  2905 	NSDictionary	*defaults = [NSDictionary dictionaryNamed:[NSString stringWithFormat:@"PurpleDefaults%@",self.service.serviceID]
  2906 													 forClass:[self class]];
  2907 	
  2908 	if (defaults) {
  2909 		[adium.preferenceController registerDefaults:defaults
  2910 											  forGroup:GROUP_ACCOUNT_STATUS
  2911 												object:self];
  2912 	} else {
  2913 		AILog(@"Failed to load defaults for %@",[NSString stringWithFormat:@"PurpleDefaults%@",self.service.serviceID]);
  2914 	}
  2915 	
  2916 	//Defaults
  2917 	[self setLastDisconnectionError:nil];
  2918 	
  2919 	permittedContactsArray = [[NSMutableArray alloc] init];
  2920 	deniedContactsArray = [[NSMutableArray alloc] init];
  2921 
  2922 	//We will create a purpleAccount the first time we attempt to connect
  2923 	account = NULL;
  2924 
  2925 	//Observe preferences changes
  2926 	[adium.preferenceController registerPreferenceObserver:self forGroup:PREF_GROUP_ALIASES];
  2927 	[adium.preferenceController registerPreferenceObserver:self forGroup:PREF_GROUP_DUAL_WINDOW_INTERFACE];
  2928 }
  2929 
  2930 - (BOOL)allowAccountUnregistrationIfSupportedByLibpurple
  2931 {
  2932 	return YES;
  2933 }
  2934 
  2935 /*!
  2936  * @brief The account will be deleted, we should ask the user for confirmation. If the prpl supports it, we can also remove
  2937  * the account from the server (if the user wants us to do that)
  2938  */
  2939 - (NSAlert*)alertForAccountDeletion
  2940 {
  2941 	PurplePluginProtocolInfo *prpl_info = self.protocolInfo;
  2942 
  2943 	//Ensure libpurple has been loaded, since we need to know whether we can unregister this account
  2944 	[self purpleAdapter];
  2945 
  2946 	if (prpl_info && 
  2947 		prpl_info->unregister_user &&
  2948 		[self allowAccountUnregistrationIfSupportedByLibpurple]) {
  2949 		return [NSAlert alertWithMessageText:AILocalizedString(@"Delete Account",nil)
  2950 							   defaultButton:AILocalizedString(@"Delete",nil)
  2951 							 alternateButton:AILocalizedString(@"Cancel",nil)
  2952 								 otherButton:AILocalizedString(@"Delete & Unregister",nil)
  2953 				   informativeTextWithFormat:AILocalizedString(@"Delete the account %@? You can also optionally unregister the account on the server if possible.",nil), ([self.formattedUID length] ? self.formattedUID : NEW_ACCOUNT_DISPLAY_TEXT)];		
  2954 
  2955 	} else {
  2956 		return [super alertForAccountDeletion];
  2957 	}
  2958 }
  2959 
  2960 - (void)alertForAccountDeletion:(id<AIAccountControllerRemoveConfirmationDialog>)dialog didReturn:(NSInteger)returnCode
  2961 {
  2962 	PurplePluginProtocolInfo *prpl_info = self.protocolInfo;
  2963 	
  2964 	if (prpl_info && 
  2965 		prpl_info->unregister_user) {
  2966 		switch (returnCode) {
  2967 			case NSAlertOtherReturn:
  2968 				// delete & unregister
  2969 				if (self.online)
  2970 					[self unregister];
  2971 				else {
  2972 					unregisterAfterConnecting = YES;
  2973 					[self setShouldBeOnline:YES];
  2974 				}
  2975 			
  2976 				// further progress happens in -unregisteredAccount:
  2977 				break;
  2978 			case NSAlertDefaultReturn:
  2979 				// delete without unregistering
  2980 				[self performDelete];
  2981 				break;
  2982 			default:
  2983 				// cancel
  2984 				break;
  2985 		}
  2986 		
  2987 	} else {
  2988 		switch(returnCode) {
  2989 			case NSAlertDefaultReturn:
  2990 				[self performDelete];
  2991 				break;
  2992 			default:
  2993 				// cancel
  2994 				break;
  2995 		}
  2996 	}
  2997 	
  2998 	//Release dialog as required by AIAccount's documentation since we didn't call super's implementation.
  2999 	[dialog release];
  3000 }
  3001 
  3002 - (void)unregisteredAccount:(BOOL)success {
  3003 	if (success) {
  3004 		/* We're not going to be online, but we *must* not disconnect within this run loop,
  3005 		 * as libpurple may still have Things To Do with the connection and it has no concept of reference
  3006 		 * counting with which to survive the disconnection. Performing a deletion would set us offline,
  3007 		 * so wait until the next run loop.
  3008 		 */
  3009 		[self performSelector:@selector(performDelete)
  3010 				   withObject:nil
  3011 				   afterDelay:0];
  3012 	}
  3013 }
  3014 
  3015 /*!
  3016  * @brief The account's UID changed
  3017  */
  3018 - (void)didChangeUID
  3019 {
  3020 	//Only need to take action if we have a created PurpleAccount already
  3021 	if (account != NULL) {
  3022 		//Remove our current account
  3023 		[[self purpleAdapter] removeAdiumAccount:self];
  3024 		
  3025 		//Clear the reference to the PurpleAccount... it'll be created when needed
  3026 		account = NULL;
  3027 	}
  3028 }
  3029 
  3030 /*!
  3031  * @brief The account will be deleted; it has already been told to disconnect
  3032  */
  3033 - (void)willBeDeleted
  3034 {	
  3035 	if (self.online) {
  3036 		//Wait until we are finished disconnecting before removing ourselves from libpurple.
  3037 		deletePurpleAccountAfterDisconnecting = TRUE;
  3038 
  3039 	} else {
  3040 		[[self purpleAdapter] removeAdiumAccount:self];
  3041 	}
  3042 
  3043 	[super willBeDeleted];
  3044 }
  3045 
  3046 - (void)dealloc
  3047 {	
  3048 	[adium.preferenceController unregisterPreferenceObserver:self];
  3049 
  3050 	[permittedContactsArray release];
  3051 	[deniedContactsArray release];
  3052 	
  3053     [super dealloc];
  3054 }
  3055 
  3056 - (NSString *)unknownGroupName {
  3057     return (@"Unknown");
  3058 }
  3059 
  3060 - (NSDictionary *)defaultProperties { return [NSDictionary dictionary]; }
  3061 
  3062 - (NSString *)encodedAttributedString:(NSAttributedString *)inAttributedString forStatusState:(AIStatus *)statusState
  3063 {
  3064 	return [self encodedAttributedString:inAttributedString forListObject:nil];	
  3065 }
  3066 
  3067 - (void)preferencesChangedForGroup:(NSString *)group key:(NSString *)key
  3068 							object:(AIListObject *)object preferenceDict:(NSDictionary *)prefDict firstTime:(BOOL)firstTime
  3069 {
  3070 	[super preferencesChangedForGroup:group key:key object:object preferenceDict:prefDict firstTime:firstTime];
  3071 
  3072 	if ([group isEqualToString:PREF_GROUP_ALIASES]) {
  3073 		//If the notification object is a listContact belonging to this account, update the serverside information
  3074 		if ((account != nil) && 
  3075 			([self shouldSetAliasesServerside]) &&
  3076 			([key isEqualToString:@"Alias"])) {
  3077 
  3078 			NSString *alias = [object preferenceForKey:@"Alias"
  3079 												 group:PREF_GROUP_ALIASES 
  3080 								];
  3081 
  3082 			if ([object isKindOfClass:[AIMetaContact class]]) {
  3083 				for(AIListContact *containedListContact in (AIMetaContact *)object) {
  3084 					if (containedListContact.account == self) {
  3085 						[purpleAdapter setAlias:alias forUID:containedListContact.UID onAccount:self];
  3086 					}
  3087 				}
  3088 				
  3089 			} else if ([object isKindOfClass:[AIListContact class]]) {
  3090 				if ([(AIListContact *)object account] == self) {
  3091 					[purpleAdapter setAlias:alias forUID:object.UID onAccount:self];
  3092 				}
  3093 			}
  3094 		}
  3095 	} else if ([group isEqualToString:PREF_GROUP_DUAL_WINDOW_INTERFACE]) {
  3096 		openPsychicChats = [[prefDict objectForKey:KEY_PSYCHIC] boolValue];
  3097 
  3098 	} else if ([group isEqualToString:GROUP_ACCOUNT_STATUS]) {
  3099 		BOOL oldNowPlaying = shouldIncludeNowPlayingInformationInAllStatuses;
  3100 		
  3101 		shouldIncludeNowPlayingInformationInAllStatuses = [[self preferenceForKey:KEY_BROADCAST_MUSIC_INFO group:GROUP_ACCOUNT_STATUS] boolValue];
  3102 
  3103 		if (oldNowPlaying && !shouldIncludeNowPlayingInformationInAllStatuses) {
  3104 			/* Clear any existing song info immediately if we're no longer supposed to broadcast it */
  3105 			[purpleAdapter setSongInformation:nil onAccount:self];
  3106 		}
  3107 	}
  3108 }
  3109 
  3110 /*!
  3111  * @brief When the account is edited, update our libpurple preferences.
  3112  */
  3113 - (void)accountEdited
  3114 {
  3115 	// We only need to re-configure if we're online or connecting. If we're offline, our next connect will do this.
  3116 	if (self.online || [self boolValueForProperty:@"Connecting"]) {
  3117 		AILog(@"Re-configuring purple account due to preference changes.");
  3118 		[self configurePurpleAccount];
  3119 	}
  3120 }
  3121 
  3122 #pragma mark Actions for chats
  3123 
  3124 /***************************/
  3125 /* Account private methods */
  3126 /***************************/
  3127 #pragma mark Private
  3128 - (void)setTypingFlagOfChat:(AIChat *)chat to:(NSNumber *)typingStateNumber
  3129 {
  3130 	NSAssert(!chat.isGroupChat, @"Chat cannot be a group chat for typing.");
  3131 	
  3132     AITypingState currentTypingState = [chat integerValueForProperty:KEY_TYPING];
  3133 	AITypingState newTypingState = [typingStateNumber integerValue];
  3134 	
  3135     if (currentTypingState != newTypingState) {
  3136 		if (newTypingState == AITyping && openPsychicChats && ![chat isOpen]) {
  3137 			[adium.interfaceController openChat:chat];
  3138 			
  3139 			/*
  3140 			 * Use the Libpurple "psychic" tagline. If this is found to be confusing, we should switch to your own version.
  3141 			 * The upside of using theirs is that clever gimmicky translations already exist.
  3142 			 */
  3143 			NSMutableString *forceString = [[NSString stringWithUTF8String:_("You feel a disturbance in the force...")] mutableCopy];
  3144 			[forceString replaceOccurrencesOfString:@"..."
  3145 										 withString:[NSString ellipsis]
  3146 											options:NSLiteralSearch];
  3147 			AIContentEvent *statusMessage = [AIContentEvent eventInChat:chat
  3148 															 withSource:chat.listObject
  3149 															destination:self
  3150 																   date:[NSDate date]
  3151 																message:[NSAttributedString stringWithString:forceString]
  3152 															   withType:@"psychic"];
  3153 			
  3154 			// Don't log the psychic message.
  3155 			statusMessage.postProcessContent = NO;
  3156 			
  3157 			[forceString release];
  3158 
  3159 			[adium.contentController receiveContentObject:statusMessage];
  3160 		}
  3161 		
  3162 		[chat setValue:(newTypingState ? typingStateNumber : nil)
  3163 					   forProperty:KEY_TYPING
  3164 					   notify:NotifyNow];
  3165     }
  3166 }
  3167 
  3168 - (NSNumber *)shouldCheckMail
  3169 {
  3170 	return [self preferenceForKey:KEY_ACCOUNT_CHECK_MAIL group:GROUP_ACCOUNT_STATUS];
  3171 }
  3172 
  3173 - (BOOL)shouldSetAliasesServerside
  3174 {
  3175 	return NO;
  3176 }
  3177 
  3178 @end