Plugins/Purple Service/SLPurpleCocoaAdapter.m
author Evan Schoenberg
Sun, 21 Aug 2011 15:14:33 -0500
changeset 3610 93c8292bc1c8
parent 3608 c5435740e49b
permissions -rw-r--r--
Apply the same fix as a00b7964253b, but for group chats, again resolving a double-retain
(transplanted from a9560a8f30eb8e61e4a327d42b5376262ad7e30b)
/* 
 * Adium is the legal property of its developers, whose names are listed in the copyright file included
 * with this source distribution.
 * 
 * This program is free software; you can redistribute it and/or modify it under the terms of the GNU
 * General Public License as published by the Free Software Foundation; either version 2 of the License,
 * or (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
 * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General
 * Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License along with this program; if not,
 * write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
 */

#import <AdiumLibpurple/SLPurpleCocoaAdapter.h>
#import "CBPurpleAccount.h"
#import "CBPurpleServicePlugin.h"
#import "adiumPurpleCore.h"
#import "adiumPurpleEventloop.h"

#import <Adium/AIAccountControllerProtocol.h>
#import <Adium/AIInterfaceControllerProtocol.h>
#import <Adium/AILoginControllerProtocol.h>
#import <AIUtilities/AIObjectAdditions.h>
#import <Adium/AIAccount.h>
#import <Adium/AICorePluginLoader.h>
#import <Adium/AIService.h>
#import <Adium/AIChat.h>
#import <Adium/AIContentTyping.h>
#import <Adium/AIHTMLDecoder.h>
#import <Adium/AIListContact.h>
#import <Adium/AIContactObserverManager.h>
#import <Adium/AIUserIcons.h>
#import <Adium/AIContactObserverManager.h>
#import <AIUtilities/AIImageAdditions.h>

#import <CoreFoundation/CoreFoundation.h>
#import <libpurple/libpurple.h>
#import <glib.h>
#import <stdlib.h>

#import "ESPurpleAIMAccount.h"
#import "CBPurpleOscarAccount.h"

#import "ESiTunesPlugin.h"

#import "adiumPurpleAccounts.h"

//Purple slash command interface
#import <libpurple/cmds.h>

#import "libpurple_extensions/oscar-adium.h"

@interface SLPurpleCocoaAdapter ()
- (void)initLibPurple;
- (BOOL)attemptPurpleCommandOnMessage:(NSString *)originalMessage fromAccount:(AIAccount *)sourceAccount inChat:(AIChat *)chat;
@end

/*
 * A pointer to the single instance of this class active in the application.
 * The purple callbacks need to be C functions with specific prototypes, so they
 * can't be ObjC methods. The ObjC callbacks do need to be ObjC methods. This
 * allows the C ones to call the ObjC ones.
 **/
static SLPurpleCocoaAdapter	*sharedInstance = nil;

static NSMutableArray		*libpurplePluginArray = nil;

@implementation SLPurpleCocoaAdapter

/*!
 * @brief Return the shared instance
 */
+ (SLPurpleCocoaAdapter *)sharedInstance
{	
	@synchronized(self) {
		if (!sharedInstance) {
			sharedInstance = [[self alloc] init];
		}
	}

	return sharedInstance;
}

/*!
 * @brief Plugin loaded
 *
 * Initialize each libpurple plugin.  These plugins should not do anything within libpurple itself; this should be done in
 * -[plugin initLibpurplePlugin].
 */
+ (void)pluginDidLoad
{
	libpurplePluginArray = [[NSMutableArray alloc] init];

	for (NSString *libpurplePluginPath in [adium allResourcesForName:@"Plugins"
													  withExtensions:@"AdiumLibpurplePlugin"]) {
		[AICorePluginLoader loadPluginAtPath:libpurplePluginPath
							  confirmLoading:YES
								 pluginArray:libpurplePluginArray];
	}
}

+ (NSArray *)libpurplePluginArray
{
	return libpurplePluginArray;
}

//Register the account purpleside in the purple thread
- (void)addAdiumAccount:(CBPurpleAccount *)adiumAccount
{
	//Note that purple_account_new() calls purple_accounts_find() first, returning an existing PurpleAccount if there is one.
	PurpleAccount *account = purple_account_new([adiumAccount purpleAccountName], [adiumAccount protocolPlugin]);

	if (account->ui_data) {
		[(CBPurpleAccount *)account->ui_data autorelease];
		[(CBPurpleAccount *)account->ui_data setPurpleAccount:nil];
	}
	account->ui_data = [adiumAccount retain];

	[adiumAccount setPurpleAccount:account];

	purple_accounts_add(account);
	purple_account_set_status_list(account, "offline", YES, NULL);
}

//Remove an account purpleside
- (void)removeAdiumAccount:(CBPurpleAccount *)adiumAccount
{
	PurpleAccount *account = accountLookupFromAdiumAccount(adiumAccount);

	if (account) {
		[(CBPurpleAccount *)account->ui_data release];
		account->ui_data = nil;
		
		purple_accounts_remove(account);
	}

	[adiumAccount setPurpleAccount:NULL];
}

#pragma mark Initialization
- (id)init
{
	if ((self = [super init])) {
		[self initLibPurple];		
	}
	
    return self;
}

static void ZombieKiller_Signal(int i)
{
	int status;
	pid_t child_pid;

	while ((child_pid = waitpid(-1, &status, WNOHANG)) > 0);
}

- (void)networkDidChange:(NSNotification *)inNotification
{
	purple_signal_emit(purple_network_get_handle(), "network-configuration-changed", NULL);
}

- (void)debugLoggingIsEnabledDidChange:(NSNotification *)inNotification
{
	configurePurpleDebugLogging();
}

- (void)initLibPurple
{
	/* Initializing libpurple may result in loading a ton of buddies if our permit and deny lists are large; that, in
	 * turn, would create and update a ton of contacts.
	 */
	[[AIContactObserverManager sharedManager] delayListObjectNotifications];
	
	//Set the gaim user directory to be within this user's directory
	if (![[NSUserDefaults standardUserDefaults] boolForKey:@"Adium 1.0.3 moved to libpurple"]) {
		//Remove old icons cache
		[[NSFileManager defaultManager]  removeItemAtPath:[[[adium.loginController userDirectory] stringByAppendingPathComponent:@"libgaim"] stringByAppendingPathComponent:@"icons"]
												  error:NULL];
		
		//Update the rest
		[[NSFileManager defaultManager] moveItemAtPath:[[adium.loginController userDirectory] stringByAppendingPathComponent:@"libgaim"]
										  toPath:[[adium.loginController userDirectory] stringByAppendingPathComponent:@"libpurple"]
										 error:NULL];
		
		[[NSUserDefaults standardUserDefaults] setBool:YES
												forKey:@"Adium 1.0.3 moved to libpurple"];
	}
	
	//Set the purple user directory to be within this user's directory
	NSString	*purpleUserDir = [[adium.loginController userDirectory] stringByAppendingPathComponent:@"libpurple"];
	purple_util_set_user_dir([[purpleUserDir stringByExpandingTildeInPath] fileSystemRepresentation]);

	//Set the caches path
	purple_buddy_icons_set_cache_dir([[[adium cachesPath] stringByExpandingTildeInPath] fileSystemRepresentation]);

	/* Delete blist.xml once when 1.2.4 runs to clear out any old silliness, including improperly blocked Yahoo contacts */
	if (![[NSUserDefaults standardUserDefaults] boolForKey:@"Adium 1.2.4 deleted blist.xml"]) {
		[[NSFileManager defaultManager] removeItemAtPath:
			[[[NSString stringWithUTF8String:purple_user_dir()] stringByAppendingPathComponent:@"blist"] stringByAppendingPathExtension:@"xml"]
												 error:NULL];
		[[NSUserDefaults standardUserDefaults] setBool:YES
												forKey:@"Adium 1.2.4 deleted blist.xml"];
	}

	purple_core_set_ui_ops(adium_purple_core_get_ops());
	purple_eventloop_set_ui_ops(adium_purple_eventloop_get_ui_ops());

	//Initialize the libpurple core; this will call back on the function specified in our core UI ops for us to finish configuring libpurple
	if (!purple_core_init("Adium")) {
		NSLog(@"*** FATAL ***: Failed to initialize purple core");
		AILog(@"*** FATAL ***: Failed to initialize purple core");
	}

	//Libpurple's async DNS lookup tends to create zombies.
	{
		struct sigaction act;
		
		act.sa_handler = ZombieKiller_Signal;		
		//Send for terminated but not stopped children
		act.sa_flags = SA_NOCLDWAIT;

		sigaction(SIGCHLD, &act, NULL);
	}
	
	//Observe for network changes to tell libpurple about it
	[[NSNotificationCenter defaultCenter] addObserver:self
								   selector:@selector(networkDidChange:)
									   name:AINetworkDidChangeNotification
									 object:nil];

	/* Be sure to enable debug logging if it is turned on after launch */
	[[NSNotificationCenter defaultCenter] addObserver:self
											 selector:@selector(debugLoggingIsEnabledDidChange:)
												 name:AIDebugLoggingEnabledNotification
											   object:nil];

	
	/* For any behaviors which occur on the next run loop, provide a buffer time of continued expectation of 
	 * heavy activity.
	 */
	[[AIContactObserverManager sharedManager] delayListObjectNotificationsUntilInactivity];
	
	[[AIContactObserverManager sharedManager] endListObjectNotificationsDelay];

}

