Replace functions which called once with their bodies
[pidgin-git.git] / libpurple / protocols / jabber / message.c
blob98c35ed8b3f69db6b517a2555b07956b2661bd87
1 /*
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
6 * source distribution.
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
23 #include "internal.h"
25 #include "debug.h"
26 #include "notify.h"
27 #include "smiley-custom.h"
28 #include "smiley-parser.h"
29 #include "server.h"
30 #include "util.h"
31 #include "adhoccommands.h"
32 #include "buddy.h"
33 #include "chat.h"
34 #include "data.h"
35 #include "google/google.h"
36 #include "message.h"
37 #include "xmlnode.h"
38 #include "pep.h"
39 #include "smiley.h"
40 #include "iq.h"
42 #include <string.h>
44 typedef struct {
45 PurpleConversation *conv;
46 gchar *shortcut;
47 } JabberMessageRemoteSmileyAddData;
49 static GString *jm_body_with_oob(JabberMessage *jm) {
50 GList *etc;
51 GString *body = g_string_new("");
53 if(jm->xhtml)
54 g_string_append(body, jm->xhtml);
55 else if(jm->body)
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");
68 if(!url)
69 continue;
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>",
76 urltxt, desctxt);
77 else
78 g_string_printf(body, "<a href='%s'>%s</a>",
79 urltxt, desctxt);
81 g_free(urltxt);
83 if(desctxt != urltxt)
84 g_free(desctxt);
88 return body;
91 void jabber_message_free(JabberMessage *jm)
93 g_free(jm->from);
94 g_free(jm->to);
95 g_free(jm->id);
96 g_free(jm->subject);
97 g_free(jm->body);
98 g_free(jm->xhtml);
99 g_free(jm->password);
100 g_free(jm->error);
101 g_free(jm->thread_id);
102 g_list_free(jm->etc);
103 g_list_free(jm->eventitems);
105 g_free(jm);
108 static void handle_chat(JabberMessage *jm)
110 JabberID *jid = jabber_id_new(jm->from);
112 PurpleConnection *gc;
113 PurpleAccount *account;
114 JabberBuddy *jb;
115 JabberBuddyResource *jbr;
116 GString *body;
118 if(!jid)
119 return;
121 gc = jm->js->gc;
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);
133 break;
134 case JM_STATE_PAUSED:
135 purple_serv_got_typing(gc, jm->from, 0, PURPLE_IM_TYPED);
136 break;
137 case JM_STATE_GONE: {
138 PurpleIMConversation *im = purple_conversations_find_im_with_account(
139 jm->from, account);
140 if (im && jid->node && jid->domain) {
141 char buf[256];
142 PurpleBuddy *buddy;
144 g_snprintf(buf, sizeof(buf), "%s@%s", jid->node, jid->domain);
146 if ((buddy = purple_blist_find_buddy(account, buf))) {
147 const char *who;
148 char *escaped;
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);
155 g_free(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);
165 break;
167 default:
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);
174 g_free(tmp);
177 body = jm_body_with_oob(jm);
179 if(body && body->len) {
180 if (jid->resource) {
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
185 * resource).
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",
196 jm->from);
197 purple_conversation_set_name(PURPLE_CONVERSATION(im), jm->from);
201 if(jbr) {
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;
206 else
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);
217 jabber_id_free(jid);
219 if(body)
220 g_string_free(body, TRUE);
223 static void handle_headline(JabberMessage *jm)
225 char *title;
226 GString *body;
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);
237 g_free(title);
238 g_string_free(body, TRUE);
241 static void handle_groupchat(JabberMessage *jm)
243 JabberID *jid = jabber_id_new(jm->from);
244 JabberChat *chat;
245 PurpleMessageFlags messageFlags = 0;
247 if(!jid)
248 return;
250 chat = jabber_chat_find(jm->js, jid->node, jid->domain);
252 if(!chat)
253 return;
255 if(jm->subject) {
256 purple_chat_conversation_set_topic(chat->conv, jid->resource,
257 jm->subject);
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);
263 if(jid->resource)
264 msg = g_strdup_printf(_("%s has set the topic to: %s"), jid->resource, tmp2);
265 else
266 msg = g_strdup_printf(_("The topic is: %s"), tmp2);
267 purple_conversation_write_system_message(PURPLE_CONVERSATION(chat->conv),
268 msg, messageFlags);
269 g_free(tmp);
270 g_free(tmp2);
271 g_free(msg);
275 if(jm->xhtml || jm->body) {
276 if(jid->resource)
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);
280 else if(chat->muc)
281 purple_conversation_write_system_message(
282 PURPLE_CONVERSATION(chat->conv),
283 jm->xhtml ? jm->xhtml : jm->body, messageFlags);
286 jabber_id_free(jid);
289 static void handle_groupchat_invite(JabberMessage *jm)
291 GHashTable *components;
292 JabberID *jid = jabber_id_new(jm->to);
294 if(!jid)
295 return;
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));
304 jabber_id_free(jid);
305 purple_serv_got_chat_invite(jm->js->gc, jm->to, jm->from, jm->body, components);
308 static void handle_error(JabberMessage *jm)
310 char *buf;
312 if(!jm->body)
313 return;
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);
321 g_free(buf);
324 static void handle_buzz(JabberMessage *jm) {
325 PurpleAccount *account;
327 /* Delayed buzz MUST NOT be accepted */
328 if(jm->delayed)
329 return;
331 /* Reject buzz when it's not enabled */
332 if(!jm->js->allowBuzz)
333 return;
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 */
345 typedef struct {
346 gchar *cid;
347 gchar *alt;
348 } JabberSmileyRef;
351 static void
352 jabber_message_get_refs_from_xmlnode_internal(const PurpleXmlNode *message,
353 GHashTable *table)
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");
370 ref->cid = temp_cid;
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);
379 } else {
380 ref->alt = g_strdup(alt);
382 } else {
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);
395 static gboolean
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);
403 return TRUE;
406 static GList *
407 jabber_message_get_refs_from_xmlnode(const PurpleXmlNode *message)
409 GList *refs = NULL;
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);
416 return refs;
419 static gchar *
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);
424 int pos = 0;
425 GString *out = g_string_new(NULL);
427 while (pos < len) {
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;
433 int pos2 = pos;
434 const gchar *src;
436 for (; pos2 < len ; pos2++) {
437 if (g_str_has_prefix(&(markup[pos2]), "/>")) {
438 pos2 += 2;
439 break;
440 } else if (g_str_has_prefix(&(markup[pos2]), "</img>")) {
441 pos2 += 5;
442 break;
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);
463 g_free(safe_alt);
464 } else {
465 gchar *alt_escaped = g_markup_escape_text(alt, -1);
466 out = g_string_append(out, alt_escaped);
467 g_free(alt_escaped);
469 } else {
470 out = g_string_append(out, src);
472 pos += pos2 - pos;
473 } else {
474 out = g_string_append_c(out, markup[pos]);
475 pos++;
478 purple_xmlnode_free(img);
480 } else {
481 out = g_string_append_c(out, markup[pos]);
482 pos++;
486 g_free(markup);
487 return g_string_free(out, FALSE);
490 static void
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) ;
496 data_tag ;
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);
505 if (new_data) {
506 jabber_data_associate_remote(js, who, new_data);
512 static void
513 jabber_message_remote_smiley_got(JabberData *jdata, gchar *alt, gpointer d) {
514 JabberMessageRemoteSmileyAddData *data = (JabberMessageRemoteSmileyAddData *)d;
516 if (jdata) {
517 PurpleSmiley *smiley = NULL;
519 purple_debug_info("jabber",
520 "smiley data retrieved successfully");
522 smiley = purple_smiley_new_from_data(
523 data->shortcut,
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));
531 } else {
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);
540 static void
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",
548 shortcut);
550 smiley = purple_conversation_get_smiley(conv, shortcut);
551 if(PURPLE_IS_SMILEY(smiley)) {
552 purple_debug_misc("jabber", "smiley was already present");
553 return;
556 /* TODO: cache lookup by "cid" */
557 jdata = jabber_data_find_remote_by_cid(js, from, cid);
558 if (jdata) {
559 purple_debug_info("jabber", "smiley data is already known");
561 smiley = purple_smiley_new_from_data(
562 shortcut,
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));
570 } else {
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)
587 JabberMessage *jm;
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));
599 if (signal_return)
600 return;
602 jm = g_new0(JabberMessage, 1);
603 jm->js = js;
604 jm->sent = time(NULL);
605 jm->delayed = FALSE;
606 jm->chat_state = JM_STATE_NONE;
608 if(type) {
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;
619 else
620 jm->type = JABBER_MESSAGE_OTHER;
621 } else {
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)
632 continue;
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);
638 if (!text) {
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);
645 if(code)
646 code_txt = g_strdup_printf(_("(Code %s)"), code);
648 if(!jm->error)
649 jm->error = g_strdup_printf("%s%s%s",
650 text ? text : "",
651 text && code_txt ? " " : "",
652 code_txt ? code_txt : "");
654 g_free(code_txt);
655 g_free(text);
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 */
662 continue;
663 } else if(purple_strequal(child->name, "subject") && purple_strequal(xmlns, NS_XMPP_CLIENT)) {
664 if(!jm->subject) {
665 jm->subject = purple_xmlnode_get_data(child);
666 if(!jm->subject)
667 jm->subject = g_strdup("");
669 } else if(purple_strequal(child->name, "thread") && purple_strequal(xmlns, NS_XMPP_CLIENT)) {
670 if(!jm->thread_id)
671 jm->thread_id = purple_xmlnode_get_data(child);
672 } else if(purple_strequal(child->name, "body") && purple_strequal(xmlns, NS_XMPP_CLIENT)) {
673 if(!jm->body) {
674 char *msg = purple_xmlnode_get_data(child);
675 char *escaped = purple_markup_escape_text(msg, -1);
676 jm->body = purple_strdup_withhtml(escaped);
677 g_free(escaped);
678 g_free(msg);
680 } else if(purple_strequal(child->name, "html") && purple_strequal(xmlns, NS_XHTML_IM)) {
681 if(!jm->xhtml && purple_xmlnode_get_child(child, "body")) {
682 char *c;
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));
697 if (smiley_refs) {
698 if (jm->type == JABBER_MESSAGE_GROUPCHAT) {
699 JabberID *jid = jabber_id_new(jm->from);
700 JabberChat *chat = NULL;
702 if (jid) {
703 chat = jabber_chat_find(js, jid->node, jid->domain);
704 if (chat)
705 conv = PURPLE_CONVERSATION(chat->conv);
706 jabber_id_free(jid);
708 } else if (jm->type == JABBER_MESSAGE_NORMAL ||
709 jm->type == JABBER_MESSAGE_CHAT) {
710 conv =
711 purple_conversations_find_with_account(from, account);
712 if (!conv) {
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 */
730 reformatted_xhtml =
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
738 be NULL */
739 for (it = smiley_refs; it; it = g_list_next(it)) {
740 JabberSmileyRef *ref = it->data;
742 if (conv) {
743 jabber_message_remote_smiley_add(js,
744 conv, from, ref->alt, ref->cid);
747 g_free(ref->cid);
748 g_free(ref->alt);
749 g_free(ref);
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++) {
757 if (*c == '\n')
758 *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)) {
777 jm->hasBuzz = TRUE;
778 } else if(purple_strequal(child->name, "delay") && purple_strequal(xmlns, NS_DELAYED_DELIVERY)) {
779 const char *timestamp = purple_xmlnode_get_attrib(child, "stamp");
780 jm->delayed = TRUE;
781 if(timestamp)
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");
786 jm->delayed = TRUE;
787 if(timestamp)
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");
793 if(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;
798 g_free(jm->to);
799 jm->to = g_strdup(jid);
801 if (reason) {
802 g_free(jm->body);
803 jm->body = g_strdup(reason);
806 if (password) {
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");
814 if(invite) {
815 PurpleXmlNode *reason, *password;
816 const char *jid = purple_xmlnode_get_attrib(invite, "from");
817 g_free(jm->to);
818 jm->to = jm->from;
819 jm->from = g_strdup(jid);
820 if((reason = purple_xmlnode_get_child(invite, "reason"))) {
821 g_free(jm->body);
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;
831 } else {
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);
843 if(jm->hasBuzz)
844 handle_buzz(jm);
846 switch(jm->type) {
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:
853 handle_chat(jm);
854 break;
855 case JABBER_MESSAGE_HEADLINE:
856 handle_headline(jm);
857 break;
858 case JABBER_MESSAGE_GROUPCHAT:
859 handle_groupchat(jm);
860 break;
861 case JABBER_MESSAGE_GROUPCHAT_INVITE:
862 handle_groupchat_invite(jm);
863 break;
864 case JABBER_MESSAGE_EVENT:
865 jabber_handle_event(jm);
866 break;
867 case JABBER_MESSAGE_ERROR:
868 handle_error(jm);
869 break;
871 jabber_message_free(jm);
874 static gboolean
875 jabber_conv_support_custom_smileys(JabberStream *js,
876 PurpleConversation *conv,
877 const gchar *who)
879 JabberBuddy *jb;
880 JabberChat *chat;
882 if (PURPLE_IS_IM_CONVERSATION(conv)) {
883 jb = jabber_buddy_find(js, who, FALSE);
884 if (jb) {
885 return jabber_buddy_has_capability(jb, NS_BOB);
886 } else {
887 return FALSE;
889 } else if (PURPLE_IS_CHAT_CONVERSATION(conv)) {
890 chat = jabber_chat_find_by_conv(PURPLE_CHAT_CONVERSATION(conv));
891 if (chat) {
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,
896 NS_BOB);
897 } else {
898 return FALSE;
900 } else {
901 return FALSE;
905 static gboolean
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;
912 gchar *node_xml;
914 shortcut = purple_smiley_get_shortcut(smiley);
915 data = jabber_data_find_local_by_alt(shortcut);
917 if (!data)
918 return FALSE;
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);
926 g_free(node_xml);
928 return TRUE;
931 static char *
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))
943 return NULL;
945 found_smileys = purple_smiley_parser_find(
946 purple_smiley_custom_get_list(), xhtml, TRUE);
947 if (!found_smileys)
948 return NULL;
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;
958 valid = FALSE;
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);
965 if (!valid)
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);
975 if (!found_smileys)
976 return NULL;
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;
982 JabberData *jdata;
984 /* the object has been sent before */
985 if (jabber_data_find_local_by_alt(shortcut))
986 continue;
988 mimetype = purple_image_get_mimetype(PURPLE_IMAGE(smiley));
989 if (!mimetype) {
990 purple_debug_error("jabber",
991 "unknown mime type for image");
992 continue;
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");
1021 switch(jm->type) {
1022 case JABBER_MESSAGE_NORMAL:
1023 type = "normal";
1024 break;
1025 case JABBER_MESSAGE_CHAT:
1026 case JABBER_MESSAGE_GROUPCHAT_INVITE:
1027 type = "chat";
1028 break;
1029 case JABBER_MESSAGE_HEADLINE:
1030 type = "headline";
1031 break;
1032 case JABBER_MESSAGE_GROUPCHAT:
1033 type = "groupchat";
1034 break;
1035 case JABBER_MESSAGE_ERROR:
1036 type = "error";
1037 break;
1038 case JABBER_MESSAGE_OTHER:
1039 default:
1040 type = NULL;
1041 break;
1044 if(type)
1045 purple_xmlnode_set_attrib(message, "type", type);
1047 if (jm->id)
1048 purple_xmlnode_set_attrib(message, "id", jm->id);
1050 purple_xmlnode_set_attrib(message, "to", jm->to);
1052 if(jm->thread_id) {
1053 child = purple_xmlnode_new_child(message, "thread");
1054 purple_xmlnode_insert_data(child, jm->thread_id, -1);
1057 child = NULL;
1058 switch(jm->chat_state)
1060 case JM_STATE_ACTIVE:
1061 child = purple_xmlnode_new_child(message, "active");
1062 break;
1063 case JM_STATE_COMPOSING:
1064 child = purple_xmlnode_new_child(message, "composing");
1065 break;
1066 case JM_STATE_PAUSED:
1067 child = purple_xmlnode_new_child(message, "paused");
1068 break;
1069 case JM_STATE_INACTIVE:
1070 child = purple_xmlnode_new_child(message, "inactive");
1071 break;
1072 case JM_STATE_GONE:
1073 child = purple_xmlnode_new_child(message, "gone");
1074 break;
1075 case JM_STATE_NONE:
1076 /* yep, nothing */
1077 break;
1079 if(child)
1080 purple_xmlnode_set_namespace(child, "http://jabber.org/protocol/chatstates");
1082 if(jm->subject) {
1083 child = purple_xmlnode_new_child(message, "subject");
1084 purple_xmlnode_insert_data(child, jm->subject, -1);
1087 if(jm->body) {
1088 child = purple_xmlnode_new_child(message, "body");
1089 purple_xmlnode_insert_data(child, jm->body, -1);
1092 if(jm->xhtml) {
1093 if ((child = purple_xmlnode_from_str(jm->xhtml, -1))) {
1094 purple_xmlnode_insert_child(message, child);
1095 } else {
1096 purple_debug_error("jabber",
1097 "XHTML translation/validation failed, returning: %s\n",
1098 jm->xhtml);
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
1110 * FALSE.
1112 static gboolean
1113 jabber_xhtml_plain_equal(const char *xhtml_escaped,
1114 const char *plain)
1116 int i = 0;
1117 int j = 0;
1118 gboolean ret;
1119 char *xhtml = purple_unescape_html(xhtml_escaped);
1121 while (xhtml[i] && plain[j]) {
1122 if (xhtml[i] == plain[j]) {
1123 i += 1;
1124 j += 1;
1125 continue;
1128 if (plain[j] == '\n' && !strncmp(xhtml+i, "<br/>", 5)) {
1129 i += 5;
1130 j += 1;
1131 continue;
1134 g_free(xhtml);
1135 return FALSE;
1138 /* Are we at the end of both strings? */
1139 ret = (xhtml[i] == plain[j]) && (xhtml[i] == '\0');
1140 g_free(xhtml);
1141 return ret;
1144 int jabber_message_send_im(PurpleConnection *gc, PurpleMessage *msg)
1146 JabberMessage *jm;
1147 JabberBuddy *jb;
1148 JabberBuddyResource *jbr;
1149 char *xhtml;
1150 char *tmp;
1151 char *resource;
1152 const gchar *rcpt = purple_message_get_recipient(msg);
1154 if (!rcpt || purple_message_is_empty(msg))
1155 return 0;
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);
1162 g_free(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);
1171 if(jbr) {
1172 if(jbr->thread_id)
1173 jm->thread_id = jbr->thread_id;
1175 if (jbr->chat_states == JABBER_CHAT_STATES_UNSUPPORTED)
1176 jm->chat_state = JM_STATE_NONE;
1177 else {
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);
1185 g_free(tmp);
1187 tmp = jabber_message_smileyfy_xhtml(jm, xhtml);
1188 if (tmp) {
1189 g_free(xhtml);
1190 xhtml = tmp;
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);
1204 g_free(xhtml);
1206 jabber_message_send(jm);
1207 jabber_message_free(jm);
1208 return 1;
1211 int jabber_message_send_chat(PurpleConnection *gc, int id, PurpleMessage *msg)
1213 JabberChat *chat;
1214 JabberMessage *jm;
1215 JabberStream *js;
1216 char *xhtml;
1217 char *tmp;
1219 if (!gc || purple_message_is_empty(msg))
1220 return 0;
1222 js = purple_connection_get_protocol_data(gc);
1223 chat = jabber_chat_find_by_id(js, id);
1225 if(!chat)
1226 return 0;
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);
1236 g_free(tmp);
1237 tmp = jabber_message_smileyfy_xhtml(jm, xhtml);
1238 if (tmp) {
1239 g_free(xhtml);
1240 xhtml = tmp;
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);
1247 g_free(xhtml);
1249 jabber_message_send(jm);
1250 jabber_message_free(jm);
1252 return 1;
1255 unsigned int jabber_send_typing(PurpleConnection *gc, const char *who, PurpleIMTypingState state)
1257 JabberStream *js;
1258 JabberMessage *jm;
1259 JabberBuddy *jb;
1260 JabberBuddyResource *jbr;
1261 char *resource;
1263 js = purple_connection_get_protocol_data(gc);
1264 jb = jabber_buddy_find(js, who, TRUE);
1265 if (!jb)
1266 return 0;
1268 resource = jabber_get_resource(who);
1269 jbr = jabber_buddy_find_resource(jb, resource);
1270 g_free(resource);
1272 /* We know this entity doesn't support chat states */
1273 if (jbr && jbr->chat_states == JABBER_CHAT_STATES_UNSUPPORTED)
1274 return 0;
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))
1280 return 0;
1282 /* TODO: figure out threading */
1283 jm = g_new0(JabberMessage, 1);
1284 jm->js = js;
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;
1293 else
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);
1302 return 0;
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);