2 * purple - Jabber Protocol Plugin
4 * Purple is the legal property of its developers, whose names are too numerous
5 * to list here. Please refer to the COPYRIGHT file distributed with this
8 * This program is free software; you can redistribute it and/or modify
9 * it under the terms of the GNU General Public License as published by
10 * the Free Software Foundation; either version 2 of the License, or
11 * (at your option) any later version.
13 * This program is distributed in the hope that it will be useful,
14 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 * GNU General Public License for more details.
18 * You should have received a copy of the GNU General Public License
19 * along with this program; if not, write to the Free Software
20 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02111-1301 USA
27 #include "smiley-custom.h"
28 #include "smiley-parser.h"
31 #include "adhoccommands.h"
35 #include "google/google.h"
45 PurpleConversation
*conv
;
47 } JabberMessageRemoteSmileyAddData
;
49 static GString
*jm_body_with_oob(JabberMessage
*jm
) {
51 GString
*body
= g_string_new("");
54 g_string_append(body
, jm
->xhtml
);
56 g_string_append(body
, jm
->body
);
58 for(etc
= jm
->etc
; etc
; etc
= etc
->next
) {
59 PurpleXmlNode
*x
= etc
->data
;
60 const char *xmlns
= purple_xmlnode_get_namespace(x
);
61 if(purple_strequal(xmlns
, NS_OOB_X_DATA
)) {
62 PurpleXmlNode
*url
, *desc
;
63 char *urltxt
, *desctxt
;
65 url
= purple_xmlnode_get_child(x
, "url");
66 desc
= purple_xmlnode_get_child(x
, "desc");
71 urltxt
= purple_xmlnode_get_data(url
);
72 desctxt
= desc
? purple_xmlnode_get_data(desc
) : urltxt
;
74 if(body
->len
&& !purple_strequal(body
->str
, urltxt
))
75 g_string_append_printf(body
, "<br/><a href='%s'>%s</a>",
78 g_string_printf(body
, "<a href='%s'>%s</a>",
91 void jabber_message_free(JabberMessage
*jm
)
101 g_free(jm
->thread_id
);
102 g_list_free(jm
->etc
);
103 g_list_free(jm
->eventitems
);
108 static void handle_chat(JabberMessage
*jm
)
110 JabberID
*jid
= jabber_id_new(jm
->from
);
112 PurpleConnection
*gc
;
113 PurpleAccount
*account
;
115 JabberBuddyResource
*jbr
;
122 account
= purple_connection_get_account(gc
);
124 jb
= jabber_buddy_find(jm
->js
, jm
->from
, TRUE
);
125 jbr
= jabber_buddy_find_resource(jb
, jid
->resource
);
127 if (jbr
&& jm
->chat_state
!= JM_STATE_NONE
)
128 jbr
->chat_states
= JABBER_CHAT_STATES_SUPPORTED
;
130 switch(jm
->chat_state
) {
131 case JM_STATE_COMPOSING
:
132 purple_serv_got_typing(gc
, jm
->from
, 0, PURPLE_IM_TYPING
);
134 case JM_STATE_PAUSED
:
135 purple_serv_got_typing(gc
, jm
->from
, 0, PURPLE_IM_TYPED
);
137 case JM_STATE_GONE
: {
138 PurpleIMConversation
*im
= purple_conversations_find_im_with_account(
140 if (im
&& jid
->node
&& jid
->domain
) {
144 g_snprintf(buf
, sizeof(buf
), "%s@%s", jid
->node
, jid
->domain
);
146 if ((buddy
= purple_blist_find_buddy(account
, buf
))) {
150 who
= purple_buddy_get_alias(buddy
);
151 escaped
= g_markup_escape_text(who
, -1);
153 g_snprintf(buf
, sizeof(buf
),
154 _("%s has left the conversation."), escaped
);
157 /* At some point when we restructure PurpleConversation,
158 * this should be able to be implemented by removing the
159 * user from the conversation like we do with chats now. */
160 purple_conversation_write_system_message(
161 PURPLE_CONVERSATION(im
), buf
, 0);
164 purple_serv_got_typing_stopped(gc
, jm
->from
);
168 purple_serv_got_typing_stopped(gc
, jm
->from
);
171 if (jm
->js
->googletalk
&& jm
->body
&& jm
->xhtml
== NULL
) {
172 char *tmp
= jm
->body
;
173 jm
->body
= jabber_google_format_to_html(jm
->body
);
177 body
= jm_body_with_oob(jm
);
179 if(body
&& body
->len
) {
182 * We received a message from a specific resource, so
183 * we probably want a reply to go to this specific
184 * resource (i.e. bind/lock the conversation to this
187 * This works because purple_im_conversation_send gets the name
188 * from purple_conversation_get_name()
190 PurpleIMConversation
*im
;
192 im
= purple_conversations_find_im_with_account(jm
->from
, account
);
193 if (im
&& !purple_strequal(jm
->from
,
194 purple_conversation_get_name(PURPLE_CONVERSATION(im
)))) {
195 purple_debug_info("jabber", "Binding conversation to %s\n",
197 purple_conversation_set_name(PURPLE_CONVERSATION(im
), jm
->from
);
202 /* Treat SUPPORTED as a terminal with no escape :) */
203 if (jbr
->chat_states
!= JABBER_CHAT_STATES_SUPPORTED
) {
204 if (jm
->chat_state
!= JM_STATE_NONE
)
205 jbr
->chat_states
= JABBER_CHAT_STATES_SUPPORTED
;
207 jbr
->chat_states
= JABBER_CHAT_STATES_UNSUPPORTED
;
210 g_free(jbr
->thread_id
);
211 jbr
->thread_id
= g_strdup(jm
->thread_id
);
214 purple_serv_got_im(gc
, jm
->from
, body
->str
, 0, jm
->sent
);
220 g_string_free(body
, TRUE
);
223 static void handle_headline(JabberMessage
*jm
)
228 if(!jm
->xhtml
&& !jm
->body
)
229 return; /* ignore headlines without any content */
231 body
= jm_body_with_oob(jm
);
232 title
= g_strdup_printf(_("Message from %s"), jm
->from
);
234 purple_notify_formatted(jm
->js
->gc
, title
, jm
->subject
? jm
->subject
: title
,
235 NULL
, body
->str
, NULL
, NULL
);
238 g_string_free(body
, TRUE
);
241 static void handle_groupchat(JabberMessage
*jm
)
243 JabberID
*jid
= jabber_id_new(jm
->from
);
245 PurpleMessageFlags messageFlags
= 0;
250 chat
= jabber_chat_find(jm
->js
, jid
->node
, jid
->domain
);
256 purple_chat_conversation_set_topic(chat
->conv
, jid
->resource
,
258 messageFlags
|= PURPLE_MESSAGE_NO_LOG
;
259 if(!jm
->xhtml
&& !jm
->body
) {
260 char *msg
, *tmp
, *tmp2
;
261 tmp
= g_markup_escape_text(jm
->subject
, -1);
262 tmp2
= purple_markup_linkify(tmp
);
264 msg
= g_strdup_printf(_("%s has set the topic to: %s"), jid
->resource
, tmp2
);
266 msg
= g_strdup_printf(_("The topic is: %s"), tmp2
);
267 purple_conversation_write_system_message(PURPLE_CONVERSATION(chat
->conv
),
275 if(jm
->xhtml
|| jm
->body
) {
277 purple_serv_got_chat_in(jm
->js
->gc
, chat
->id
, jid
->resource
,
278 messageFlags
| (jm
->delayed
? PURPLE_MESSAGE_DELAYED
: 0),
279 jm
->xhtml
? jm
->xhtml
: jm
->body
, jm
->sent
);
281 purple_conversation_write_system_message(
282 PURPLE_CONVERSATION(chat
->conv
),
283 jm
->xhtml
? jm
->xhtml
: jm
->body
, messageFlags
);
289 static void handle_groupchat_invite(JabberMessage
*jm
)
291 GHashTable
*components
;
292 JabberID
*jid
= jabber_id_new(jm
->to
);
297 components
= g_hash_table_new_full(g_str_hash
, g_str_equal
, NULL
, g_free
);
299 g_hash_table_replace(components
, "room", g_strdup(jid
->node
));
300 g_hash_table_replace(components
, "server", g_strdup(jid
->domain
));
301 g_hash_table_replace(components
, "handle", g_strdup(jm
->js
->user
->node
));
302 g_hash_table_replace(components
, "password", g_strdup(jm
->password
));
305 purple_serv_got_chat_invite(jm
->js
->gc
, jm
->to
, jm
->from
, jm
->body
, components
);
308 static void handle_error(JabberMessage
*jm
)
315 buf
= g_strdup_printf(_("Message delivery to %s failed: %s"),
316 jm
->from
, jm
->error
? jm
->error
: "");
318 purple_notify_formatted(jm
->js
->gc
, _("XMPP Message Error"), _("XMPP Message Error"), buf
,
319 jm
->xhtml
? jm
->xhtml
: jm
->body
, NULL
, NULL
);
324 static void handle_buzz(JabberMessage
*jm
) {
325 PurpleAccount
*account
;
327 /* Delayed buzz MUST NOT be accepted */
331 /* Reject buzz when it's not enabled */
332 if(!jm
->js
->allowBuzz
)
335 account
= purple_connection_get_account(jm
->js
->gc
);
337 if (purple_blist_find_buddy(account
, jm
->from
) == NULL
)
338 return; /* Do not accept buzzes from unknown people */
340 /* xmpp only has 1 attention type, so index is 0 */
341 purple_protocol_got_attention(jm
->js
->gc
, jm
->from
, 0);
344 /* used internally by the functions below */
352 jabber_message_get_refs_from_xmlnode_internal(const PurpleXmlNode
*message
,
355 PurpleXmlNode
*child
;
357 for (child
= purple_xmlnode_get_child(message
, "img") ; child
;
358 child
= purple_xmlnode_get_next_twin(child
)) {
359 const gchar
*src
= purple_xmlnode_get_attrib(child
, "src");
361 if (g_str_has_prefix(src
, "cid:")) {
362 const gchar
*cid
= src
+ 4;
364 /* if we haven't "fetched" this yet... */
365 if (!g_hash_table_lookup(table
, cid
)) {
366 /* take a copy of the cid and let the SmileyRef own it... */
367 gchar
*temp_cid
= g_strdup(cid
);
368 JabberSmileyRef
*ref
= g_new0(JabberSmileyRef
, 1);
369 const gchar
*alt
= purple_xmlnode_get_attrib(child
, "alt");
371 /* if there is no "alt" string, use the cid...
372 include the entire src, eg. "cid:.." to avoid linkification */
373 if (alt
&& alt
[0] != '\0') {
374 /* workaround for when "alt" is set to the value of the
375 CID (which Jabbim seems to do), to avoid it showing up
376 as an mailto: link */
377 if (purple_email_is_valid(alt
)) {
378 ref
->alt
= g_strdup_printf("smiley:%s", alt
);
380 ref
->alt
= g_strdup(alt
);
383 ref
->alt
= g_strdup(src
);
385 g_hash_table_insert(table
, temp_cid
, ref
);
390 for (child
= message
->child
; child
; child
= child
->next
) {
391 jabber_message_get_refs_from_xmlnode_internal(child
, table
);
396 jabber_message_get_refs_steal(gpointer key
, gpointer value
, gpointer user_data
)
398 GList
**refs
= (GList
**) user_data
;
399 JabberSmileyRef
*ref
= (JabberSmileyRef
*) value
;
401 *refs
= g_list_append(*refs
, ref
);
407 jabber_message_get_refs_from_xmlnode(const PurpleXmlNode
*message
)
410 GHashTable
*unique_refs
= g_hash_table_new(g_str_hash
, g_str_equal
);
412 jabber_message_get_refs_from_xmlnode_internal(message
, unique_refs
);
413 (void) g_hash_table_foreach_steal(unique_refs
,
414 jabber_message_get_refs_steal
, (gpointer
) &refs
);
415 g_hash_table_destroy(unique_refs
);
420 jabber_message_xml_to_string_strip_img_smileys(PurpleXmlNode
*xhtml
)
422 gchar
*markup
= purple_xmlnode_to_str(xhtml
, NULL
);
423 int len
= strlen(markup
);
425 GString
*out
= g_string_new(NULL
);
428 /* this is a bit cludgy, maybe there is a better way to do this...
429 we need to find all <img> tags within the XHTML and replace those
430 tags with the value of their "alt" attributes */
431 if (g_str_has_prefix(&(markup
[pos
]), "<img")) {
432 PurpleXmlNode
*img
= NULL
;
436 for (; pos2
< len
; pos2
++) {
437 if (g_str_has_prefix(&(markup
[pos2
]), "/>")) {
440 } else if (g_str_has_prefix(&(markup
[pos2
]), "</img>")) {
446 /* note, if the above loop didn't find the end of the <img> tag,
447 it the parsed string will be until the end of the input string,
448 in which case purple_xmlnode_from_str will bail out and return NULL,
449 in this case the "if" statement below doesn't trigger and the
450 text is copied unchanged */
451 img
= purple_xmlnode_from_str(&(markup
[pos
]), pos2
- pos
);
452 src
= purple_xmlnode_get_attrib(img
, "src");
454 if (g_str_has_prefix(src
, "cid:")) {
455 const gchar
*alt
= purple_xmlnode_get_attrib(img
, "alt");
456 /* if the "alt" attribute is empty, put the cid as smiley string */
457 if (alt
&& alt
[0] != '\0') {
458 /* if the "alt" is the same as the CID, as Jabbim does,
459 this prevents linkification... */
460 if (purple_email_is_valid(alt
)) {
461 gchar
*safe_alt
= g_strdup_printf("smiley:%s", alt
);
462 out
= g_string_append(out
, safe_alt
);
465 gchar
*alt_escaped
= g_markup_escape_text(alt
, -1);
466 out
= g_string_append(out
, alt_escaped
);
470 out
= g_string_append(out
, src
);
474 out
= g_string_append_c(out
, markup
[pos
]);
478 purple_xmlnode_free(img
);
481 out
= g_string_append_c(out
, markup
[pos
]);
487 return g_string_free(out
, FALSE
);
491 jabber_message_add_remote_smileys(JabberStream
*js
, const gchar
*who
,
492 const PurpleXmlNode
*message
)
494 PurpleXmlNode
*data_tag
;
495 for (data_tag
= purple_xmlnode_get_child_with_namespace(message
, "data", NS_BOB
) ;
497 data_tag
= purple_xmlnode_get_next_twin(data_tag
)) {
498 const gchar
*cid
= purple_xmlnode_get_attrib(data_tag
, "cid");
499 const JabberData
*data
= jabber_data_find_remote_by_cid(js
, who
, cid
);
501 if (!data
&& cid
!= NULL
) {
502 /* we haven't cached this already, let's add it */
503 JabberData
*new_data
= jabber_data_create_from_xml(data_tag
);
506 jabber_data_associate_remote(js
, who
, new_data
);
513 jabber_message_remote_smiley_got(JabberData
*jdata
, gchar
*alt
, gpointer d
) {
514 JabberMessageRemoteSmileyAddData
*data
= (JabberMessageRemoteSmileyAddData
*)d
;
517 PurpleSmiley
*smiley
= NULL
;
519 purple_debug_info("jabber",
520 "smiley data retrieved successfully");
522 smiley
= purple_smiley_new_from_data(
524 jabber_data_get_data(jdata
),
525 jabber_data_get_size(jdata
)
528 purple_conversation_add_smiley(data
->conv
, smiley
);
530 g_object_unref(G_OBJECT(smiley
));
532 purple_debug_error("jabber", "failed retrieving smiley data");
535 g_object_unref(G_OBJECT(data
->conv
));
536 g_free(data
->shortcut
);
537 g_slice_free(JabberMessageRemoteSmileyAddData
, data
);
541 jabber_message_remote_smiley_add(JabberStream
*js
, PurpleConversation
*conv
,
542 const gchar
*from
, const gchar
*shortcut
, const gchar
*cid
)
544 PurpleSmiley
*smiley
= NULL
;
545 const JabberData
*jdata
= NULL
;
547 purple_debug_misc("jabber", "about to add remote smiley %s to the conv",
550 smiley
= purple_conversation_get_smiley(conv
, shortcut
);
551 if(PURPLE_IS_SMILEY(smiley
)) {
552 purple_debug_misc("jabber", "smiley was already present");
556 /* TODO: cache lookup by "cid" */
557 jdata
= jabber_data_find_remote_by_cid(js
, from
, cid
);
559 purple_debug_info("jabber", "smiley data is already known");
561 smiley
= purple_smiley_new_from_data(
563 jabber_data_get_data(jdata
),
564 jabber_data_get_size(jdata
)
567 purple_conversation_add_smiley(conv
, smiley
);
569 g_object_unref(G_OBJECT(smiley
));
571 JabberMessageRemoteSmileyAddData
*data
= NULL
;
573 data
= g_slice_new(JabberMessageRemoteSmileyAddData
);
574 data
->conv
= g_object_ref(conv
);
575 data
->shortcut
= g_strdup(shortcut
);
577 purple_debug_info("jabber", "smiley data is unknown, "
578 "need to request it");
580 jabber_data_request(js
, cid
, from
, NULL
, FALSE
,
581 jabber_message_remote_smiley_got
, data
);
585 void jabber_message_parse(JabberStream
*js
, PurpleXmlNode
*packet
)
588 const char *id
, *from
, *to
, *type
;
589 PurpleXmlNode
*child
;
590 gboolean signal_return
;
592 from
= purple_xmlnode_get_attrib(packet
, "from");
593 id
= purple_xmlnode_get_attrib(packet
, "id");
594 to
= purple_xmlnode_get_attrib(packet
, "to");
595 type
= purple_xmlnode_get_attrib(packet
, "type");
597 signal_return
= GPOINTER_TO_INT(purple_signal_emit_return_1(purple_connection_get_protocol(js
->gc
),
598 "jabber-receiving-message", js
->gc
, type
, id
, from
, to
, packet
));
602 jm
= g_new0(JabberMessage
, 1);
604 jm
->sent
= time(NULL
);
606 jm
->chat_state
= JM_STATE_NONE
;
609 if(purple_strequal(type
, "normal"))
610 jm
->type
= JABBER_MESSAGE_NORMAL
;
611 else if(purple_strequal(type
, "chat"))
612 jm
->type
= JABBER_MESSAGE_CHAT
;
613 else if(purple_strequal(type
, "groupchat"))
614 jm
->type
= JABBER_MESSAGE_GROUPCHAT
;
615 else if(purple_strequal(type
, "headline"))
616 jm
->type
= JABBER_MESSAGE_HEADLINE
;
617 else if(purple_strequal(type
, "error"))
618 jm
->type
= JABBER_MESSAGE_ERROR
;
620 jm
->type
= JABBER_MESSAGE_OTHER
;
622 jm
->type
= JABBER_MESSAGE_NORMAL
;
625 jm
->from
= g_strdup(from
);
626 jm
->to
= g_strdup(to
);
627 jm
->id
= g_strdup(id
);
629 for(child
= packet
->child
; child
; child
= child
->next
) {
630 const char *xmlns
= purple_xmlnode_get_namespace(child
);
631 if(child
->type
!= PURPLE_XMLNODE_TYPE_TAG
)
634 if(purple_strequal(child
->name
, "error")) {
635 const char *code
= purple_xmlnode_get_attrib(child
, "code");
636 char *code_txt
= NULL
;
637 char *text
= purple_xmlnode_get_data(child
);
639 PurpleXmlNode
*enclosed_text_node
;
641 if ((enclosed_text_node
= purple_xmlnode_get_child(child
, "text")))
642 text
= purple_xmlnode_get_data(enclosed_text_node
);
646 code_txt
= g_strdup_printf(_("(Code %s)"), code
);
649 jm
->error
= g_strdup_printf("%s%s%s",
651 text
&& code_txt
? " " : "",
652 code_txt
? code_txt
: "");
656 } else if (xmlns
== NULL
) {
657 /* QuLogic: Not certain this is correct, but it would have happened
658 with the previous code. */
659 if(purple_strequal(child
->name
, "x"))
660 jm
->etc
= g_list_append(jm
->etc
, child
);
661 /* The following tests expect xmlns != NULL */
663 } else if(purple_strequal(child
->name
, "subject") && purple_strequal(xmlns
, NS_XMPP_CLIENT
)) {
665 jm
->subject
= purple_xmlnode_get_data(child
);
667 jm
->subject
= g_strdup("");
669 } else if(purple_strequal(child
->name
, "thread") && purple_strequal(xmlns
, NS_XMPP_CLIENT
)) {
671 jm
->thread_id
= purple_xmlnode_get_data(child
);
672 } else if(purple_strequal(child
->name
, "body") && purple_strequal(xmlns
, NS_XMPP_CLIENT
)) {
674 char *msg
= purple_xmlnode_get_data(child
);
675 char *escaped
= purple_markup_escape_text(msg
, -1);
676 jm
->body
= purple_strdup_withhtml(escaped
);
680 } else if(purple_strequal(child
->name
, "html") && purple_strequal(xmlns
, NS_XHTML_IM
)) {
681 if(!jm
->xhtml
&& purple_xmlnode_get_child(child
, "body")) {
684 PurpleConnection
*gc
= js
->gc
;
685 PurpleAccount
*account
= purple_connection_get_account(gc
);
686 PurpleConversation
*conv
= NULL
;
687 GList
*smiley_refs
= NULL
, *it
;
688 gchar
*reformatted_xhtml
;
690 if (purple_account_get_bool(account
, "custom_smileys", TRUE
)) {
691 /* find a list of smileys ("cid" and "alt" text pairs)
692 occuring in the message */
693 smiley_refs
= jabber_message_get_refs_from_xmlnode(child
);
694 purple_debug_info("jabber", "found %d smileys\n",
695 g_list_length(smiley_refs
));
698 if (jm
->type
== JABBER_MESSAGE_GROUPCHAT
) {
699 JabberID
*jid
= jabber_id_new(jm
->from
);
700 JabberChat
*chat
= NULL
;
703 chat
= jabber_chat_find(js
, jid
->node
, jid
->domain
);
705 conv
= PURPLE_CONVERSATION(chat
->conv
);
708 } else if (jm
->type
== JABBER_MESSAGE_NORMAL
||
709 jm
->type
== JABBER_MESSAGE_CHAT
) {
711 purple_conversations_find_with_account(from
, account
);
713 /* we need to create the conversation here */
714 conv
= PURPLE_CONVERSATION(
715 purple_im_conversation_new(account
, from
));
720 /* process any newly provided smileys */
721 jabber_message_add_remote_smileys(js
, to
, packet
);
724 purple_xmlnode_strip_prefixes(child
);
726 /* reformat xhtml so that img tags with a "cid:" src gets
727 translated to the bare text of the emoticon (the "alt" attrib) */
728 /* this is done also when custom smiley retrieval is turned off,
729 this way the receiver always sees the shortcut instead */
731 jabber_message_xml_to_string_strip_img_smileys(child
);
733 jm
->xhtml
= reformatted_xhtml
;
735 /* add known custom emoticons to the conversation */
736 /* note: if there were no smileys in the incoming message, or
737 if receiving custom smileys is turned off, smiley_refs will
739 for (it
= smiley_refs
; it
; it
= g_list_next(it
)) {
740 JabberSmileyRef
*ref
= it
->data
;
743 jabber_message_remote_smiley_add(js
,
744 conv
, from
, ref
->alt
, ref
->cid
);
751 g_list_free(smiley_refs
);
753 /* Convert all newlines to whitespace. Technically, even regular, non-XML HTML is supposed to ignore newlines, but Pidgin has, as convention
754 * treated \n as a newline for compatibility with other protocols
756 for (c
= jm
->xhtml
; *c
!= '\0'; c
++) {
761 } else if(purple_strequal(child
->name
, "active") && purple_strequal(xmlns
,"http://jabber.org/protocol/chatstates")) {
762 jm
->chat_state
= JM_STATE_ACTIVE
;
763 } else if(purple_strequal(child
->name
, "composing") && purple_strequal(xmlns
,"http://jabber.org/protocol/chatstates")) {
764 jm
->chat_state
= JM_STATE_COMPOSING
;
765 } else if(purple_strequal(child
->name
, "paused") && purple_strequal(xmlns
,"http://jabber.org/protocol/chatstates")) {
766 jm
->chat_state
= JM_STATE_PAUSED
;
767 } else if(purple_strequal(child
->name
, "inactive") && purple_strequal(xmlns
,"http://jabber.org/protocol/chatstates")) {
768 jm
->chat_state
= JM_STATE_INACTIVE
;
769 } else if(purple_strequal(child
->name
, "gone") && purple_strequal(xmlns
,"http://jabber.org/protocol/chatstates")) {
770 jm
->chat_state
= JM_STATE_GONE
;
771 } else if(purple_strequal(child
->name
, "event") && purple_strequal(xmlns
,"http://jabber.org/protocol/pubsub#event")) {
772 PurpleXmlNode
*items
;
773 jm
->type
= JABBER_MESSAGE_EVENT
;
774 for(items
= purple_xmlnode_get_child(child
,"items"); items
; items
= items
->next
)
775 jm
->eventitems
= g_list_append(jm
->eventitems
, items
);
776 } else if(purple_strequal(child
->name
, "attention") && purple_strequal(xmlns
, NS_ATTENTION
)) {
778 } else if(purple_strequal(child
->name
, "delay") && purple_strequal(xmlns
, NS_DELAYED_DELIVERY
)) {
779 const char *timestamp
= purple_xmlnode_get_attrib(child
, "stamp");
782 jm
->sent
= purple_str_to_time(timestamp
, TRUE
, NULL
, NULL
, NULL
);
783 } else if(purple_strequal(child
->name
, "x")) {
784 if(purple_strequal(xmlns
, NS_DELAYED_DELIVERY_LEGACY
)) {
785 const char *timestamp
= purple_xmlnode_get_attrib(child
, "stamp");
788 jm
->sent
= purple_str_to_time(timestamp
, TRUE
, NULL
, NULL
, NULL
);
789 } else if(purple_strequal(xmlns
, "jabber:x:conference") &&
790 jm
->type
!= JABBER_MESSAGE_GROUPCHAT_INVITE
&&
791 jm
->type
!= JABBER_MESSAGE_ERROR
) {
792 const char *jid
= purple_xmlnode_get_attrib(child
, "jid");
794 const char *reason
= purple_xmlnode_get_attrib(child
, "reason");
795 const char *password
= purple_xmlnode_get_attrib(child
, "password");
797 jm
->type
= JABBER_MESSAGE_GROUPCHAT_INVITE
;
799 jm
->to
= g_strdup(jid
);
803 jm
->body
= g_strdup(reason
);
807 g_free(jm
->password
);
808 jm
->password
= g_strdup(password
);
811 } else if(purple_strequal(xmlns
, "http://jabber.org/protocol/muc#user") &&
812 jm
->type
!= JABBER_MESSAGE_ERROR
) {
813 PurpleXmlNode
*invite
= purple_xmlnode_get_child(child
, "invite");
815 PurpleXmlNode
*reason
, *password
;
816 const char *jid
= purple_xmlnode_get_attrib(invite
, "from");
819 jm
->from
= g_strdup(jid
);
820 if((reason
= purple_xmlnode_get_child(invite
, "reason"))) {
822 jm
->body
= purple_xmlnode_get_data(reason
);
824 if((password
= purple_xmlnode_get_child(child
, "password"))) {
825 g_free(jm
->password
);
826 jm
->password
= purple_xmlnode_get_data(password
);
829 jm
->type
= JABBER_MESSAGE_GROUPCHAT_INVITE
;
832 jm
->etc
= g_list_append(jm
->etc
, child
);
834 } else if (purple_strequal(child
->name
, "query")) {
835 const char *node
= purple_xmlnode_get_attrib(child
, "node");
836 if (purple_strequal(xmlns
, NS_DISCO_ITEMS
)
837 && purple_strequal(node
, "http://jabber.org/protocol/commands")) {
838 jabber_adhoc_got_list(js
, jm
->from
, child
);
847 case JABBER_MESSAGE_OTHER
:
848 purple_debug_info("jabber",
849 "Received message of unknown type: %s\n", type
);
850 /* Fall-through is intentional */
851 case JABBER_MESSAGE_NORMAL
:
852 case JABBER_MESSAGE_CHAT
:
855 case JABBER_MESSAGE_HEADLINE
:
858 case JABBER_MESSAGE_GROUPCHAT
:
859 handle_groupchat(jm
);
861 case JABBER_MESSAGE_GROUPCHAT_INVITE
:
862 handle_groupchat_invite(jm
);
864 case JABBER_MESSAGE_EVENT
:
865 jabber_handle_event(jm
);
867 case JABBER_MESSAGE_ERROR
:
871 jabber_message_free(jm
);
875 jabber_conv_support_custom_smileys(JabberStream
*js
,
876 PurpleConversation
*conv
,
882 if (PURPLE_IS_IM_CONVERSATION(conv
)) {
883 jb
= jabber_buddy_find(js
, who
, FALSE
);
885 return jabber_buddy_has_capability(jb
, NS_BOB
);
889 } else if (PURPLE_IS_CHAT_CONVERSATION(conv
)) {
890 chat
= jabber_chat_find_by_conv(PURPLE_CHAT_CONVERSATION(conv
));
892 /* do not attempt to send custom smileys in a MUC with more than
893 10 people, to avoid getting too many BoB requests */
894 return jabber_chat_get_num_participants(chat
) <= 10 &&
895 jabber_chat_all_participants_have_capability(chat
,
906 jabber_message_smileyify_cb(GString
*out
, PurpleSmiley
*smiley
,
907 PurpleConversation
*_empty
, gpointer _unused
)
909 const gchar
*shortcut
;
910 const JabberData
*data
;
911 PurpleXmlNode
*smiley_node
;
914 shortcut
= purple_smiley_get_shortcut(smiley
);
915 data
= jabber_data_find_local_by_alt(shortcut
);
920 smiley_node
= jabber_data_get_xhtml_im(data
, shortcut
);
921 node_xml
= purple_xmlnode_to_str(smiley_node
, NULL
);
923 g_string_append(out
, node_xml
);
925 purple_xmlnode_free(smiley_node
);
932 jabber_message_smileyfy_xhtml(JabberMessage
*jm
, const char *xhtml
)
934 PurpleAccount
*account
= purple_connection_get_account(jm
->js
->gc
);
935 GList
*found_smileys
, *it
, *it_next
;
936 PurpleConversation
*conv
;
937 gboolean has_too_large_smiley
= FALSE
;
938 gchar
*smileyfied_xhtml
= NULL
;
940 conv
= purple_conversations_find_with_account(jm
->to
, account
);
942 if (!jabber_conv_support_custom_smileys(jm
->js
, conv
, jm
->to
))
945 found_smileys
= purple_smiley_parser_find(
946 purple_smiley_custom_get_list(), xhtml
, TRUE
);
950 for (it
= found_smileys
; it
; it
= it_next
) {
951 PurpleSmiley
*smiley
= it
->data
;
952 gboolean valid
= TRUE
;
954 it_next
= g_list_next(it
);
956 if (purple_image_get_data_size(PURPLE_IMAGE(smiley
)) > JABBER_DATA_MAX_SIZE
) {
957 has_too_large_smiley
= TRUE
;
959 purple_debug_warning("jabber", "Refusing to send "
960 "smiley %s (too large, max is %d)",
961 purple_smiley_get_shortcut(smiley
),
962 JABBER_DATA_MAX_SIZE
);
966 found_smileys
= g_list_delete_link(found_smileys
, it
);
969 if (has_too_large_smiley
) {
970 purple_conversation_write_system_message(conv
,
971 _("A custom smiley in the message is too large to send."),
972 PURPLE_MESSAGE_ERROR
);
978 for (it
= found_smileys
; it
; it
= g_list_next(it
)) {
979 PurpleSmiley
*smiley
= it
->data
;
980 const gchar
*shortcut
= purple_smiley_get_shortcut(smiley
);
981 const gchar
*mimetype
;
984 /* the object has been sent before */
985 if (jabber_data_find_local_by_alt(shortcut
))
988 mimetype
= purple_image_get_mimetype(PURPLE_IMAGE(smiley
));
990 purple_debug_error("jabber",
991 "unknown mime type for image");
995 jdata
= jabber_data_create_from_data(
996 purple_image_get_data(PURPLE_IMAGE(smiley
)),
997 purple_image_get_data_size(PURPLE_IMAGE(smiley
)),
998 mimetype
, FALSE
, jm
->js
);
1000 purple_debug_info("jabber", "cache local smiley alt=%s, cid=%s",
1001 shortcut
, jabber_data_get_cid(jdata
));
1002 jabber_data_associate_local(jdata
, shortcut
);
1005 g_list_free(found_smileys
);
1007 smileyfied_xhtml
= purple_smiley_parser_replace(
1008 purple_smiley_custom_get_list(), xhtml
,
1009 jabber_message_smileyify_cb
, NULL
);
1011 return smileyfied_xhtml
;
1014 void jabber_message_send(JabberMessage
*jm
)
1016 PurpleXmlNode
*message
, *child
;
1017 const char *type
= NULL
;
1019 message
= purple_xmlnode_new("message");
1022 case JABBER_MESSAGE_NORMAL
:
1025 case JABBER_MESSAGE_CHAT
:
1026 case JABBER_MESSAGE_GROUPCHAT_INVITE
:
1029 case JABBER_MESSAGE_HEADLINE
:
1032 case JABBER_MESSAGE_GROUPCHAT
:
1035 case JABBER_MESSAGE_ERROR
:
1038 case JABBER_MESSAGE_OTHER
:
1045 purple_xmlnode_set_attrib(message
, "type", type
);
1048 purple_xmlnode_set_attrib(message
, "id", jm
->id
);
1050 purple_xmlnode_set_attrib(message
, "to", jm
->to
);
1053 child
= purple_xmlnode_new_child(message
, "thread");
1054 purple_xmlnode_insert_data(child
, jm
->thread_id
, -1);
1058 switch(jm
->chat_state
)
1060 case JM_STATE_ACTIVE
:
1061 child
= purple_xmlnode_new_child(message
, "active");
1063 case JM_STATE_COMPOSING
:
1064 child
= purple_xmlnode_new_child(message
, "composing");
1066 case JM_STATE_PAUSED
:
1067 child
= purple_xmlnode_new_child(message
, "paused");
1069 case JM_STATE_INACTIVE
:
1070 child
= purple_xmlnode_new_child(message
, "inactive");
1073 child
= purple_xmlnode_new_child(message
, "gone");
1080 purple_xmlnode_set_namespace(child
, "http://jabber.org/protocol/chatstates");
1083 child
= purple_xmlnode_new_child(message
, "subject");
1084 purple_xmlnode_insert_data(child
, jm
->subject
, -1);
1088 child
= purple_xmlnode_new_child(message
, "body");
1089 purple_xmlnode_insert_data(child
, jm
->body
, -1);
1093 if ((child
= purple_xmlnode_from_str(jm
->xhtml
, -1))) {
1094 purple_xmlnode_insert_child(message
, child
);
1096 purple_debug_error("jabber",
1097 "XHTML translation/validation failed, returning: %s\n",
1102 jabber_send(jm
->js
, message
);
1104 purple_xmlnode_free(message
);
1108 * Compare the XHTML and plain strings passed in for "equality". Any HTML markup
1109 * other than <br/> (matches a newline) in the XHTML will cause this to return
1113 jabber_xhtml_plain_equal(const char *xhtml_escaped
,
1119 char *xhtml
= purple_unescape_html(xhtml_escaped
);
1121 while (xhtml
[i
] && plain
[j
]) {
1122 if (xhtml
[i
] == plain
[j
]) {
1128 if (plain
[j
] == '\n' && !strncmp(xhtml
+i
, "<br/>", 5)) {
1138 /* Are we at the end of both strings? */
1139 ret
= (xhtml
[i
] == plain
[j
]) && (xhtml
[i
] == '\0');
1144 int jabber_message_send_im(PurpleConnection
*gc
, PurpleMessage
*msg
)
1148 JabberBuddyResource
*jbr
;
1152 const gchar
*rcpt
= purple_message_get_recipient(msg
);
1154 if (!rcpt
|| purple_message_is_empty(msg
))
1157 resource
= jabber_get_resource(rcpt
);
1159 jb
= jabber_buddy_find(purple_connection_get_protocol_data(gc
), rcpt
, TRUE
);
1160 jbr
= jabber_buddy_find_resource(jb
, resource
);
1164 jm
= g_new0(JabberMessage
, 1);
1165 jm
->js
= purple_connection_get_protocol_data(gc
);
1166 jm
->type
= JABBER_MESSAGE_CHAT
;
1167 jm
->chat_state
= JM_STATE_ACTIVE
;
1168 jm
->to
= g_strdup(rcpt
);
1169 jm
->id
= jabber_get_next_id(jm
->js
);
1173 jm
->thread_id
= jbr
->thread_id
;
1175 if (jbr
->chat_states
== JABBER_CHAT_STATES_UNSUPPORTED
)
1176 jm
->chat_state
= JM_STATE_NONE
;
1178 /* if(JABBER_CHAT_STATES_UNKNOWN == jbr->chat_states)
1179 jbr->chat_states = JABBER_CHAT_STATES_UNSUPPORTED; */
1183 tmp
= purple_utf8_strip_unprintables(purple_message_get_contents(msg
));
1184 purple_markup_html_to_xhtml(tmp
, &xhtml
, &jm
->body
);
1187 tmp
= jabber_message_smileyfy_xhtml(jm
, xhtml
);
1194 * For backward compatibility with user expectations or for those not on
1195 * the user's roster, allow sending XHTML-IM markup.
1197 if (!jbr
|| !jbr
->caps
.info
||
1198 jabber_resource_has_capability(jbr
, NS_XHTML_IM
)) {
1199 if (!jabber_xhtml_plain_equal(xhtml
, jm
->body
))
1200 /* Wrap the message in <p/> for great interoperability justice. */
1201 jm
->xhtml
= g_strdup_printf("<html xmlns='" NS_XHTML_IM
"'><body xmlns='" NS_XHTML
"'><p>%s</p></body></html>", xhtml
);
1206 jabber_message_send(jm
);
1207 jabber_message_free(jm
);
1211 int jabber_message_send_chat(PurpleConnection
*gc
, int id
, PurpleMessage
*msg
)
1219 if (!gc
|| purple_message_is_empty(msg
))
1222 js
= purple_connection_get_protocol_data(gc
);
1223 chat
= jabber_chat_find_by_id(js
, id
);
1228 jm
= g_new0(JabberMessage
, 1);
1229 jm
->js
= purple_connection_get_protocol_data(gc
);
1230 jm
->type
= JABBER_MESSAGE_GROUPCHAT
;
1231 jm
->to
= g_strdup_printf("%s@%s", chat
->room
, chat
->server
);
1232 jm
->id
= jabber_get_next_id(jm
->js
);
1234 tmp
= purple_utf8_strip_unprintables(purple_message_get_contents(msg
));
1235 purple_markup_html_to_xhtml(tmp
, &xhtml
, &jm
->body
);
1237 tmp
= jabber_message_smileyfy_xhtml(jm
, xhtml
);
1243 if (chat
->xhtml
&& !jabber_xhtml_plain_equal(xhtml
, jm
->body
))
1244 /* Wrap the message in <p/> for greater interoperability justice. */
1245 jm
->xhtml
= g_strdup_printf("<html xmlns='" NS_XHTML_IM
"'><body xmlns='" NS_XHTML
"'><p>%s</p></body></html>", xhtml
);
1249 jabber_message_send(jm
);
1250 jabber_message_free(jm
);
1255 unsigned int jabber_send_typing(PurpleConnection
*gc
, const char *who
, PurpleIMTypingState state
)
1260 JabberBuddyResource
*jbr
;
1263 js
= purple_connection_get_protocol_data(gc
);
1264 jb
= jabber_buddy_find(js
, who
, TRUE
);
1268 resource
= jabber_get_resource(who
);
1269 jbr
= jabber_buddy_find_resource(jb
, resource
);
1272 /* We know this entity doesn't support chat states */
1273 if (jbr
&& jbr
->chat_states
== JABBER_CHAT_STATES_UNSUPPORTED
)
1276 /* *If* we don't have presence /and/ the buddy can't see our
1277 * presence, don't send typing notifications.
1279 if (!jbr
&& !(jb
->subscription
& JABBER_SUB_FROM
))
1282 /* TODO: figure out threading */
1283 jm
= g_new0(JabberMessage
, 1);
1285 jm
->type
= JABBER_MESSAGE_CHAT
;
1286 jm
->to
= g_strdup(who
);
1287 jm
->id
= jabber_get_next_id(jm
->js
);
1289 if(PURPLE_IM_TYPING
== state
)
1290 jm
->chat_state
= JM_STATE_COMPOSING
;
1291 else if(PURPLE_IM_TYPED
== state
)
1292 jm
->chat_state
= JM_STATE_PAUSED
;
1294 jm
->chat_state
= JM_STATE_ACTIVE
;
1296 /* if(JABBER_CHAT_STATES_UNKNOWN == jbr->chat_states)
1297 jbr->chat_states = JABBER_CHAT_STATES_UNSUPPORTED; */
1299 jabber_message_send(jm
);
1300 jabber_message_free(jm
);
1305 gboolean
jabber_buzz_isenabled(JabberStream
*js
, const gchar
*namespace) {
1306 return js
->allowBuzz
;
1309 gboolean
jabber_custom_smileys_isenabled(JabberStream
*js
, const gchar
*namespace)
1311 PurpleAccount
*account
= purple_connection_get_account(js
->gc
);
1313 return purple_account_get_bool(account
, "custom_smileys", TRUE
);