#pragma mark Lookup functions

NSString* serviceClassForPurpleProtocolID(const char *protocolID)
{
	NSString	*serviceClass = nil;
	if (protocolID) {
		if (!strcmp(protocolID, "prpl-oscar"))
			serviceClass = @"AIM-compatible";
		else if (!strcmp(protocolID, "prpl-gg"))
			serviceClass = @"Gadu-Gadu";
		else if (!strcmp(protocolID, "prpl-jabber"))
			serviceClass = @"Jabber";
		else if (!strcmp(protocolID, "prpl-meanwhile"))
			serviceClass = @"Sametime";
		else if (!strcmp(protocolID, "prpl-msn"))
			serviceClass = @"MSN";
		else if (!strcmp(protocolID, "prpl-novell"))
			serviceClass = @"GroupWise";
		else if (!strcmp(protocolID, "prpl-yahoo"))
			serviceClass = @"Yahoo!";
		else if (!strcmp(protocolID, "prpl-zephyr"))
			serviceClass = @"Zephyr";
	}
	
	return serviceClass;
}

/*
 * Finds an instance of CBPurpleAccount for a pointer to a PurpleAccount struct.
 */
CBPurpleAccount* accountLookup(PurpleAccount *acct)
{
	CBPurpleAccount *adiumPurpleAccount = (acct ? (CBPurpleAccount *)acct->ui_data : nil);
	/* If the account doesn't have its ui_data associated yet (we haven't tried to connect) but we want this
	 * lookup data, we have to do some manual parsing.  This is used for example from the OTR preferences.
	 */
	if (!adiumPurpleAccount && acct) {
		const char	*protocolID = acct->protocol_id;
		NSString	*serviceClass = serviceClassForPurpleProtocolID(protocolID);

		for (adiumPurpleAccount in adium.accountController.accounts) {
			if ([adiumPurpleAccount isKindOfClass:[CBPurpleAccount class]] &&
			   [adiumPurpleAccount.service.serviceClass isEqualToString:serviceClass] &&
			   [adiumPurpleAccount.UID caseInsensitiveCompare:[NSString stringWithUTF8String:acct->username]] == NSOrderedSame) {
				break;
			}
		}
	}
    return adiumPurpleAccount;
}

PurpleAccount* accountLookupFromAdiumAccount(CBPurpleAccount *adiumAccount)
{
	return [adiumAccount purpleAccount];
}

AIListContact* contactLookupFromBuddy(PurpleBuddy *buddy)
{
	//Get the node's ui_data
	AIListContact *theContact = (buddy ? (AIListContact *)buddy->node.ui_data : nil);

	//If the node does not have ui_data yet, we need to create a contact and associate it
	if (!theContact && buddy) {
		NSString	*UID;
	
		UID = [NSString stringWithUTF8String:purple_normalize(purple_buddy_get_account(buddy), purple_buddy_get_name(buddy))];
		
		theContact = [accountLookup(purple_buddy_get_account(buddy)) contactWithUID:UID];
		
		//Associate the handle with ui_data and the buddy with our statusDictionary
		buddy->node.ui_data = [theContact retain];
		
		//This is the first time the contact has been accessed from the buddy; reset the icon cache for it
		[AIUserIcons flushCacheForObject:theContact];
	}
	
	return theContact;
}

AIListContact* contactLookupFromIMConv(PurpleConversation *conv)
{
	return nil;
}

AIChat* groupChatLookupFromConv(PurpleConversation *conv)
{
	AIChat *chat;
	
	chat = (AIChat *)conv->ui_data;
	if (!chat) {
		NSString *name = [NSString stringWithUTF8String:purple_conversation_get_name(conv)];
		
		CBPurpleAccount *account = accountLookup(purple_conversation_get_account(conv));
        
        /* 
         * Need to start a new chat, associating with the PurpleConversation.
         *
         * This may call back through to us recursively, via:
         *   -[CBPurpleAccount chatWithContact:identifier:]
         *   -[AIChatController chatWithContact:]
         *   -[CBPurpleAccount openChat:]
         *   -[SLPurpleCocoaAdaper openChat:onAccount:]
         *   convLookupFromChat()
         *   groupChatLookupFromConv()
         *
         * That's fine, as we'll get the same lookups the second time through; we just need to be cautious.
         */
		chat = [account chatWithName:name identifier:[NSValue valueWithPointer:conv]];
		if (!chat.chatCreationDictionary) {
			// If we don't have a chat creation dictionary (i.e., we didn't initiate the join), create one.
			chat.chatCreationDictionary = [account extractChatCreationDictionaryFromConversation: conv];
		}
        if (conv->ui_data != chat) {
            [(AIChat *)(conv->ui_data) release];
            conv->ui_data = [chat retain];
        }
		AILog(@"group chat lookup assigned %@ to %p (%s)",chat,conv, purple_conversation_get_name(conv));
	}

	return chat;
}

AIChat* existingChatLookupFromConv(PurpleConversation *conv)
{
	return (conv ? conv->ui_data : nil);
}

AIChat* chatLookupFromConv(PurpleConversation *conv)
{
	switch(purple_conversation_get_type(conv)) {
		case PURPLE_CONV_TYPE_CHAT:
			return groupChatLookupFromConv(conv);
			break;
		case PURPLE_CONV_TYPE_IM:
			return imChatLookupFromConv(conv);
			break;
		default:
			return existingChatLookupFromConv(conv);
			break;
	}
}

