Display unhandled purple conversation writes in the next run loop. Fixes #13190.
2 * Adium is the legal property of its developers, whose names are listed in the copyright file included
3 * with this source distribution.
5 * This program is free software; you can redistribute it and/or modify it under the terms of the GNU
6 * General Public License as published by the Free Software Foundation; either version 2 of the License,
7 * or (at your option) any later version.
9 * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
10 * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
11 * Public License for more details.
13 * You should have received a copy of the GNU General Public License along with this program; if not,
14 * write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
17 #import "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"
27 #pragma mark Purple Images
29 #pragma mark Conversations
30 static void adiumPurpleConvCreate(PurpleConversation *conv)
32 //Pass chats along to the account
33 if (purple_conversation_get_type(conv) == PURPLE_CONV_TYPE_CHAT) {
35 AIChat *chat = groupChatLookupFromConv(conv);
37 [accountLookup(purple_conversation_get_account(conv)) addChat:chat];
41 static void adiumPurpleConvDestroy(PurpleConversation *conv)
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').
46 AIChat *chat = (AIChat *)conv->ui_data;
48 AILogWithSignature(@"%p: %@", conv, chat);
50 //Chat will be nil if we've already cleaned up, at which point no further action is needed.
52 [accountLookup(purple_conversation_get_account(conv)) chatWasDestroyed:chat];
54 [chat setIdentifier:nil];
60 static void adiumPurpleConvWriteChat(PurpleConversation *conv, const char *who,
61 const char *message, PurpleMessageFlags flags,
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
68 if (!(flags & PURPLE_MESSAGE_SEND) || (flags & PURPLE_MESSAGE_DELAYED)) {
69 NSDictionary *messageDict;
70 NSString *messageString;
72 messageString = [NSString stringWithUTF8String:message];
73 AILog(@"Source: %s \t Name: %s \t MyNick: %s : Message %@",
75 purple_conversation_get_name(conv),
76 purple_conv_chat_get_nick(PURPLE_CONV_CHAT(conv)),
79 NSDate *date = [NSDate dateWithTimeIntervalSince1970:mtime];
80 PurpleAccount *purpleAccount = purple_conversation_get_account(conv);
82 if ((flags & PURPLE_MESSAGE_SYSTEM) == PURPLE_MESSAGE_SYSTEM || !who) {
83 CBPurpleAccount *account = accountLookup(purpleAccount);
85 [account receivedEventForChat:groupChatLookupFromConv(conv)
88 flags:[NSNumber numberWithInteger:flags]];
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);
94 if (normalizedUID.length) {
95 messageDict = [NSDictionary dictionaryWithObjectsAndKeys:attributedMessage, @"AttributedMessage",
96 normalizedUID, @"Source",
97 purpleMessageFlags, @"PurpleMessageFlags",
101 messageDict = [NSDictionary dictionaryWithObjectsAndKeys:attributedMessage, @"AttributedMessage",
102 purpleMessageFlags, @"PurpleMessageFlags",
106 [accountLookup(purple_conversation_get_account(conv)) receivedMultiChatMessage:messageDict inChat:groupChatLookupFromConv(conv)];
111 static void adiumPurpleConvWriteIm(PurpleConversation *conv, const char *who,
112 const char *message, PurpleMessageFlags flags,
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];
121 // Determine what we're actually notifying about.
122 if ([messageString rangeOfString:@"nudge" options:(NSCaseInsensitiveSearch | NSLiteralSearch)].location != NSNotFound) {
124 } else if ([messageString rangeOfString:@"buzz" options:(NSCaseInsensitiveSearch | NSLiteralSearch)].location != NSNotFound) {
127 // Just call an unknown type a "notification"
128 type = @"notification";
131 [[NSNotificationCenter defaultCenter] postNotificationName:Chat_NudgeBuzzOccured
132 object:chatLookupFromConv(conv)
133 userInfo:[NSDictionary dictionaryWithObjectsAndKeys:
137 NSDictionary *messageDict;
138 CBPurpleAccount *adiumAccount = accountLookup(purple_conversation_get_account(conv));
139 NSString *messageString;
142 messageString = [NSString stringWithUTF8String:message];
143 chat = chatLookupFromConv(conv);
145 AILog(@"adiumPurpleConvWriteIm: Received %@ from %@", messageString, chat.listObject.UID);
147 //Process any purple imgstore references into real HTML tags pointing to real images
148 messageString = processPurpleImages(messageString, adiumAccount);
150 messageDict = [NSDictionary dictionaryWithObjectsAndKeys:messageString,@"Message",
151 [NSNumber numberWithInteger:flags],@"PurpleMessageFlags",
152 [NSDate dateWithTimeIntervalSince1970:mtime],@"Date",nil];
154 [adiumAccount receivedIMChatMessage:messageDict
160 static void adiumPurpleConvWriteConv(PurpleConversation *conv, const char *who, const char *alias,
161 const char *message, PurpleMessageFlags flags,
164 AILog(@"adiumPurpleConvWriteConv: Received %s from %s [%i]",message,who,flags);
165 AIChat *chat = chatLookupFromConv(conv);
171 NSString *messageString = [NSString stringWithUTF8String:message];
173 if (!messageString) {
174 AILogWithSignature(@"Received write without message: %@ %d", chat, flags);
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
185 AIChatErrorType errorType = AIChatUnknownError;
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;
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
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
214 if (errorType != AIChatUnknownError) {
215 [accountLookup(purple_conversation_get_account(conv)) performSelector:@selector(errorForChat:type:)
217 withObject:[NSNumber numberWithInteger:errorType]
220 [adium.contentController performSelector:@selector(displayEvent:ofType:inChat:)
221 withObject:messageString
222 withObject:@"libpurpleMessage"
227 AILog(@"*** Conversation error %@: %@", chat, messageString);
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;
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;
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;
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"
259 [accountLookup(purple_conversation_get_account(conv)) updateContact:chat.listObject forEvent:[NSNumber numberWithInteger:PURPLE_BUDDY_DIRECTIM_DISCONNECTED]];
260 shouldDisplayMessage = FALSE;
264 if (shouldDisplayMessage) {
265 CBPurpleAccount *account = accountLookup(purple_conversation_get_account(conv));
267 [account performSelector:@selector(receivedEventForChat:message:date:flags:)
269 withObject:messageString
270 withObject:[NSDate dateWithTimeIntervalSince1970:mtime]
271 withObject:[NSNumber numberWithInteger:flags]
277 NSString *get_real_name_for_account_conv_buddy(PurpleAccount *account, PurpleConversation *conv, char *who)
279 g_return_val_if_fail(who != NULL && strlen(who), nil);
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);
287 NSString *normalizedUID;
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),
297 // strdup it, mostly so the free below won't have to be cased out.
301 normalizedUID = [NSString stringWithUTF8String:purple_normalize(account, uid)];
303 // We have to free the result of get_cb_real_name.
306 return normalizedUID;
309 static void adiumPurpleConvChatAddUsers(PurpleConversation *conv, GList *cbuddies, gboolean new_arrivals)
311 if (purple_conversation_get_type(conv) == PURPLE_CONV_TYPE_CHAT) {
312 PurpleAccount *account = purple_conversation_get_account(conv);
314 NSMutableArray *users = [NSMutableArray array];
316 for (GList *l = cbuddies; l; l = l->next) {
317 PurpleConvChatBuddy *cb = (PurpleConvChatBuddy *)l->data;
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"];
323 [user setObject:[NSString stringWithUTF8String:cb->alias] forKey:@"Alias"];
326 [users addObject:user];
329 [accountLookup(account) updateUserListForChat:groupChatLookupFromConv(conv)
331 newlyAdded:new_arrivals];
333 AILog(@"adiumPurpleConvChatAddUsers: IM");
336 static void adiumPurpleConvChatRenameUser(PurpleConversation *conv, const char *oldName,
337 const char *newName, const char *newAlias)
339 AILog(@"adiumPurpleConvChatRenameUser: %s: oldName %s, newName %s, newAlias %s",
340 purple_conversation_get_name(conv),
341 oldName, newName, newAlias);
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);
347 PurpleAccount *account = purple_conversation_get_account(conv);
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]
353 inChat:groupChatLookupFromConv(conv)];
357 static void adiumPurpleConvChatRemoveUsers(PurpleConversation *conv, GList *users)
359 if (purple_conversation_get_type(conv) == PURPLE_CONV_TYPE_CHAT) {
360 NSMutableArray *usersArray = [NSMutableArray array];
361 PurpleAccount *account = purple_conversation_get_account(conv);
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];
369 [accountLookup(account) removeUsersArray:usersArray
370 fromChat:groupChatLookupFromConv(conv)];
373 AILog(@"adiumPurpleConvChatRemoveUser: IM");
377 static void adiumPurpleConvUpdateUser(PurpleConversation *conv, const char *user)
379 PurpleAccount *account = purple_conversation_get_account(conv);
380 CBPurpleAccount *adiumAccount = accountLookup(account);
382 PurpleConvChatBuddy *cb = purple_conv_chat_cb_find(PURPLE_CONV_CHAT(conv), user);
384 NSMutableDictionary *attributes = [NSMutableDictionary dictionary];
386 GList *attribute = purple_conv_chat_cb_get_attribute_keys(cb);
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]];
393 g_list_free(attribute);
395 NSString *alias = cb->alias ? [NSString stringWithUTF8String:cb->alias] : nil;
397 [adiumAccount updateUser:get_real_name_for_account_conv_buddy(account, conv, (char *)user)
398 forChat:groupChatLookupFromConv(conv)
401 attributes:attributes];
404 static void adiumPurpleConvPresent(PurpleConversation *conv)
409 //This isn't a function we want Purple doing anything with, I don't think
410 static gboolean adiumPurpleConvHasFocus(PurpleConversation *conv)
415 static void adiumPurpleConvUpdated(PurpleConversation *conv, PurpleConvUpdateType type)
417 if (purple_conversation_get_type(conv) == PURPLE_CONV_TYPE_CHAT) {
418 PurpleConvChat *chat = purple_conversation_get_chat_data(conv);
421 case PURPLE_CONV_UPDATE_TOPIC:
425 if (chat->who != NULL) {
426 who = [NSString stringWithUTF8String:chat->who];
429 [accountLookup(purple_conversation_get_account(conv)) updateTopic:(purple_conv_chat_get_topic(chat) ?
430 [NSString stringWithUTF8String:purple_conv_chat_get_topic(chat)] :
432 forChat:groupChatLookupFromConv(conv)
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)] :
440 forChat:groupChatLookupFromConv(conv)];
442 AILog(@"Update to title: %s",purple_conversation_get_title(conv));
444 case PURPLE_CONV_UPDATE_CHATLEFT:
445 [accountLookup(purple_conversation_get_account(conv)) leftChat:groupChatLookupFromConv(conv)];
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:
460 [accountLookup(purple_conversation_get_account(conv)) mainPerformSelector:@selector(convUpdateForChat:type:)
461 withObject:groupChatLookupFromConv(conv)
462 withObject:[NSNumber numberWithInt:type]];
468 } else if (purple_conversation_get_type(conv) == PURPLE_CONV_TYPE_IM) {
469 PurpleConvIm *im = purple_conversation_get_im_data(conv);
471 case PURPLE_CONV_UPDATE_TYPING: {
473 AITypingState typingState;
475 switch (purple_conv_im_get_typing_state(im)) {
477 typingState = AITyping;
480 typingState = AIEnteredText;
482 case PURPLE_NOT_TYPING:
484 typingState = AINotTyping;
488 NSNumber *typingStateNumber = [NSNumber numberWithInteger:typingState];
490 [accountLookup(purple_conversation_get_account(conv)) typingUpdateForIMChat:imChatLookupFromConv(conv)
491 typing:typingStateNumber];
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);
507 #pragma mark Custom smileys
508 gboolean adiumPurpleConvCustomSmileyAdd(PurpleConversation *conv, const char *smile, gboolean remote)
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]];
517 void adiumPurpleConvCustomSmileyWrite(PurpleConversation *conv, const char *smile,
518 const guchar *data, gsize size)
520 AILog(@"%s: Write Custom Smiley %s (%x %i)",purple_conversation_get_name(conv),smile,data,size);
522 [accountLookup(purple_conversation_get_account(conv)) chat:chatLookupFromConv(conv)
523 setCustomEmoticon:[NSString stringWithUTF8String:smile]
524 withImageData:[NSData dataWithBytes:data
528 void adiumPurpleConvCustomSmileyClose(PurpleConversation *conv, const char *smile)
530 AILog(@"%s: Close Custom Smiley %s",purple_conversation_get_name(conv),smile);
532 [accountLookup(purple_conversation_get_account(conv)) chat:chatLookupFromConv(conv)
533 closedCustomEmoticon:[NSString stringWithUTF8String:smile]];
536 static gboolean adiumPurpleConvJoin(PurpleConversation *conv, const char *name,
537 PurpleConvChatBuddyFlags flags,
540 AIChat *chat = groupChatLookupFromConv(conv);
542 // We return TRUE if we want to hide it.
543 return !chat.showJoinLeave;
546 static gboolean adiumPurpleConvLeave(PurpleConversation *conv, const char *name,
547 const char *reason, GHashTable *users)
549 AIChat *chat = groupChatLookupFromConv(conv);
551 // We return TRUE if we want to hide it.
552 return !chat.showJoinLeave;
555 static PurpleConversationUiOps adiumPurpleConversationOps = {
556 adiumPurpleConvCreate,
557 adiumPurpleConvDestroy,
558 adiumPurpleConvWriteChat,
559 adiumPurpleConvWriteIm,
560 adiumPurpleConvWriteConv,
561 adiumPurpleConvChatAddUsers,
562 adiumPurpleConvChatRenameUser,
563 adiumPurpleConvChatRemoveUsers,
564 adiumPurpleConvUpdateUser,
566 adiumPurpleConvPresent,
567 adiumPurpleConvHasFocus,
570 adiumPurpleConvCustomSmileyAdd,
571 adiumPurpleConvCustomSmileyWrite,
572 adiumPurpleConvCustomSmileyClose,
578 PurpleConversationUiOps *adium_purple_conversation_get_ui_ops(void)
580 return &adiumPurpleConversationOps;
583 void adiumPurpleConversation_init(void)
585 purple_conversations_set_ui_ops(adium_purple_conversation_get_ui_ops());
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);
591 purple_signal_connect(purple_conversations_get_handle(), "chat-buddy-joining", adium_purple_get_handle(),
592 PURPLE_CALLBACK(adiumPurpleConvJoin), NULL);
594 purple_signal_connect(purple_conversations_get_handle(), "chat-buddy-leaving", adium_purple_get_handle(),
595 PURPLE_CALLBACK(adiumPurpleConvLeave), NULL);