Plugins/Purple Service/adiumPurpleConversation.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 3081 6388b2768ef1
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 "adiumPurpleConversation.h"
    18 #import <AIUtilities/AIObjectAdditions.h>
    19 #import <AIUtilities/AIAttributedStringAdditions.h>
    20 #import <Adium/AIChat.h>
    21 #import <Adium/AIContentTyping.h>
    22 #import <Adium/AIHTMLDecoder.h>
    23 #import <Adium/AIListContact.h>
    24 #import <Adium/AIContentControllerProtocol.h>
    25 #import "AINudgeBuzzHandlerPlugin.h"
    26 
    27 #pragma mark Purple Images
    28 
    29 #pragma mark Conversations
    30 static void adiumPurpleConvCreate(PurpleConversation *conv)
    31 {
    32 	//Pass chats along to the account
    33 	if (purple_conversation_get_type(conv) == PURPLE_CONV_TYPE_CHAT) {
    34 		
    35 		AIChat *chat = groupChatLookupFromConv(conv);
    36 		
    37 		[accountLookup(purple_conversation_get_account(conv)) addChat:chat];
    38 	}
    39 }
    40 
    41 static void adiumPurpleConvDestroy(PurpleConversation *conv)
    42 {
    43 	/* Purple is telling us a conv was destroyed.  We've probably already cleaned up, but be sure in case purple calls this
    44 	 * when we don't ask it to (for example if we are summarily kicked from a chat room and purple closes the 'window').
    45 	 */
    46 	AIChat *chat = (AIChat *)conv->ui_data;
    47 
    48 	AILogWithSignature(@"%p: %@", conv, chat);
    49 
    50 	//Chat will be nil if we've already cleaned up, at which point no further action is needed.
    51 	if (chat) {
    52 		[accountLookup(purple_conversation_get_account(conv)) chatWasDestroyed:chat];
    53 
    54 		[chat setIdentifier:nil];
    55 		[chat release];
    56 		conv->ui_data = nil;
    57 	}
    58 }
    59 
    60 static void adiumPurpleConvWriteChat(PurpleConversation *conv, const char *who,
    61 								   const char *message, PurpleMessageFlags flags,
    62 								   time_t mtime)
    63 {
    64 	/* We only care about this if:
    65 	 *	1) It does not have the PURPLE_MESSAGE_SEND flag, which is set if Purple is sending a sent message back to us -or-
    66 	 *  2) It is a delayed (history) message from a chat
    67 	 */
    68 	if (!(flags & PURPLE_MESSAGE_SEND) || (flags & PURPLE_MESSAGE_DELAYED)) {
    69 		NSDictionary	*messageDict;
    70 		NSString		*messageString;
    71 
    72 		messageString = [NSString stringWithUTF8String:message];
    73 		AILog(@"Source: %s \t Name: %s \t MyNick: %s : Message %@", 
    74 			  who,
    75 			  purple_conversation_get_name(conv),
    76 			  purple_conv_chat_get_nick(PURPLE_CONV_CHAT(conv)),
    77 			  messageString);
    78 
    79 		NSDate				*date = [NSDate dateWithTimeIntervalSince1970:mtime];
    80 		PurpleAccount		*purpleAccount = purple_conversation_get_account(conv);
    81 		
    82 		if ((flags & PURPLE_MESSAGE_SYSTEM) == PURPLE_MESSAGE_SYSTEM || !who) {
    83 			CBPurpleAccount *account = accountLookup(purpleAccount);
    84 			
    85 			[account receivedEventForChat:groupChatLookupFromConv(conv)
    86 								  message:messageString
    87 									 date:date
    88 									flags:[NSNumber numberWithInteger:flags]];
    89 		} else {
    90 			NSAttributedString	*attributedMessage = [AIHTMLDecoder decodeHTML:messageString];
    91 			NSNumber			*purpleMessageFlags = [NSNumber numberWithInteger:flags];
    92 			NSString			*normalizedUID = get_real_name_for_account_conv_buddy(purpleAccount, conv, (char *)who);
    93 			
    94 			if (normalizedUID.length) {
    95 				messageDict = [NSDictionary dictionaryWithObjectsAndKeys:attributedMessage, @"AttributedMessage",
    96 							   normalizedUID, @"Source",
    97 							   purpleMessageFlags, @"PurpleMessageFlags",
    98 							   date, @"Date",nil];
    99 				
   100 			} else {
   101 				messageDict = [NSDictionary dictionaryWithObjectsAndKeys:attributedMessage, @"AttributedMessage",
   102 							   purpleMessageFlags, @"PurpleMessageFlags",
   103 							   date, @"Date",nil];
   104 			}
   105 
   106 			[accountLookup(purple_conversation_get_account(conv)) receivedMultiChatMessage:messageDict inChat:groupChatLookupFromConv(conv)];
   107 		}
   108 	}
   109 }
   110 
   111 static void adiumPurpleConvWriteIm(PurpleConversation *conv, const char *who,
   112 								 const char *message, PurpleMessageFlags flags,
   113 								 time_t mtime)
   114 {
   115 	//We only care about this if it does not have the PURPLE_MESSAGE_SEND flag, which is set if Purple is sending a sent message back to us
   116 	if ((flags & PURPLE_MESSAGE_SEND) == 0) {
   117 		if (flags & PURPLE_MESSAGE_NOTIFY) {
   118 			// We received a notification (nudge or buzz). Send a notification of such.
   119 			NSString *type, *messageString = [NSString stringWithUTF8String:message];
   120 
   121 			// Determine what we're actually notifying about.
   122 			if ([messageString rangeOfString:@"nudge" options:(NSCaseInsensitiveSearch | NSLiteralSearch)].location != NSNotFound) {
   123 				type = @"Nudge";
   124 			} else if ([messageString rangeOfString:@"buzz" options:(NSCaseInsensitiveSearch | NSLiteralSearch)].location != NSNotFound) {
   125 				type = @"Buzz";
   126 			} else {
   127 				// Just call an unknown type a "notification"
   128 				type = @"notification";
   129 			}
   130 
   131 			[[NSNotificationCenter defaultCenter] postNotificationName:Chat_NudgeBuzzOccured
   132 																			   object:chatLookupFromConv(conv)
   133 																			 userInfo:[NSDictionary dictionaryWithObjectsAndKeys:
   134 																					   type, @"Type",
   135 																					   nil]];
   136 		} else {
   137 			NSDictionary		*messageDict;
   138 			CBPurpleAccount		*adiumAccount = accountLookup(purple_conversation_get_account(conv));
   139 			NSString			*messageString;
   140 			AIChat				*chat;
   141 			
   142 			messageString = [NSString stringWithUTF8String:message];
   143 			chat = chatLookupFromConv(conv);
   144 			
   145 			AILog(@"adiumPurpleConvWriteIm: Received %@ from %@", messageString, chat.listObject.UID);
   146 			
   147 			//Process any purple imgstore references into real HTML tags pointing to real images
   148 			messageString = processPurpleImages(messageString, adiumAccount);
   149 			
   150 			messageDict = [NSDictionary dictionaryWithObjectsAndKeys:messageString,@"Message",
   151 						   [NSNumber numberWithInteger:flags],@"PurpleMessageFlags",
   152 						   [NSDate dateWithTimeIntervalSince1970:mtime],@"Date",nil];
   153 			
   154 			[adiumAccount receivedIMChatMessage:messageDict
   155 										 inChat:chat];
   156 		}
   157 	}
   158 }
   159 
   160 static void adiumPurpleConvWriteConv(PurpleConversation *conv, const char *who, const char *alias,
   161 								   const char *message, PurpleMessageFlags flags,
   162 								   time_t mtime)
   163 {
   164 	AILog(@"adiumPurpleConvWriteConv: Received %s from %s [%i]",message,who,flags);
   165 	AIChat	*chat = chatLookupFromConv(conv);
   166 
   167 	if (!chat) {
   168 		return;
   169 	}
   170 	
   171 	NSString			*messageString = [NSString stringWithUTF8String:message];
   172 	
   173 	if (!messageString) {
   174 		AILogWithSignature(@"Received write without message: %@ %d", chat, flags);
   175 		return;
   176 	}
   177 	
   178 	if (flags & PURPLE_MESSAGE_ERROR) {	
   179 		if ([messageString rangeOfString:@"User information not available"].location != NSNotFound) {
   180 			//Ignore user information errors; they are irrelevent
   181 			//XXX The user info check only works in English; libpurple should be modified to be better about this useless information spamming
   182 			return;
   183 		}
   184 		
   185 		AIChatErrorType	errorType = AIChatUnknownError;
   186 		
   187 		if (([messageString rangeOfString:[NSString stringWithUTF8String:_("Not logged in")]].location != NSNotFound) || 
   188 			([messageString rangeOfString:[NSString stringWithUTF8String:_("User temporarily unavailable")]].location != NSNotFound)) {
   189 			errorType = AIChatMessageSendingUserNotAvailable;
   190 		} else if ([messageString rangeOfString:[NSString stringWithUTF8String:_("In local permit/deny")]].location != NSNotFound) {
   191 			errorType = AIChatMessageSendingUserIsBlocked;
   192 		} else if (([messageString rangeOfString:[NSString stringWithUTF8String:_("Reply too big")]].location != NSNotFound) ||
   193 				   ([messageString rangeOfString:@"message is too large"].location != NSNotFound)) {
   194 			//XXX - there may be other conditions, but this seems the most common so that's how we'll classify it
   195 			errorType = AIChatMessageSendingTooLarge;
   196 		} else if ([messageString rangeOfString:[NSString stringWithUTF8String:_("Command failed")]].location != NSNotFound) {
   197 			errorType = AIChatCommandFailed;
   198 		} else if ([messageString rangeOfString:[NSString stringWithUTF8String:_("Wrong number of arguments")]].location != NSNotFound) {
   199 			errorType = AIChatInvalidNumberOfArguments;
   200 		} else if ([messageString rangeOfString:[NSString stringWithUTF8String:_("Rate")]].location != NSNotFound) {
   201 			//XXX Is 'Rate' really a standalone translated string?
   202 			errorType = AIChatMessageSendingMissedRateLimitExceeded;
   203 		} else if ([messageString rangeOfString:[NSString stringWithUTF8String:_("Too evil")]].location != NSNotFound) {
   204 			errorType = AIChatMessageReceivingMissedRemoteIsTooEvil;
   205 		}
   206 		/* Another is 'refused by client', which is definitely seen when sending an offline message to an invalid screenname...
   207 		 * but I don't know when else it is sent. -evands
   208 		 */
   209 		
   210 		/* We will wait until the next run loop, in case this error message was generated by
   211 		 * the sending of a message. This allows the results of sending the message to be displayed
   212 		 * first.
   213 		 */
   214 		if (errorType != AIChatUnknownError) {
   215 			[accountLookup(purple_conversation_get_account(conv)) performSelector:@selector(errorForChat:type:)
   216 																	   withObject:chat
   217 																	   withObject:[NSNumber numberWithInteger:errorType]
   218 																	   afterDelay:0];
   219 		} else {
   220 			[adium.contentController performSelector:@selector(displayEvent:ofType:inChat:)
   221 										  withObject:messageString
   222 										  withObject:@"libpurpleMessage"
   223 										  withObject:chat
   224 										  afterDelay:0];
   225 		}
   226 		
   227 		AILog(@"*** Conversation error %@: %@", chat, messageString);
   228 	} else {
   229 		BOOL				shouldDisplayMessage = TRUE;
   230 		if (strcmp(message, _("Direct IM established")) == 0) {
   231 			[accountLookup(purple_conversation_get_account(conv)) updateContact:chat.listObject
   232 											   forEvent:[NSNumber numberWithInteger:PURPLE_BUDDY_DIRECTIM_CONNECTED]];
   233 			shouldDisplayMessage = FALSE;
   234 			
   235 		} else {
   236 			BOOL isClosingDirectIM = FALSE;
   237 			if ((strcmp(message, _("The remote user has closed the connection.")) == 0) ||
   238 				(strcmp(message, _("The remote user has declined your request.")) == 0) ||
   239 				(strcmp(message, _("Received invalid data on connection with remote user.")) == 0) ||
   240 				(strcmp(message, _("Could not establish a connection with the remote user.")) == 0)) {
   241 				isClosingDirectIM = TRUE;
   242 			}
   243 			
   244 			if (!isClosingDirectIM) {
   245 				//Only works in English - XXX fix me!
   246 				if ([messageString rangeOfString:@"Lost connection with the remote user:"].location != NSNotFound) {
   247 					isClosingDirectIM = TRUE;
   248 				}
   249 			}
   250 			
   251 			if (isClosingDirectIM) {
   252 				if (strcmp(message, _("The remote user has closed the connection.")) != 0) {
   253 					//Display the message if it's not just the one for the other guy closing it...
   254 					[adium.contentController displayEvent:messageString
   255 												   ofType:@"directIMDisconnected"
   256 												   inChat:chat];
   257 				}
   258 				
   259 				[accountLookup(purple_conversation_get_account(conv)) updateContact:chat.listObject forEvent:[NSNumber numberWithInteger:PURPLE_BUDDY_DIRECTIM_DISCONNECTED]];
   260 				shouldDisplayMessage = FALSE;
   261 			}
   262 		}
   263 
   264 		if (shouldDisplayMessage) {
   265 			CBPurpleAccount *account = accountLookup(purple_conversation_get_account(conv));
   266 			
   267 			[account performSelector:@selector(receivedEventForChat:message:date:flags:)
   268 						  withObject:chat
   269 						  withObject:messageString
   270 						  withObject:[NSDate dateWithTimeIntervalSince1970:mtime]
   271 						  withObject:[NSNumber numberWithInteger:flags]
   272 						  afterDelay:0];
   273 		}
   274 	}
   275 }
   276 
   277 NSString *get_real_name_for_account_conv_buddy(PurpleAccount *account, PurpleConversation *conv, char *who)
   278 {
   279 	g_return_val_if_fail(who != NULL && strlen(who), nil);
   280 	
   281 	PurplePlugin *prpl = purple_find_prpl(purple_account_get_protocol_id(account));
   282 	PurplePluginProtocolInfo  *prpl_info = (prpl ? PURPLE_PLUGIN_PROTOCOL_INFO(prpl) : NULL);
   283 	PurpleConvChat *convChat = purple_conversation_get_chat_data(conv);
   284 	
   285 	char *uid = NULL;
   286 	
   287 	NSString *normalizedUID;
   288 	
   289 	if (prpl_info && prpl_info->get_cb_real_name) {
   290 		// Get the real name of the buddy for use as a UID, if available.
   291 		uid = prpl_info->get_cb_real_name(purple_account_get_connection(account),
   292 										  purple_conv_chat_get_id(convChat),
   293 										  who);
   294 	}
   295 	
   296 	if (!uid) {
   297 		// strdup it, mostly so the free below won't have to be cased out.
   298 		uid = g_strdup(who);
   299 	}
   300 		
   301 	normalizedUID = [NSString stringWithUTF8String:purple_normalize(account, uid)];
   302 		
   303 	// We have to free the result of get_cb_real_name.
   304 	g_free(uid);
   305 
   306 	return normalizedUID;
   307 }
   308 
   309 static void adiumPurpleConvChatAddUsers(PurpleConversation *conv, GList *cbuddies, gboolean new_arrivals)
   310 {
   311 	if (purple_conversation_get_type(conv) == PURPLE_CONV_TYPE_CHAT) {
   312 		PurpleAccount *account = purple_conversation_get_account(conv);
   313 
   314 		NSMutableArray *users = [NSMutableArray array];
   315 		
   316 		for (GList *l = cbuddies; l; l = l->next) {
   317 			PurpleConvChatBuddy *cb = (PurpleConvChatBuddy *)l->data;
   318 			
   319 			NSMutableDictionary *user = [NSMutableDictionary dictionary];
   320 			[user setObject:get_real_name_for_account_conv_buddy(account, conv, cb->name) forKey:@"UID"];
   321 			[user setObject:[NSNumber numberWithInteger:cb->flags] forKey:@"Flags"];
   322 			if (cb->alias) {
   323 				[user setObject:[NSString stringWithUTF8String:cb->alias] forKey:@"Alias"];
   324 			}
   325 			
   326 			[users addObject:user];
   327 		}
   328 
   329 		[accountLookup(account) updateUserListForChat:groupChatLookupFromConv(conv)
   330 												users:users
   331 										   newlyAdded:new_arrivals];
   332 	} else
   333 		AILog(@"adiumPurpleConvChatAddUsers: IM");
   334 }
   335 
   336 static void adiumPurpleConvChatRenameUser(PurpleConversation *conv, const char *oldName,
   337 										const char *newName, const char *newAlias)
   338 {
   339 	AILog(@"adiumPurpleConvChatRenameUser: %s: oldName %s, newName %s, newAlias %s",
   340 			   purple_conversation_get_name(conv),
   341 			   oldName, newName, newAlias);
   342 	
   343 	if (purple_conversation_get_type(conv) == PURPLE_CONV_TYPE_CHAT) {
   344 		PurpleConvChat *convChat = purple_conversation_get_chat_data(conv);
   345 		PurpleConvChatBuddy *cb = purple_conv_chat_cb_find(convChat, oldName);
   346 		
   347 		PurpleAccount *account = purple_conversation_get_account(conv);
   348 		
   349 		[accountLookup(purple_conversation_get_account(conv)) renameParticipant:get_real_name_for_account_conv_buddy(account, conv, (char *)oldName)
   350 																		newName:get_real_name_for_account_conv_buddy(account, conv, (char *)newName)
   351 																	   newAlias:[NSString stringWithUTF8String:newAlias]
   352 																		  flags:cb->flags
   353 																		 inChat:groupChatLookupFromConv(conv)];
   354 	}
   355 }
   356 
   357 static void adiumPurpleConvChatRemoveUsers(PurpleConversation *conv, GList *users)
   358 {
   359 	if (purple_conversation_get_type(conv) == PURPLE_CONV_TYPE_CHAT) {
   360 		NSMutableArray	*usersArray = [NSMutableArray array];
   361 		PurpleAccount	*account = purple_conversation_get_account(conv);
   362 
   363 		GList *l;
   364 		for (l = users; l != NULL; l = l->next) {
   365 			NSString *normalizedUID = get_real_name_for_account_conv_buddy(account, conv, (char *)l->data);
   366 			[usersArray addObject:normalizedUID];
   367 		}
   368 
   369 		[accountLookup(account) removeUsersArray:usersArray
   370 										fromChat:groupChatLookupFromConv(conv)];
   371 
   372 	} else {
   373 		AILog(@"adiumPurpleConvChatRemoveUser: IM");
   374 	}
   375 }
   376 
   377 static void adiumPurpleConvUpdateUser(PurpleConversation *conv, const char *user)
   378 {
   379 	PurpleAccount *account = purple_conversation_get_account(conv);
   380 	CBPurpleAccount *adiumAccount = accountLookup(account);
   381 	
   382 	PurpleConvChatBuddy *cb = purple_conv_chat_cb_find(PURPLE_CONV_CHAT(conv), user);
   383 	
   384 	NSMutableDictionary *attributes = [NSMutableDictionary dictionary];
   385 	
   386 	GList *attribute = purple_conv_chat_cb_get_attribute_keys(cb);
   387 	
   388 	for (; attribute != NULL; attribute = g_list_next(attribute)) {
   389 		[attributes setObject:[NSString stringWithUTF8String:purple_conv_chat_cb_get_attribute(cb, attribute->data)]
   390 					   forKey:[NSString stringWithUTF8String:attribute->data]];
   391 	}
   392 	
   393 	g_list_free(attribute);
   394 	
   395 	NSString *alias = cb->alias ? [NSString stringWithUTF8String:cb->alias] : nil;
   396 	
   397 	[adiumAccount updateUser:get_real_name_for_account_conv_buddy(account, conv, (char *)user)
   398 					 forChat:groupChatLookupFromConv(conv)
   399 					   flags:cb->flags
   400 					   alias:alias
   401 				  attributes:attributes];
   402 }
   403 
   404 static void adiumPurpleConvPresent(PurpleConversation *conv)
   405 {
   406 	
   407 }
   408 
   409 //This isn't a function we want Purple doing anything with, I don't think
   410 static gboolean adiumPurpleConvHasFocus(PurpleConversation *conv)
   411 {
   412 	return NO;
   413 }
   414 
   415 static void adiumPurpleConvUpdated(PurpleConversation *conv, PurpleConvUpdateType type)
   416 {
   417 	if (purple_conversation_get_type(conv) == PURPLE_CONV_TYPE_CHAT) {
   418 		PurpleConvChat  *chat = purple_conversation_get_chat_data(conv);
   419 		
   420 		switch(type) {
   421 			case PURPLE_CONV_UPDATE_TOPIC:
   422 			{
   423 				NSString *who = nil;
   424 				
   425 				if (chat->who != NULL) {
   426 					who = [NSString stringWithUTF8String:chat->who];
   427 				}
   428 				
   429 				[accountLookup(purple_conversation_get_account(conv)) updateTopic:(purple_conv_chat_get_topic(chat) ?
   430 																				   [NSString stringWithUTF8String:purple_conv_chat_get_topic(chat)] :
   431 																				   nil)
   432 																		  forChat:groupChatLookupFromConv(conv)
   433 																	   withSource:who];
   434 				break;
   435 			}
   436 			case PURPLE_CONV_UPDATE_TITLE:
   437 				[accountLookup(purple_conversation_get_account(conv)) updateTitle:(purple_conversation_get_title(conv) ?
   438 														   [NSString stringWithUTF8String:purple_conversation_get_title(conv)] :
   439 														   nil)
   440 												  forChat:groupChatLookupFromConv(conv)];
   441 				
   442 				AILog(@"Update to title: %s",purple_conversation_get_title(conv));
   443 				break;
   444 			case PURPLE_CONV_UPDATE_CHATLEFT:
   445 				[accountLookup(purple_conversation_get_account(conv)) leftChat:groupChatLookupFromConv(conv)];
   446 				break;
   447 			case PURPLE_CONV_UPDATE_ADD:
   448 			case PURPLE_CONV_UPDATE_REMOVE:
   449 			case PURPLE_CONV_UPDATE_ACCOUNT:
   450 			case PURPLE_CONV_UPDATE_TYPING:
   451 			case PURPLE_CONV_UPDATE_UNSEEN:
   452 			case PURPLE_CONV_UPDATE_LOGGING:
   453 			case PURPLE_CONV_ACCOUNT_ONLINE:
   454 			case PURPLE_CONV_ACCOUNT_OFFLINE:
   455 			case PURPLE_CONV_UPDATE_AWAY:
   456 			case PURPLE_CONV_UPDATE_ICON:
   457 			case PURPLE_CONV_UPDATE_FEATURES:
   458 
   459 /*				
   460 				[accountLookup(purple_conversation_get_account(conv)) mainPerformSelector:@selector(convUpdateForChat:type:)
   461 													   withObject:groupChatLookupFromConv(conv)
   462 													   withObject:[NSNumber numberWithInt:type]];
   463 */				
   464 			default:
   465 				break;
   466 		}
   467 
   468 	} else if (purple_conversation_get_type(conv) == PURPLE_CONV_TYPE_IM) {
   469 		PurpleConvIm  *im = purple_conversation_get_im_data(conv);
   470 		switch (type) {
   471 			case PURPLE_CONV_UPDATE_TYPING: {
   472 
   473 				AITypingState typingState;
   474 
   475 				switch (purple_conv_im_get_typing_state(im)) {
   476 					case PURPLE_TYPING:
   477 						typingState = AITyping;
   478 						break;
   479 					case PURPLE_TYPED:
   480 						typingState = AIEnteredText;
   481 						break;
   482 					case PURPLE_NOT_TYPING:
   483 					default:
   484 						typingState = AINotTyping;
   485 						break;
   486 				}
   487 
   488 				NSNumber	*typingStateNumber = [NSNumber numberWithInteger:typingState];
   489 
   490 				[accountLookup(purple_conversation_get_account(conv)) typingUpdateForIMChat:imChatLookupFromConv(conv)
   491 															 typing:typingStateNumber];
   492 				break;
   493 			}
   494 			case PURPLE_CONV_UPDATE_AWAY: {
   495 				//If the conversation update is UPDATE_AWAY, it seems to suppress the typing state being updated
   496 				//Reset purple's typing tracking, then update to receive a PURPLE_CONV_UPDATE_TYPING message
   497 				purple_conv_im_set_typing_state(im, PURPLE_NOT_TYPING);
   498 				purple_conv_im_update_typing(im);
   499 				break;
   500 			}
   501 			default:
   502 				break;
   503 		}
   504 	}
   505 }
   506 
   507 #pragma mark Custom smileys
   508 gboolean adiumPurpleConvCustomSmileyAdd(PurpleConversation *conv, const char *smile, gboolean remote)
   509 {
   510 	AILog(@"%s: Added Custom Smiley %s",purple_conversation_get_name(conv),smile);
   511 	[accountLookup(purple_conversation_get_account(conv)) chat:chatLookupFromConv(conv)
   512 			 isWaitingOnCustomEmoticon:[NSString stringWithUTF8String:smile]];
   513 
   514 	return TRUE;
   515 }
   516 
   517 void adiumPurpleConvCustomSmileyWrite(PurpleConversation *conv, const char *smile,
   518 									const guchar *data, gsize size)
   519 {
   520 	AILog(@"%s: Write Custom Smiley %s (%x %i)",purple_conversation_get_name(conv),smile,data,size);
   521 
   522 	[accountLookup(purple_conversation_get_account(conv)) chat:chatLookupFromConv(conv)
   523 					 setCustomEmoticon:[NSString stringWithUTF8String:smile]
   524 						 withImageData:[NSData dataWithBytes:data
   525 													  length:size]];
   526 }
   527 
   528 void adiumPurpleConvCustomSmileyClose(PurpleConversation *conv, const char *smile)
   529 {
   530 	AILog(@"%s: Close Custom Smiley %s",purple_conversation_get_name(conv),smile);
   531 
   532 	[accountLookup(purple_conversation_get_account(conv)) chat:chatLookupFromConv(conv)
   533 				  closedCustomEmoticon:[NSString stringWithUTF8String:smile]];
   534 }
   535 
   536 static gboolean adiumPurpleConvJoin(PurpleConversation *conv, const char *name,
   537 									PurpleConvChatBuddyFlags flags,
   538 									GHashTable *users)
   539 {
   540 	AIChat *chat = groupChatLookupFromConv(conv);
   541 
   542 	// We return TRUE if we want to hide it.
   543 	return !chat.showJoinLeave;
   544 }
   545 
   546 static gboolean adiumPurpleConvLeave(PurpleConversation *conv, const char *name,
   547 									 const char *reason, GHashTable *users)
   548 {
   549 	AIChat *chat = groupChatLookupFromConv(conv);
   550 	
   551 	// We return TRUE if we want to hide it.
   552 	return !chat.showJoinLeave;	
   553 }
   554 
   555 static PurpleConversationUiOps adiumPurpleConversationOps = {
   556 	adiumPurpleConvCreate,
   557     adiumPurpleConvDestroy,
   558     adiumPurpleConvWriteChat,
   559     adiumPurpleConvWriteIm,
   560     adiumPurpleConvWriteConv,
   561     adiumPurpleConvChatAddUsers,
   562     adiumPurpleConvChatRenameUser,
   563     adiumPurpleConvChatRemoveUsers,
   564 	adiumPurpleConvUpdateUser,
   565 	
   566 	adiumPurpleConvPresent,
   567 	adiumPurpleConvHasFocus,
   568 
   569 	/* Custom Smileys */
   570 	adiumPurpleConvCustomSmileyAdd,
   571 	adiumPurpleConvCustomSmileyWrite,
   572 	adiumPurpleConvCustomSmileyClose,
   573 
   574 	/* send_confirm */
   575 	NULL
   576 };
   577 
   578 PurpleConversationUiOps *adium_purple_conversation_get_ui_ops(void)
   579 {
   580 	return &adiumPurpleConversationOps;
   581 }
   582 
   583 void adiumPurpleConversation_init(void)
   584 {	
   585 	purple_conversations_set_ui_ops(adium_purple_conversation_get_ui_ops());
   586 
   587 	purple_signal_connect_priority(purple_conversations_get_handle(), "conversation-updated", adium_purple_get_handle(),
   588 								 PURPLE_CALLBACK(adiumPurpleConvUpdated), NULL,
   589 								 PURPLE_SIGNAL_PRIORITY_LOWEST);
   590 	
   591 	purple_signal_connect(purple_conversations_get_handle(), "chat-buddy-joining", adium_purple_get_handle(),
   592 						  PURPLE_CALLBACK(adiumPurpleConvJoin), NULL);
   593 	
   594 	purple_signal_connect(purple_conversations_get_handle(), "chat-buddy-leaving", adium_purple_get_handle(),
   595 						  PURPLE_CALLBACK(adiumPurpleConvLeave), NULL);
   596 	
   597 }