AIChat* imChatLookupFromConv(PurpleConversation *conv)
{
	AIChat			*chat;
	
	chat = (AIChat *)conv->ui_data;

	if (!chat) {
		//No chat is associated with the IM conversation
		AIListContact   *sourceContact;
		PurpleBuddy		*buddy;
		PurpleAccount	*account;
		
		account = purple_conversation_get_account(conv);
//		AILog(@"%x purple_conversation_get_name(conv) %s; normalizes to %s",account,purple_conversation_get_name(conv),purple_normalize(account,purple_conversation_get_name(conv)));

		//First, find the PurpleBuddy with whom we are conversing
		buddy = purple_find_buddy(account, purple_conversation_get_name(conv));
		if (!buddy) {
			//No purple_buddy corresponding to the purple_conversation_get_name(conv) is on our list, so create one
			buddy = purple_buddy_new(account, purple_normalize(account, purple_conversation_get_name(conv)), NULL);	//create a PurpleBuddy
		}

		NSCAssert(buddy != nil, @"buddy was nil");

		sourceContact = contactLookupFromBuddy(buddy);
		/* 
         * Need to start a new chat, associating with the PurpleConversation.
         *
         * This may call back through to us recursively, via:
         *   -[CBPurpleAccount chatWithContact:identifier:]
         *   -[AIChatController chatWithContact:]
         *   -[CBPurpleAccount openChat:]
         *   -[SLPurpleCocoaAdaper openChat:onAccount:]
         *   convLookupFromChat()
         *   imChatLookupFromConv()
         *
         * That's fine, as we'll get the same lookups the second time through; we just need to be cautious.
         */
		chat = [accountLookup(account) chatWithContact:sourceContact identifier:[NSValue valueWithPointer:conv]];

		if (!chat) {
			NSString	*errorString;

			errorString = [NSString stringWithFormat:@"conv %x: Got nil chat in lookup for sourceContact %@ (%x ; \"%s\" ; \"%s\") on adiumAccount %@ (%x ; \"%s\")",
				conv,
				sourceContact,
				buddy,
				(buddy ? purple_buddy_get_name(buddy) : ""),
				((buddy && purple_buddy_get_account(buddy) && purple_buddy_get_name(buddy)) ? purple_normalize(purple_buddy_get_account(buddy), purple_buddy_get_name(buddy)) : ""),
				accountLookup(account),
				account,
				(account ? purple_account_get_username(account) : "")];

			NSCAssert(chat != nil, errorString);
		}

		//Associate the PurpleConversation with the AIChat
        if (conv->ui_data != chat) {
            [(AIChat *)(conv->ui_data) release];
            conv->ui_data = [chat retain];
        }
	}
    
	return chat;	
}

PurpleConversation* convLookupFromChat(AIChat *chat, id adiumAccount)
{
	PurpleConversation	*conv = [[chat identifier] pointerValue];
	PurpleAccount		*account = accountLookupFromAdiumAccount(adiumAccount);

	if (!conv && adiumAccount && purple_account_get_connection(account)) {
		AIListObject *listObject = chat.listObject;
		
		//If we have a listObject, we are dealing with a one-on-one chat, so proceed accordingly
		if (listObject) {
			char *destination;
			
			destination = g_strdup(purple_normalize(account, [listObject.UID UTF8String]));
			
			conv = purple_conversation_new(PURPLE_CONV_TYPE_IM, account, destination);
			
			//associate the AIChat with the purple conv
			if (conv) imChatLookupFromConv(conv);

			g_free(destination);
			
		} else {
			//Otherwise, we have a multiuser chat.
			
			//All multiuser chats should have a non-nil name.
			NSString	*chatName = chat.name;
			if (chatName) {
				const char *name = [chatName UTF8String];
				
				/*
				 Look for an existing purpleChat.  If we find one, our job is complete.
				 
				 We will never find one if we are joining a chat on our own (via the Join Chat dialogue).
				 
				 We should never get to this point if we were invited to a chat, as groupChatLookupFromConv(),
				 which was called when we accepted the invitation and got the chat information from Purple,
				 will have associated the PurpleConversation with the chat and we would have stopped after
				 [[chat identifier] pointerValue] above.
				 
				 However, there's no reason not to check just in case.
				 */
				PurpleChat *purpleChat = purple_blist_find_chat(account, name);
				if (!purpleChat) {
					
					/*
					 If we don't have a PurpleChat with this name on this account, we need to create one.
					 Our chat, which should have been created via the Adium Join Chat API, should have
					 a ChatCreationInfo property with the information we need to ask Purple to
					 perform the join.
					 */
					NSDictionary	*chatCreationInfo = [chat valueForProperty:@"ChatCreationInfo"];
					chatCreationInfo = [(CBPurpleAccount *)chat.account willJoinChatUsingDictionary:chatCreationInfo];

					if (!chatCreationInfo) {
						AILog(@"*** No chat creation info for %@ on %@",chat,adiumAccount);
						return NULL;
					}
					
					AILog(@"Creating a chat with name %s (Creation info: %@).", name, chatCreationInfo);

					GHashTable				*components;
					
					//Prpl Info
					PurpleConnection		*gc = purple_account_get_connection(account);
					GList					*list, *tmp;
					struct proto_chat_entry *pce;

					g_return_val_if_fail(gc != NULL, NULL);

					//Create a hash table
					//The hash table should contain char* objects created via a g_strdup method
					components = g_hash_table_new_full(g_str_hash, g_str_equal,
													   g_free, g_free);
					
					for (NSString *identifier in chatCreationInfo) {
						id		value = [chatCreationInfo objectForKey:identifier];
						char	*valueUTF8String = NULL;
						
						if ([value isKindOfClass:[NSNumber class]]) {
							valueUTF8String = g_strdup_printf("%d",[value intValue]);

						} else if ([value isKindOfClass:[NSString class]]) {
							valueUTF8String = g_strdup([value UTF8String]);

						} else {
							AILog(@"Invalid value %@ for identifier %@",value,identifier);
						}
						
						//Store our chatCreationInfo-supplied value in the compnents hash table
						if (valueUTF8String) {
							g_hash_table_replace(components,
												 g_strdup([identifier UTF8String]),
												 valueUTF8String);
						}
					}

					//In debug mode, verify we didn't miss any required values
					if (AIDebugLoggingIsEnabled()) {
						/* Get the chat_info for our desired account.  This will be a GList of proto_chat_entry
						 * objects, each of which has a label and identifier.  Each may also have is_int, with a minimum
						 * and a maximum integer value.
						 */
						if ((PURPLE_PLUGIN_PROTOCOL_INFO(gc->prpl))->chat_info)
						{
							list = (PURPLE_PLUGIN_PROTOCOL_INFO(gc->prpl))->chat_info(gc);

							//Look at each proto_chat_entry in the list to verify we have it in chatCreationInfo
							for (tmp = list; tmp; tmp = tmp->next)
							{
								pce = tmp->data;
								char	*identifier = g_strdup(pce->identifier);
								
								NSString	*value = [chatCreationInfo objectForKey:[NSString stringWithUTF8String:identifier]];
								if (!value) {
									AILog(@"Danger, Will Robinson! %s is in the proto_info but can't be found in %@",identifier,chatCreationInfo);
								}
								
								g_free(identifier);
							}
						}
					}

					/* Join the chat serverside - the GHashTable components, coupled with the originating PurpleConnection,
					 * now contains all the information the prpl will need to process our request.
					 */
					AILog(@"In the event of an emergency, your GHashTable may be used as a flotation device...");
					serv_join_chat(gc, components);
					g_hash_table_unref(components);
				}
			}
		}
	}
	
	return conv;
}

PurpleConversation* existingConvLookupFromChat(AIChat *chat)
{
	return (PurpleConversation *)[[chat identifier] pointerValue];
}

void* adium_purple_get_handle(void)
{
	static int adium_purple_handle;
	
	return &adium_purple_handle;
}

#pragma mark Images

static NSString *_messageImageCachePathWithoutExtension(int imageID, AIAccount* adiumAccount)
{
    NSString    *messageImageCacheFilename = [NSString stringWithFormat:@"TEMP-Image_%@_%i", adiumAccount.internalObjectID, imageID];
    return [[adium cachesPath] stringByAppendingPathComponent:messageImageCacheFilename];
}

NSString *processPurpleImages(NSString* inString, AIAccount* adiumAccount)
{
	NSScanner			*scanner;
    NSString			*chunkString = nil;
    NSMutableString		*newString;
	NSString			*targetString = @"<IMG ID=";
	NSCharacterSet		*quoteApostropheCharacterSet = [NSCharacterSet characterSetWithCharactersInString:@"\"\'"];
    int imageID;
	
	if ([inString rangeOfString:targetString options:NSCaseInsensitiveSearch].location == NSNotFound) {
		return inString;
	}
	
    //set up
	newString = [[NSMutableString alloc] init];
	
    scanner = [NSScanner scannerWithString:inString];
    [scanner setCharactersToBeSkipped:[NSCharacterSet characterSetWithCharactersInString:@""]];
	
	//A purple image tag takes the form <IMG ID='12'></IMG> where 12 is the reference for use in PurpleStoredImage* purple_imgstore_get(int)
	
	//Parse the incoming HTML
    while (![scanner isAtEnd]) {
		//Find the beginning of a purple IMG ID tag
		if ([scanner scanUpToString:targetString intoString:&chunkString]) {
			[newString appendString:chunkString];
		}
		
		if ([scanner scanString:targetString intoString:&chunkString]) {
			//Skip past a quote or apostrophe
			[scanner scanCharactersFromSet:quoteApostropheCharacterSet intoString:NULL];
			
			//Get the image ID from the tag
			[scanner scanInt:&imageID];

			//Skip past a quote or apostrophe
			[scanner scanCharactersFromSet:quoteApostropheCharacterSet intoString:NULL];

			//Scan past a >
			[scanner scanString:@">" intoString:nil];
			
			//Get the image, then write it out as a png
			PurpleStoredImage		*purpleImage = purple_imgstore_find_by_id(imageID);
			if (purpleImage) {
				NSString		*filename = (purple_imgstore_get_filename(purpleImage) ?
											 [NSString stringWithUTF8String:purple_imgstore_get_filename(purpleImage)] :
											 @"Image");
				NSString		*imagePath = _messageImageCachePathWithoutExtension(imageID, adiumAccount);
				
				//First make an NSImage, then request a TIFFRepresentation to avoid an obscure bug in the PNG writing routines
				//Exception: PNG writer requires compacted components (bits/component * components/pixel = bits/pixel)
				NSData *data = [NSData dataWithBytes:purple_imgstore_get_data(purpleImage)
											  length:purple_imgstore_get_size(purpleImage)];
				
				NSString *extension = [NSImage extensionForBitmapImageFileType:[NSImage fileTypeOfData:data]];
				if (!extension) {
					//We don't know what it is; try to make a png out of it
					NSImage				*image = [[NSImage alloc] initWithData:data];
					NSData				*imageTIFFData = [image TIFFRepresentation];
					NSBitmapImageRep	*bitmapRep = [NSBitmapImageRep imageRepWithData:imageTIFFData];
					
					data = [bitmapRep representationUsingType:NSPNGFileType properties:nil];
					extension = @"png";
					[image release];
				}
				
				filename = [filename stringByAppendingPathExtension:extension];
				imagePath = [imagePath stringByAppendingPathExtension:extension];

				//If writing the file is successful, write an <IMG SRC="filepath"> tag to our string; the 'scaledToFitImage' class lets us apply CSS to directIM images only
				if ([data writeToFile:imagePath atomically:YES]) {
					[newString appendString:[NSString stringWithFormat:@"<IMG CLASS=\"scaledToFitImage\" SRC=\"%@\" ALT=\"%@\">",
						imagePath, filename]];	
				}

			} else {
				//If we didn't get a purpleImage, just leave the tag for now.. maybe it was important?
				[newString appendFormat:@"<IMG ID=\"%i\">",chunkString];
			}
		}
	}

	return ([newString autorelease]);
}

#pragma mark Notify
// Notify ----------------------------------------------------------------------------------------------------------
// We handle the notify messages within SLPurpleCocoaAdapter so we can use our localized string macro
- (void *)handleNotifyMessageOfType:(PurpleNotifyType)type withTitle:(const char *)title primary:(const char *)primary secondary:(const char *)secondary;
{

    NSString *primaryString = [NSString stringWithUTF8String:primary];
	NSString *secondaryString = secondary ? [NSString stringWithUTF8String:secondary] : nil;
	
	NSString *titleString;
	if (title) {
		titleString = [NSString stringWithFormat:AILocalizedString(@"Adium Notice: %@",nil),[NSString stringWithUTF8String:title]];
	} else {
		titleString = AILocalizedString(@"Adium : Notice", nil);
	}
	
	NSString *errorMessage = nil;
	NSString *description = nil;
	
	if (primary && strcmp(primary, _("Already there")) == 0) 
		return NULL;

	//Suppress notification warnings we have no interest in seeing
	if (secondaryString) {
		if ((strcmp(secondary, _("Not supported by host")) == 0) || /* OSCAR */
			(strcmp(secondary, _("Not logged in")) == 0) || /* OSCAR */
			(strcmp(secondary, _("Your buddy list was downloaded from the server.")) == 0) || /* Gadu-gadu */
			(strcmp(secondary, _("Your buddy list was stored on the server.")) == 0) /* Gadu-gadu */) {
			return NULL;
		}
		
		if ([secondaryString isEqualToString:
			 [NSString stringWithFormat:[NSString stringWithUTF8String:_("Could not add the buddy %s for an unknown reason.")], "1"]]) {
			/* Rather random error displayed by OSCAR (since forever, as of libpurple 2.4.0) for some clients while connecting */
			return NULL;
		}
		
		if ([secondaryString rangeOfString:@"Your contact is using Windows Live"].location != NSNotFound) {
			 /* Yahoo without MSN support - English string from the server */
			return NULL;
		}

	} 
	
	if ([primaryString rangeOfString: @"did not get sent"].location != NSNotFound) {
		//Oscar send error
		//This may not ever occur as of libpurple 2.4.0; I can't find the phrase 'did not get sent' in any of the code. -evands
		NSString *targetUserName = [[[[primaryString componentsSeparatedByString:@" message to "] objectAtIndex:1] componentsSeparatedByString:@" did not get "] objectAtIndex:0];
		
		errorMessage = [NSString stringWithFormat:AILocalizedString(@"Your message to %@ did not get sent",nil),targetUserName];
		
		if (secondaryString) {
			if ([secondaryString rangeOfString:[NSString stringWithUTF8String:_("Rate")]].location != NSNotFound) {
				description = AILocalizedString(@"You are sending messages too quickly; wait a moment and try again.",nil);
			} else if ([secondaryString rangeOfString:[NSString stringWithUTF8String:_("Service unavailable")]].location != NSNotFound ||
					   [secondaryString rangeOfString:[NSString stringWithUTF8String:_("Not logged in")]].location != NSNotFound) {
				description = AILocalizedString(@"Connection error.",nil);
				
			} else if ([secondaryString rangeOfString:[NSString stringWithUTF8String:_("Refused by client")]].location != NSNotFound) {
				description = AILocalizedString(@"Your message was refused by the other user.",nil);
				
			} else if ([secondaryString rangeOfString:[NSString stringWithUTF8String:_("Reply too big")]].location != NSNotFound) {
				description = AILocalizedString(@"Your message was too big.",nil);
				
			} else if ([secondaryString rangeOfString:[NSString stringWithUTF8String:_("In local permit/deny")]].location != NSNotFound) {
				description = AILocalizedString(@"The other user is in your deny list.",nil);
				
			} else if ([secondaryString rangeOfString:[NSString stringWithUTF8String:_("Too evil")]].location != NSNotFound) {
				description = AILocalizedString(@"Warning level is too high.",nil);
				
			} else if ([secondaryString rangeOfString:[NSString stringWithUTF8String:_("User temporarily unavailable")]].location != NSNotFound) {
				description = AILocalizedString(@"The other user is temporarily unavailable.",nil);
			}
		}
		
		if (!description)
			description = AILocalizedString(@"No reason was given.",nil);
    }
	
	//If we didn't grab a translated version, at least display the English version Purple supplied
	[adium.interfaceController handleMessage:([errorMessage length] ? errorMessage : primaryString)
							   withDescription:([description length] ? description : ([secondaryString length] ? secondaryString : @"") )
							   withWindowTitle:titleString];
	
	return NULL;
}

/* XXX ugly */
- (void *)handleNotifyFormattedWithTitle:(const char *)title primary:(const char *)primary secondary:(const char *)secondary text:(const char *)text
{
	NSString *titleString = (title ? [NSString stringWithUTF8String:title] : nil);
	NSString *primaryString = (primary ? [NSString stringWithUTF8String:primary] : nil);
	
	if (!titleString) {
		titleString = primaryString;
		primaryString = nil;
	}
	
	NSString *secondaryString = (secondary ? [NSString stringWithUTF8String:secondary] : nil);
	if (!primaryString) {
		primaryString = secondaryString;
		secondaryString = nil;
	}
	
	static AIHTMLDecoder	*notifyFormattedHTMLDecoder = nil;
	if (!notifyFormattedHTMLDecoder) notifyFormattedHTMLDecoder = [[AIHTMLDecoder decoder] retain];

	NSString	*textString = (text ? [NSString stringWithUTF8String:text] : nil); 
	if (textString) textString = [[notifyFormattedHTMLDecoder decodeHTML:textString] string];
	
	NSString	*description = nil;
	if ([textString length] && [secondaryString length]) {
		description = [NSString stringWithFormat:@"%@\n\n%@",secondaryString,textString];
		
	} else if (textString) {
		description = textString;
		
	} else if (secondaryString) {
		description = secondaryString;
		
	}
	
	NSString	*message = primaryString;
	
	[adium.interfaceController handleMessage:(message ? message : @"")
							   withDescription:(description ? description : @"")
							   withWindowTitle:(titleString ? titleString : @"")];

	return NULL;
}


#pragma mark File transfers
- (void)displayFileSendError
{
	[adium.interfaceController handleMessage:AILocalizedString(@"File Send Error",nil)
							   withDescription:AILocalizedString(@"An error was encoutered sending the file.",nil)
							   withWindowTitle:AILocalizedString(@"File Send Error",nil)];
}

#pragma mark Thread accessors
- (void)disconnectAccount:(id)adiumAccount
{
	PurpleAccount *account = accountLookupFromAdiumAccount(adiumAccount);
	AILog(@"Setting %x disabled and offline (%s)...",account,
		  purple_status_type_get_id(purple_account_get_status_type_with_primitive(account, PURPLE_STATUS_OFFLINE)));

	purple_account_set_enabled(account, "Adium", NO);
}

- (void)registerAccount:(id)adiumAccount
{
	purple_account_set_register_callback(accountLookupFromAdiumAccount(adiumAccount), adiumPurpleAccountRegisterCb, adiumAccount);
	purple_account_register(accountLookupFromAdiumAccount(adiumAccount));
}

static void purpleUnregisterCb(PurpleAccount *account, gboolean success, void *user_data) {
	[(CBPurpleAccount*)user_data unregisteredAccount:success?YES:NO];
}

- (void)unregisterAccount:(id)adiumAccount
{
	purple_account_unregister(accountLookupFromAdiumAccount(adiumAccount), purpleUnregisterCb, adiumAccount);
}

//Called on the purple thread, actually performs the specified command (it should have already been tested by 
//attemptPurpleCommandOnMessage:... below.
- (BOOL)doCommand:(NSString *)originalMessage
			fromAccount:(id)sourceAccount
				 inChat:(AIChat *)chat
{
	PurpleConversation	*conv = convLookupFromChat(chat, sourceAccount);
	PurpleCmdStatus		status;
	char				*markup, *error;
	const char			*cmd;
	BOOL				didCommand = NO;

	if (!conv || ([originalMessage length] < 2)) return NO;
	
	cmd = [originalMessage UTF8String];
	
	//cmd+1 will be the cmd without the leading character, which should be "/"
	markup = g_markup_escape_text(cmd+1, -1);
	status = purple_cmd_do_command(conv, cmd+1, markup, &error);
	g_free(markup);
	
	//The only error status which is possible now is either 
	switch (status) {
		case PURPLE_CMD_STATUS_FAILED:
		{
			purple_conv_present_error(purple_conversation_get_name(conv), purple_conversation_get_account(conv), "Command failed");
			didCommand = YES;
			break;
		}	
		case PURPLE_CMD_STATUS_WRONG_ARGS:
		{
			purple_conv_present_error(purple_conversation_get_name(conv), purple_conversation_get_account(conv), "Wrong number of arguments");
			didCommand = YES;			
			break;
		}
		case PURPLE_CMD_STATUS_OK:
			didCommand = YES;
			break;
		case PURPLE_CMD_STATUS_NOT_FOUND:
		case PURPLE_CMD_STATUS_WRONG_TYPE:
		case PURPLE_CMD_STATUS_WRONG_PRPL:
			/* Ignore this command and let the message send; the user probably doesn't even know what they typed is a command */
			didCommand = NO;
			break;
	}

	return didCommand;
}

/*!
 * @brief Check a message for purple / commands=
 *
 * @result YES if a command was performed; NO if it was not
 */
- (BOOL)attemptPurpleCommandOnMessage:(NSString *)originalMessage fromAccount:(AIAccount *)sourceAccount inChat:(AIChat *)chat
{
	BOOL				didCommand = NO;
	
	if ([originalMessage hasPrefix:@"/"]) {	
		didCommand = [self doCommand:originalMessage
						 fromAccount:sourceAccount
							  inChat:chat];
	}

	return didCommand;
}

/*!
 * @brief Send a notification over a service which supports that
 *
 * This should not be called for an account whose service doesn't support sending notifications (check before calling).
 * Doing so will return without displaying an error; the message should be sent as a normal message in this case.
 *
 * @param type An AINotificationType.
 * @param sourceAccount The account from which to send
 * @param chat The chat in which to send the notification
 */
- (void)sendNotificationOfType:(AINotificationType)type
				   fromAccount:(id)sourceAccount
						inChat:(AIChat *)chat
{
	PurpleConversation	*conv = convLookupFromChat(chat,sourceAccount);

	purple_prpl_send_attention(purple_conversation_get_gc(conv),
							   purple_conversation_get_name(conv),
							   type);
}

//Returns YES if the message was sent (and should therefore be displayed).  Returns NO if it was not sent or was otherwise used.
- (void)sendEncodedMessage:(NSString *)encodedMessage
			   fromAccount:(id)sourceAccount
					inChat:(AIChat *)chat
				 withFlags:(PurpleMessageFlags)flags
{	
	const char *encodedMessageUTF8String;
	
	if (encodedMessage && (encodedMessageUTF8String = [encodedMessage UTF8String])) {
		PurpleConversation	*conv = convLookupFromChat(chat,sourceAccount);

		switch (purple_conversation_get_type(conv)) {				
			case PURPLE_CONV_TYPE_IM: {
				PurpleConvIm			*im = purple_conversation_get_im_data(conv);
				purple_conv_im_send_with_flags(im, encodedMessageUTF8String, flags);
				break;
			}

			case PURPLE_CONV_TYPE_CHAT: {
				PurpleConvChat	*purpleChat = purple_conversation_get_chat_data(conv);
				purple_conv_chat_send(purpleChat, encodedMessageUTF8String);
				break;
			}
			
			case PURPLE_CONV_TYPE_ANY:
				AILog(@"What in the world? Got PURPLE_CONV_TYPE_ANY.");
				break;

			case PURPLE_CONV_TYPE_MISC:
			case PURPLE_CONV_TYPE_UNKNOWN:
				break;
		}
	} else {
		AILog(@"*** Error encoding %@ to UTF8",encodedMessage);
	}
}

- (void)sendTyping:(AITypingState)typingState inChat:(AIChat *)chat
{
	PurpleConversation *conv = convLookupFromChat(chat,nil);
	if (conv) {
		//		BOOL isTyping = (([typingState intValue] == AINotTyping) ? FALSE : TRUE);

		PurpleTypingState purpleTypingState;
		
		switch (typingState) {
			case AINotTyping:
			default:
				purpleTypingState = PURPLE_NOT_TYPING;
				break;
			case AITyping:
				purpleTypingState = PURPLE_TYPING;
				break;
			case AIEnteredText:
				purpleTypingState = PURPLE_TYPED;
				break;
		}
	
		serv_send_typing(purple_conversation_get_gc(conv),
						 purple_conversation_get_name(conv),
						 purpleTypingState);
	}	
}

- (void)addUID:(NSString *)objectUID onAccount:(id)adiumAccount toGroup:(NSString *)groupName withAlias:(NSString *)alias
{
	PurpleAccount *account = accountLookupFromAdiumAccount(adiumAccount);
	const char	*groupUTF8String, *buddyUTF8String, *aliasUTF8String;
	PurpleGroup	*group;
	PurpleBuddy	*buddy;
	
	//Find the group (Create if necessary)
	groupUTF8String = (groupName ? [groupName UTF8String] : "Buddies");
	if (!(group = purple_find_group(groupUTF8String))) {
		group = purple_group_new(groupUTF8String);
		purple_blist_add_group(group, NULL);
	}
	
	buddyUTF8String = [objectUID UTF8String];
	aliasUTF8String = alias.length ? [alias UTF8String] : NULL;
	
	// Find an existing buddy in the group.
	buddy = purple_find_buddy_in_group(account, buddyUTF8String, group);
	if (!buddy) buddy = purple_buddy_new(account, buddyUTF8String, aliasUTF8String);

	AILog(@"Adding buddy %s to group %s with alias %s",purple_buddy_get_name(buddy), group->name, aliasUTF8String);

	/* purple_blist_add_buddy() will move an existing contact serverside, but will not add a buddy serverside.
	 * We're working with a new contact, hopefully, so we want to call serv_add_buddy() after modifying the purple list.
	 * This is the order done in add_buddy_cb() in gtkblist.c */
	purple_blist_add_buddy(buddy, NULL, group, NULL);
	purple_account_add_buddy(account, buddy);
}

- (void)removeUID:(NSString *)objectUID onAccount:(id)adiumAccount fromGroup:(NSString *)groupName
{
	const char	*groupUTF8String;
	PurpleGroup	*group;
	
	// Find the right buddy; group -> buddy in group -> remove that buddy
	
	groupUTF8String = (groupName ? [groupName UTF8String] : "Buddies");
	if ((group = purple_find_group(groupUTF8String))) {
		PurpleAccount *account = accountLookupFromAdiumAccount(adiumAccount);
		PurpleBuddy 	*buddy;
		
		if ((buddy = purple_find_buddy_in_group(account, [objectUID UTF8String], group))) {
			/* Remove this contact from the server-side and purple-side lists. 
			 * Updating purpleside does not change the server.
			 *
			 * Purple has a commented XXX as to whether this order or the reverse (blist, then serv) is correct.
			 * We'll use the order which purple uses as of purple 1.1.4. */
			
			AILog(@"Removing buddy %s from group %s", purple_buddy_get_name(buddy), purple_group_get_name(purple_buddy_get_group(buddy)));
			
			purple_account_remove_buddy(account, buddy, group);
			purple_blist_remove_buddy(buddy);
		}
	}
}

- (void)moveUID:(NSString *)objectUID onAccount:(id)adiumAccount fromGroups:(NSSet *)oldGroups toGroups:(NSSet *)groupNames withAlias:(NSString *)alias;
{
	PurpleAccount *account = accountLookupFromAdiumAccount(adiumAccount);
	
	for (NSString *groupName in groupNames) {
		if (!oldGroups.count) {
			// If we don't have any source groups, silently turn this into an add.
			AILog(@"Move of %@ failed because we have no oldGroups; adding instead", objectUID);
			[self addUID:objectUID onAccount:adiumAccount toGroup:groupName withAlias:alias];
			continue;
		}
		
		PurpleGroup *group;
		const char *groupUTF8String = (groupName ? [groupName UTF8String] : "Buddies");
		
		// Find the PurpleGroup, otherwise create a new one.
		if (!(group = purple_find_group(groupUTF8String))) {
			group = purple_group_new(groupUTF8String);
			purple_blist_add_group(group, NULL);
		}
		
		for (NSString *sourceGroupName in oldGroups) {
			PurpleGroup *oldGroup;
			
			if ((oldGroup = purple_find_group([sourceGroupName UTF8String]))) {
				PurpleBuddy *buddy;	
				
				if ((buddy = purple_find_buddy_in_group(account, [objectUID UTF8String], oldGroup))) {
					// Perform the add to the new group. This will turn into a move, and will update serverside.

					if (strcmp(purple_account_get_protocol_id(account), "prpl-yahoo") == 0) {
						/* XXX File a bug report with the need for this special-case w/ libpurple -evands 10/14/10 */

						/* Work around a Yahoo! bug in which buddies in multiple groups can't be moved properly.
						 *
						 * Traverse all buddies on this account.
						 * If the buddy is in the old group (it must be, for us to reach this point given the if
						 * statement above) and is also in another group, we need to remove it from the old group before
						 * this move. Otherwise, it won't work. However, if we remove it from the old group and it *isn't* in 
						 * another group already, Yahoo will force reauthorization, which is ugly.  */
						GSList	*buddies = purple_find_buddies(account, [objectUID UTF8String]);
						
						BOOL isInGroupBesidesOldGroup = NO;
						for (GSList	*bb = buddies; bb != NULL; bb = bb->next) {
							PurpleBuddy *aBuddy = (PurpleBuddy *)bb->data;
							if (purple_buddy_get_group(aBuddy) != oldGroup) {
								isInGroupBesidesOldGroup = YES;
							}
						}
	
						if (isInGroupBesidesOldGroup) {
							purple_account_remove_buddy(account, buddy, oldGroup);
							AILog(@"Removed because it met the Yahoo! workaround criteria");
						}

					}
					
					purple_blist_add_buddy(buddy, NULL, group, NULL);
					// Continue so we avoid the "add to group" code below.
					continue;
				}
			}
			
			// If we got this far, the move failed; turn into an add.
			AILog(@"Move of %@ failed; adding instead", objectUID);
			[self addUID:objectUID onAccount:adiumAccount toGroup:groupName withAlias:alias];
		}
	}
}

- (void)renameGroup:(NSString *)oldGroupName onAccount:(id)adiumAccount to:(NSString *)newGroupName
{
    PurpleGroup *group = purple_find_group([oldGroupName UTF8String]);
	
	//If we don't have a group with this name, just ignore the rename request
    if (group) {
		//Rename purpleside, which will rename serverside as well
		purple_blist_rename_group(group, [newGroupName UTF8String]);
	}
}

- (void)deleteGroup:(NSString *)groupName onAccount:(id)adiumAccount
{
	PurpleGroup *group = purple_find_group([groupName UTF8String]);
	
	if (group) {
		purple_blist_remove_group(group);
	}
}

#pragma mark Alias
- (void)setAlias:(NSString *)alias forUID:(NSString *)UID onAccount:(id)adiumAccount
{
	PurpleAccount *account = accountLookupFromAdiumAccount(adiumAccount);
	if (purple_account_is_connected(account)) {
		const char  *uidUTF8String = [UID UTF8String];
		PurpleBuddy   *buddy = purple_find_buddy(account, uidUTF8String);
		const char  *aliasUTF8String = [alias UTF8String];
		const char	*oldAlias = (buddy ? purple_buddy_get_alias(buddy) : nil);
	
		if (buddy && ((aliasUTF8String && !oldAlias) ||
					  (!aliasUTF8String && oldAlias) ||
					  ((oldAlias && aliasUTF8String && (strcmp(oldAlias,aliasUTF8String) != 0))))) {

			purple_blist_alias_buddy(buddy,aliasUTF8String);
			serv_alias_buddy(buddy);
			
			//If we had an alias before but no longer have, adiumPurpleBlistUpdate() is not going to send the update
			//(Because normally it's wasteful to send a nil alias to the account).  We need to manually invoke the update.
			if (oldAlias && !alias) {
				AIListContact *theContact = contactLookupFromBuddy(buddy);
				
				[adiumAccount updateContact:theContact
									toAlias:nil];
			}
		}
	}
}

#pragma mark Chats
- (void)openChat:(AIChat *)chat onAccount:(id)adiumAccount
{
	//Looking up the conv from the chat will create the PurpleConversation purpleside, joining the chat, opening the server
	//connection, or whatever else is done when a chat is opened.
	convLookupFromChat(chat,adiumAccount);
}

- (void)closeChat:(AIChat *)chat
{
	PurpleConversation *conv = existingConvLookupFromChat(chat);

	if (conv) {
		[chat setIdentifier:nil];

		/* We retained the chat when setting it as the ui_data; we are releasing here, so be sure to set conv->ui_data
		 * to nil so we don't try to do it again.
		 */
        AILogWithSignature(@"Destroying %p (and releasing chat %p)", conv, conv->ui_data);

		[(AIChat *)conv->ui_data release];
		conv->ui_data = nil;

		//Tell purple to destroy the conversation.
		purple_conversation_destroy(conv);
	}	
}

- (void)inviteContact:(AIListContact *)listContact toChat:(AIChat *)chat withMessage:(NSString *)inviteMessage;
{
	PurpleConversation	*conv;
	PurpleAccount			*account;
	PurpleConvChat		*purpleChat;
	AIAccount			*adiumAccount = chat.account;
	
	AILog(@"#### inviteContact:%@ toChat:%@",listContact.UID,chat.name);
	// dchoby98
	if (([adiumAccount isKindOfClass:[CBPurpleAccount class]]) &&
	   (conv = convLookupFromChat(chat, adiumAccount)) &&
	   (account = accountLookupFromAdiumAccount((CBPurpleAccount *)adiumAccount)) &&
	   (purpleChat = purple_conversation_get_chat_data(conv))) {

		//PurpleBuddy		*buddy = purple_find_buddy(account, [listObject.UID UTF8String]);
		AILog(@"#### addChatUser chat: %@ (%@) buddy: %@",chat.name, chat,listContact.UID);
		serv_chat_invite(purple_conversation_get_gc(conv),
						 purple_conv_chat_get_id(purpleChat),
						 (inviteMessage ? [inviteMessage UTF8String] : ""),
						 [listContact.UID UTF8String]);
		
	}
}

- (void)createNewGroupChat:(AIChat *)chat withListContact:(AIListContact *)contact
{
	//Create the chat
	convLookupFromChat(chat, chat.account);
	
	//Invite the contact, with no message
	[self inviteContact:contact toChat:chat withMessage:nil];
}

- (BOOL)contact:(AIListContact *)inContact isIgnoredInChat:(AIChat *)inChat
{
	PurpleConversation *conv = existingConvLookupFromChat(inChat);
	
	if (!conv)
		return NO;
	
	PurpleConvChat *convChat = purple_conversation_get_chat_data(conv);

	return (purple_conv_chat_is_user_ignored(convChat, [inContact.UID UTF8String]) ? YES : NO);
}

- (void)setContact:(AIListContact *)inContact ignored:(BOOL)inIgnored inChat:(AIChat *)inChat
{
	PurpleConversation *conv = existingConvLookupFromChat(inChat);
	
	if (!conv)
		return;
	
	PurpleConvChat *convChat = purple_conversation_get_chat_data(conv);
	
	if ([self contact:inContact isIgnoredInChat:inChat]) {
		purple_conv_chat_unignore(convChat, [inContact.UID UTF8String]);
	} else {
		purple_conv_chat_ignore(convChat, [inContact.UID UTF8String]);
	}
}

#pragma mark Account Status
/*!
 * @brief Generate a GList from a dictionary
 *
 * @param arguments An NSDictionary, whose keys and values will be used to form alternating key-value items in the GList 
 *
 * @result A GList, which the caller is responsible for freeing
 */
GList *createListFromDictionary(NSDictionary *arguments)
{
	GList *attrs = NULL;

	if ([arguments count]) {
		for (NSString *key in arguments) {
			id	valueObject;

			if ((valueObject = [arguments objectForKey:key])) {
				const char *value = NULL;

				if ([valueObject isKindOfClass:[NSNumber class]])
					value = GINT_TO_POINTER([valueObject intValue]);
				else if ([valueObject isKindOfClass:[NSString class]])
					value = [valueObject UTF8String];
				else
					AILogWithSignature(@"Warning: unknown class %@ (%@) for key %@",
									   NSStringFromClass([valueObject class]), valueObject, key);

				//Append the key
				attrs = g_list_append(attrs, (gpointer)[key UTF8String]);

				//Now append the value
				attrs = g_list_append(attrs, (gpointer)value);

			} else {
				AILogWithSignature(@"Warning: could not determine value of %@ for key %@",
								   valueObject, key);
			}
		}
	}
	
	return attrs;
}

- (void)setStatusID:(const char *)statusID 
		   isActive:(NSNumber *)isActive
		  arguments:(NSMutableDictionary *)arguments
		  onAccount:(id)adiumAccount
{
	PurpleAccount	*account = accountLookupFromAdiumAccount(adiumAccount);
	GList			*attrs = createListFromDictionary(arguments);

	AILog(@"Setting status on %x (%s): ID %s, isActive %i, attributes %@",account, purple_account_get_username(account),
		  statusID, [isActive boolValue], arguments);

	purple_account_set_status_list(account, statusID, [isActive boolValue], attrs);
	g_list_free(attrs);

	if (purple_status_is_online(purple_account_get_active_status(account)) &&
		purple_account_is_disconnected(account))  {
		//This status is an online status, but the account is not connected or connecting

		//Ensure the account is enabled
		if (!purple_account_get_enabled(account, "Adium")) {
			purple_account_set_enabled(account, "Adium", YES);
		}

		//Now connect the account
		purple_account_connect(account);
	}	
}

- (void)setSongInformation:(NSDictionary *)arguments onAccount:(id)adiumAccount
{
	PurpleAccount	*account = accountLookupFromAdiumAccount(adiumAccount);
	GList			*attrs;
	PurpleStatus	*tune;

	tune = purple_presence_get_status(purple_account_get_presence(account), "tune");
	if (!tune)
		return;
	
	if (!arguments)
		purple_status_set_active(tune, FALSE);
	else
	{
		attrs = createListFromDictionary(arguments);
		purple_status_set_active_with_attrs_list(tune, TRUE, attrs);
		g_list_free(attrs);
	}
}	

- (void)setInfo:(NSString *)profileHTML onAccount:(id)adiumAccount
{
	PurpleAccount 	*account = accountLookupFromAdiumAccount(adiumAccount);
	const char *profileHTMLUTF8 = [profileHTML UTF8String];

	purple_account_set_user_info(account, profileHTMLUTF8);

	if (purple_account_get_connection(account) != NULL && purple_account_is_connected(account)) {
		serv_set_info(purple_account_get_connection(account), profileHTMLUTF8);
	}
}

- (void)setBuddyIcon:(NSData *)buddyImageData onAccount:(id)adiumAccount
{
	PurpleAccount *account = accountLookupFromAdiumAccount(adiumAccount);
	if (account) {
		unsigned len = [buddyImageData length];
		/* purple_buddy_icons_set_account_icon() takes responsibility for the buddy icon memory */
		purple_buddy_icons_set_account_icon(account, g_memdup([buddyImageData bytes], len), len);
	}
}

- (void)setIdleSinceTo:(NSDate *)idleSince onAccount:(id)adiumAccount
{
	PurpleAccount *account = accountLookupFromAdiumAccount(adiumAccount);
	if (purple_account_is_connected(account)) {
		NSTimeInterval idle = (idleSince != nil ? [idleSince timeIntervalSince1970] : 0);
		PurplePresence *presence;

		presence = purple_account_get_presence(account);

		purple_presence_set_idle(presence, (idle > 0), idle);
	}
}

#pragma mark Get Info
- (void)getInfoFor:(NSString *)inUID onAccount:(id)adiumAccount
{
	PurpleAccount *account = accountLookupFromAdiumAccount(adiumAccount);
	if (purple_account_is_connected(account)) {
		serv_get_info(purple_account_get_connection(account), purple_normalize(account, [inUID UTF8String]));
	}
}

#pragma mark Xfer
- (void)xferRequest:(PurpleXfer *)xfer
{
	purple_xfer_request(xfer);
}

- (void)xferRequestAccepted:(PurpleXfer *)xfer withFileName:(NSString *)xferFileName
{
	//Only start the file transfer if it's still not marked as cancelled and therefore can be begun.
	if ((purple_xfer_get_status(xfer) != PURPLE_XFER_STATUS_CANCEL_LOCAL) &&
		(purple_xfer_get_status(xfer) != PURPLE_XFER_STATUS_CANCEL_REMOTE)) {
		//XXX should do further error checking as done by purple_xfer_choose_file_ok_cb() in purple's ft.c
		purple_xfer_request_accepted(xfer, [xferFileName UTF8String]);
	}
}

- (void)xferRequestRejected:(PurpleXfer *)xfer
{
	purple_xfer_request_denied(xfer);
}

- (void)xferCancel:(PurpleXfer *)xfer
{
	if ((purple_xfer_get_status(xfer) == PURPLE_XFER_STATUS_UNKNOWN) ||
		(purple_xfer_get_status(xfer) == PURPLE_XFER_STATUS_NOT_STARTED) ||
		(purple_xfer_get_status(xfer) == PURPLE_XFER_STATUS_STARTED) ||
		(purple_xfer_get_status(xfer) == PURPLE_XFER_STATUS_ACCEPTED)) {
		purple_xfer_cancel_local(xfer);
	}
}

#pragma mark Account settings
- (void)setCheckMail:(NSNumber *)checkMail forAccount:(id)adiumAccount
{
	PurpleAccount *account = accountLookupFromAdiumAccount(adiumAccount);
	BOOL		shouldCheckMail = [checkMail boolValue];

	purple_account_set_check_mail(account, shouldCheckMail);
}

- (void)setDefaultPermitDenyForAccount:(id)adiumAccount
{
	PurpleAccount *account = accountLookupFromAdiumAccount(adiumAccount);

	if (account && purple_account_get_connection(account)) {
		account->perm_deny = PURPLE_PRIVACY_DENY_USERS;
		serv_set_permit_deny(purple_account_get_connection(account));
	}	
}

#pragma mark Protocol specific accessors
- (void)OSCAREditComment:(NSString *)comment forUID:(NSString *)inUID onAccount:(id)adiumAccount
{
	PurpleAccount *account = accountLookupFromAdiumAccount(adiumAccount);
	if (purple_account_is_connected(account)) {
		PurpleBuddy   *buddy;
		PurpleGroup   *g;
		OscarData   *od;

		const char  *uidUTF8String = [inUID UTF8String];

		if ((buddy = purple_find_buddy(account, uidUTF8String)) &&
			(g = purple_buddy_get_group(buddy)) && 
			(od = purple_account_get_connection(account)->proto_data)) {
			aim_ssi_editcomment(od, purple_group_get_name(g), uidUTF8String, [comment UTF8String]);	
		}
	}
}

- (void)OSCARSetFormatTo:(NSString *)inFormattedUID onAccount:(id)adiumAccount
{
	PurpleAccount *account = accountLookupFromAdiumAccount(adiumAccount);

	if (account &&
		purple_account_is_connected(account) &&
		[inFormattedUID length]) {
		
		oscar_reformat_screenname(purple_account_get_connection(account), [inFormattedUID UTF8String]);
	}
}

#pragma mark Request callbacks

- (void)performContactMenuActionFromDict:(NSDictionary *)dict forAccount:(id)adiumAccount
{
	PurpleBuddy		*buddy = [[dict objectForKey:@"PurpleBuddy"] pointerValue];
	void (*callback)(gpointer, gpointer);
	
	//Perform act's callback with the desired buddy and data
	callback = [[dict objectForKey:@"PurpleMenuActionCallback"] pointerValue];
	if (callback)
		callback((PurpleBlistNode *)buddy, [[dict objectForKey:@"PurpleMenuActionData"] pointerValue]);
}

- (void)performAccountMenuActionFromDict:(NSDictionary *)dict forAccount:(id)adiumAccount
{
	PurplePluginAction *act;
	PurpleAccount		 *account = accountLookupFromAdiumAccount(adiumAccount);

	if (account && purple_account_get_connection(account)) {
		act = purple_plugin_action_new(NULL, [[dict objectForKey:@"PurplePluginActionCallback"] pointerValue]);
		if (act->callback) {
			act->plugin = purple_account_get_connection(account)->prpl;
			act->context = purple_account_get_connection(account);
			act->user_data = [[dict objectForKey:@"PurplePluginActionCallbackUserData"] pointerValue];
			act->callback(act);
		}
		purple_plugin_action_free(act);
	}
}

/*!
* @brief Call the purple callback to pass on an authorization response
 *
 * @param inCallBackValue The cb to use
 * @param inUserDataValue Original user data
 */
- (void)doAuthRequestCbValue:(NSValue *)inCallBackValue withUserDataValue:(NSValue *)inUserDataValue 
{	
	PurpleAccountRequestAuthorizationCb callBack = [inCallBackValue pointerValue];
	if (callBack) {
		callBack([inUserDataValue pointerValue]);
	}
}

/*!
 * @brief Tell purple we closed an authorization request without a response
 */
- (void)closeAuthRequestWithHandle:(id)authRequestHandle
{
	purple_account_request_close(authRequestHandle);
}

#pragma mark Secure messaging

- (void)purpleConversation:(PurpleConversation *)conv setSecurityDetails:(NSDictionary *)securityDetailsDict
{
}

- (void)refreshedSecurityOfPurpleConversation:(PurpleConversation *)conv
{
	AILog(@"*** Refreshed security...");
}

- (void)dealloc
{
	purple_signals_disconnect_by_handle(adium_purple_get_handle());

	[super dealloc];
}

#ifdef HAVE_CDSA
- (CFArrayRef)copyServerCertificates:(PurpleSslConnection*)gsc {
	PurplePlugin *cdsa_plugin = purple_plugins_find_with_name("CDSA");
	if(!cdsa_plugin)
		return nil;
	CFArrayRef result;
	gboolean ok = NO;
	purple_plugin_ipc_call(cdsa_plugin, "copy_certificate_chain", &ok, gsc, &result);
	
	return result;
}
#endif

@end