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
37 #include "useravatar.h"
40 #include "adhoccommands.h"
41 #include "google/google.h"
45 } JabberBuddyInfoResource
;
52 GHashTable
*resources
;
55 PurpleNotifyUserInfo
*user_info
;
61 jabber_buddy_resource_free(JabberBuddyResource
*jbr
)
63 g_return_if_fail(jbr
!= NULL
);
65 jbr
->jb
->resources
= g_list_remove(jbr
->jb
->resources
, jbr
);
67 while(jbr
->commands
) {
68 JabberAdHocCommands
*cmd
= jbr
->commands
->data
;
73 jbr
->commands
= g_list_delete_link(jbr
->commands
, jbr
->commands
);
76 while (jbr
->caps
.exts
) {
77 g_free(jbr
->caps
.exts
->data
);
78 jbr
->caps
.exts
= g_list_delete_link(jbr
->caps
.exts
, jbr
->caps
.exts
);
83 g_free(jbr
->thread_id
);
84 g_free(jbr
->client
.name
);
85 g_free(jbr
->client
.version
);
86 g_free(jbr
->client
.os
);
90 void jabber_buddy_free(JabberBuddy
*jb
)
92 g_return_if_fail(jb
!= NULL
);
94 g_free(jb
->error_msg
);
96 jabber_buddy_resource_free(jb
->resources
->data
);
101 JabberBuddy
*jabber_buddy_find(JabberStream
*js
, const char *name
,
107 if (js
->buddies
== NULL
)
110 if(!(realname
= jabber_get_bare_jid(name
)))
113 jb
= g_hash_table_lookup(js
->buddies
, realname
);
116 jb
= g_new0(JabberBuddy
, 1);
117 g_hash_table_insert(js
->buddies
, realname
, jb
);
124 /* Returns -1 if a is a higher priority resource than b, or is
125 * "more available" than b. 0 if they're the same, and 1 if b is
126 * higher priority/more available than a.
128 static gint
resource_compare_cb(gconstpointer a
, gconstpointer b
)
130 const JabberBuddyResource
*jbra
= a
;
131 const JabberBuddyResource
*jbrb
= b
;
132 JabberBuddyState state_a
, state_b
;
134 if (jbra
->priority
!= jbrb
->priority
)
135 return jbra
->priority
> jbrb
->priority
? -1 : 1;
137 /* Fold the states for easier comparison */
138 /* TODO: Differentiate online/chat and away/dnd? */
139 switch (jbra
->state
) {
140 case JABBER_BUDDY_STATE_ONLINE
:
141 case JABBER_BUDDY_STATE_CHAT
:
142 state_a
= JABBER_BUDDY_STATE_ONLINE
;
144 case JABBER_BUDDY_STATE_AWAY
:
145 case JABBER_BUDDY_STATE_DND
:
146 state_a
= JABBER_BUDDY_STATE_AWAY
;
148 case JABBER_BUDDY_STATE_XA
:
149 state_a
= JABBER_BUDDY_STATE_XA
;
151 case JABBER_BUDDY_STATE_UNAVAILABLE
:
152 state_a
= JABBER_BUDDY_STATE_UNAVAILABLE
;
155 state_a
= JABBER_BUDDY_STATE_UNKNOWN
;
159 switch (jbrb
->state
) {
160 case JABBER_BUDDY_STATE_ONLINE
:
161 case JABBER_BUDDY_STATE_CHAT
:
162 state_b
= JABBER_BUDDY_STATE_ONLINE
;
164 case JABBER_BUDDY_STATE_AWAY
:
165 case JABBER_BUDDY_STATE_DND
:
166 state_b
= JABBER_BUDDY_STATE_AWAY
;
168 case JABBER_BUDDY_STATE_XA
:
169 state_b
= JABBER_BUDDY_STATE_XA
;
171 case JABBER_BUDDY_STATE_UNAVAILABLE
:
172 state_b
= JABBER_BUDDY_STATE_UNAVAILABLE
;
175 state_b
= JABBER_BUDDY_STATE_UNKNOWN
;
179 if (state_a
== state_b
) {
180 if (jbra
->idle
== jbrb
->idle
)
182 else if ((jbra
->idle
&& !jbrb
->idle
) ||
183 (jbra
->idle
&& jbrb
->idle
&& jbra
->idle
< jbrb
->idle
))
189 if (state_a
== JABBER_BUDDY_STATE_ONLINE
)
191 else if (state_a
== JABBER_BUDDY_STATE_AWAY
&&
192 (state_b
== JABBER_BUDDY_STATE_XA
||
193 state_b
== JABBER_BUDDY_STATE_UNAVAILABLE
||
194 state_b
== JABBER_BUDDY_STATE_UNKNOWN
))
196 else if (state_a
== JABBER_BUDDY_STATE_XA
&&
197 (state_b
== JABBER_BUDDY_STATE_UNAVAILABLE
||
198 state_b
== JABBER_BUDDY_STATE_UNKNOWN
))
200 else if (state_a
== JABBER_BUDDY_STATE_UNAVAILABLE
&&
201 state_b
== JABBER_BUDDY_STATE_UNKNOWN
)
207 JabberBuddyResource
*jabber_buddy_find_resource(JabberBuddy
*jb
,
208 const char *resource
)
215 if (resource
== NULL
)
216 return jb
->resources
? jb
->resources
->data
: NULL
;
218 for (l
= jb
->resources
; l
; l
= l
->next
)
220 JabberBuddyResource
*jbr
= l
->data
;
221 if (jbr
->name
&& purple_strequal(resource
, jbr
->name
))
228 JabberBuddyResource
*jabber_buddy_track_resource(JabberBuddy
*jb
, const char *resource
,
229 int priority
, JabberBuddyState state
, const char *status
)
231 /* TODO: Optimization: Only reinsert if priority+state changed */
232 JabberBuddyResource
*jbr
= jabber_buddy_find_resource(jb
, resource
);
234 jb
->resources
= g_list_remove(jb
->resources
, jbr
);
236 jbr
= g_new0(JabberBuddyResource
, 1);
238 jbr
->name
= g_strdup(resource
);
239 jbr
->capabilities
= JABBER_CAP_NONE
;
240 jbr
->tz_off
= PURPLE_NO_TZ_OFF
;
242 jbr
->priority
= priority
;
245 jbr
->status
= g_strdup(status
);
247 jb
->resources
= g_list_insert_sorted(jb
->resources
, jbr
,
248 resource_compare_cb
);
252 void jabber_buddy_remove_resource(JabberBuddy
*jb
, const char *resource
)
254 JabberBuddyResource
*jbr
= jabber_buddy_find_resource(jb
, resource
);
259 jabber_buddy_resource_free(jbr
);
263 * This is the old vCard stuff taken from the old prpl. vCards, by definition
264 * are a temporary thing until jabber can get its act together and come up
265 * with a format for user information, hence the namespace of 'vcard-temp'
267 * Since I don't feel like putting that much work into something that's
268 * _supposed_ to go away, i'm going to just copy the kludgy old code here,
269 * and make it purdy when jabber comes up with a standards-track JEP to
274 /*---------------------------------------*/
275 /* Jabber "set info" (vCard) support */
276 /*---------------------------------------*/
281 * <vCard prodid='' version='' xmlns=''>
311 * http://docs.jabber.org/proto/html/vcard-temp.html
312 * http://www.vcard-xml.org/dtd/vCard-XML-v2-20010520.dtd
316 * Cross-reference user-friendly V-Card entry labels to vCard XML tags
319 * Order is (or should be) unimportant. For example: we have no way of
320 * knowing in what order real data will arrive.
322 * Format: Label, Pre-set text, "visible" flag, "editable" flag, XML tag
323 * name, XML tag's parent tag "path" (relative to vCard node).
325 * List is terminated by a NULL label pointer.
327 * Entries with no label text, but with XML tag and parent tag
328 * entries, are used by V-Card XML construction routines to
329 * "automagically" construct the appropriate XML node tree.
331 * Thoughts on future direction/expansion
333 * This is a "simple" vCard.
335 * It is possible for nodes other than the "vCard" node to have
336 * attributes. Should that prove necessary/desirable, add an
337 * "attributes" pointer to the vcard_template struct, create the
338 * necessary tag_attr structs, and add 'em to the vcard_dflt_data
341 * The above changes will (obviously) require changes to the vCard
342 * construction routines.
345 struct vcard_template
{
346 char *label
; /* label text pointer */
347 char *tag
; /* tag text */
348 char *ptag
; /* parent tag "path" text */
349 } const vcard_template_data
[] = {
350 {N_("Full Name"), "FN", NULL
},
351 {N_("Family Name"), "FAMILY", "N"},
352 {N_("Given Name"), "GIVEN", "N"},
353 {N_("Nickname"), "NICKNAME", NULL
},
354 {N_("URL"), "URL", NULL
},
355 {N_("Street Address"), "STREET", "ADR"},
356 {N_("Extended Address"), "EXTADD", "ADR"},
357 {N_("Locality"), "LOCALITY", "ADR"},
358 {N_("Region"), "REGION", "ADR"},
359 {N_("Postal Code"), "PCODE", "ADR"},
360 {N_("Country"), "CTRY", "ADR"},
361 {N_("Telephone"), "NUMBER", "TEL"},
362 {N_("Email"), "USERID", "EMAIL"},
363 {N_("Organization Name"), "ORGNAME", "ORG"},
364 {N_("Organization Unit"), "ORGUNIT", "ORG"},
365 {N_("Job Title"), "TITLE", NULL
},
366 {N_("Role"), "ROLE", NULL
},
367 {N_("Birthday"), "BDAY", NULL
},
368 {N_("Description"), "DESC", NULL
},
376 * The "vCard" tag's attribute list...
381 } const vcard_tag_attr_list
[] = {
382 {"prodid", "-//HandGen//NONSGML vGen v1.0//EN"},
383 {"version", "2.0", },
384 {"xmlns", "vcard-temp", },
390 * Insert a tag node into an xmlnode tree, recursively inserting parent tag
393 * Returns pointer to inserted node
395 * Note to hackers: this code is designed to be re-entrant (it's recursive--it
396 * calls itself), so don't put any "static"s in here!
398 static xmlnode
*insert_tag_to_parent_tag(xmlnode
*start
, const char *parent_tag
, const char *new_tag
)
403 * If the parent tag wasn't specified, see if we can get it
404 * from the vCard template struct.
406 if(parent_tag
== NULL
) {
407 const struct vcard_template
*vc_tp
= vcard_template_data
;
409 while(vc_tp
->label
!= NULL
) {
410 if(purple_strequal(vc_tp
->tag
, new_tag
)) {
411 parent_tag
= vc_tp
->ptag
;
419 * If we have a parent tag...
421 if(parent_tag
!= NULL
) {
423 * Try to get the parent node for a tag
425 if((x
= xmlnode_get_child(start
, parent_tag
)) == NULL
) {
429 char *grand_parent
= g_strdup(parent_tag
);
432 if((parent
= strrchr(grand_parent
, '/')) != NULL
) {
434 x
= insert_tag_to_parent_tag(start
, grand_parent
, parent
);
436 x
= xmlnode_new_child(start
, grand_parent
);
438 g_free(grand_parent
);
441 * We found *something* to be the parent node.
442 * Note: may be the "root" node!
445 if((y
= xmlnode_get_child(x
, new_tag
)) != NULL
) {
452 * insert the new tag into its parent node
454 return(xmlnode_new_child((x
== NULL
? start
: x
), new_tag
));
458 * Send vCard info to Jabber server
460 void jabber_set_info(PurpleConnection
*gc
, const char *info
)
462 PurpleStoredImage
*img
;
464 JabberStream
*js
= purple_connection_get_protocol_data(gc
);
466 const struct tag_attr
*tag_attr
;
468 /* if we have't grabbed the remote vcard yet, we can't
469 * assume that what we have here is correct */
470 if(!js
->vcard_fetched
)
473 if (js
->vcard_timer
) {
474 purple_timeout_remove(js
->vcard_timer
);
478 g_free(js
->avatar_hash
);
479 js
->avatar_hash
= NULL
;
482 * Send only if there's actually any *information* to send
484 vc_node
= info
? xmlnode_from_str(info
, -1) : NULL
;
486 if (vc_node
&& (!vc_node
->name
||
487 g_ascii_strncasecmp(vc_node
->name
, "vCard", 5))) {
488 xmlnode_free(vc_node
);
492 if ((img
= purple_buddy_icons_find_account_icon(gc
->account
))) {
493 gconstpointer avatar_data
;
495 xmlnode
*photo
, *binval
, *type
;
499 vc_node
= xmlnode_new("vCard");
500 for(tag_attr
= vcard_tag_attr_list
; tag_attr
->attr
!= NULL
; ++tag_attr
)
501 xmlnode_set_attrib(vc_node
, tag_attr
->attr
, tag_attr
->value
);
504 avatar_data
= purple_imgstore_get_data(img
);
505 avatar_len
= purple_imgstore_get_size(img
);
506 /* Get rid of an old PHOTO if one exists.
507 * TODO: This may want to be modified to remove all old PHOTO
508 * children, at the moment some people have managed to get
509 * multiple PHOTO entries in their vCard. */
510 if((photo
= xmlnode_get_child(vc_node
, "PHOTO"))) {
513 photo
= xmlnode_new_child(vc_node
, "PHOTO");
514 type
= xmlnode_new_child(photo
, "TYPE");
515 xmlnode_insert_data(type
, "image/png", -1);
516 binval
= xmlnode_new_child(photo
, "BINVAL");
517 enc
= purple_base64_encode(avatar_data
, avatar_len
);
520 jabber_calculate_data_hash(avatar_data
, avatar_len
, "sha1");
522 xmlnode_insert_data(binval
, enc
, -1);
524 purple_imgstore_unref(img
);
525 } else if (vc_node
) {
527 /* TODO: Remove all PHOTO children? (see above note) */
528 if ((photo
= xmlnode_get_child(vc_node
, "PHOTO"))) {
533 if (vc_node
!= NULL
) {
534 iq
= jabber_iq_new(js
, JABBER_IQ_SET
);
535 xmlnode_insert_child(iq
->node
, vc_node
);
538 /* Send presence to update vcard-temp:x:update */
539 jabber_presence_send(js
, FALSE
);
543 void jabber_set_buddy_icon(PurpleConnection
*gc
, PurpleStoredImage
*img
)
545 PurpleAccount
*account
= purple_connection_get_account(gc
);
547 /* Publish the avatar as specified in XEP-0084 */
548 jabber_avatar_set(gc
->proto_data
, img
);
549 /* Set the image in our vCard */
550 jabber_set_info(gc
, purple_account_get_user_info(account
));
552 /* TODO: Fake image to ourselves, since a number of servers do not echo
553 * back our presence to us. To do this without uselessly copying the data
554 * of the image, we need purple_buddy_icons_set_for_user_image (i.e. takes
555 * an existing icon/stored image). */
559 * This is the callback from the "ok clicked" for "set vCard"
561 * Sets the vCard with data from PurpleRequestFields.
564 jabber_format_info(PurpleConnection
*gc
, PurpleRequestFields
*fields
)
567 PurpleRequestField
*field
;
570 const struct vcard_template
*vc_tp
;
571 const struct tag_attr
*tag_attr
;
573 vc_node
= xmlnode_new("vCard");
575 for(tag_attr
= vcard_tag_attr_list
; tag_attr
->attr
!= NULL
; ++tag_attr
)
576 xmlnode_set_attrib(vc_node
, tag_attr
->attr
, tag_attr
->value
);
578 for (vc_tp
= vcard_template_data
; vc_tp
->label
!= NULL
; vc_tp
++) {
579 if (*vc_tp
->label
== '\0')
582 field
= purple_request_fields_get_field(fields
, vc_tp
->tag
);
583 text
= purple_request_field_string_get_value(field
);
586 if (text
!= NULL
&& *text
!= '\0') {
589 purple_debug_info("jabber", "Setting %s to '%s'\n", vc_tp
->tag
, text
);
591 if ((xp
= insert_tag_to_parent_tag(vc_node
,
592 NULL
, vc_tp
->tag
)) != NULL
) {
594 xmlnode_insert_data(xp
, text
, -1);
599 p
= xmlnode_to_str(vc_node
, NULL
);
600 xmlnode_free(vc_node
);
602 purple_account_set_user_info(purple_connection_get_account(gc
), p
);
603 serv_set_info(gc
, p
);
609 * This gets executed by the proto action
611 * Creates a new PurpleRequestFields struct, gets the XML-formatted user_info
612 * string (if any) into GSLists for the (multi-entry) edit dialog and
613 * calls the set_vcard dialog.
615 void jabber_setup_set_info(PurplePluginAction
*action
)
617 PurpleConnection
*gc
= (PurpleConnection
*) action
->context
;
618 PurpleRequestFields
*fields
;
619 PurpleRequestFieldGroup
*group
;
620 PurpleRequestField
*field
;
621 const struct vcard_template
*vc_tp
;
622 const char *user_info
;
624 xmlnode
*x_vc_data
= NULL
;
626 fields
= purple_request_fields_new();
627 group
= purple_request_field_group_new(NULL
);
628 purple_request_fields_add_group(fields
, group
);
631 * Get existing, XML-formatted, user info
633 if((user_info
= purple_account_get_user_info(gc
->account
)) != NULL
)
634 x_vc_data
= xmlnode_from_str(user_info
, -1);
637 * Set up GSLists for edit with labels from "template," data from user info
639 for(vc_tp
= vcard_template_data
; vc_tp
->label
!= NULL
; ++vc_tp
) {
641 if((vc_tp
->label
)[0] == '\0')
644 if (x_vc_data
!= NULL
) {
645 if(vc_tp
->ptag
== NULL
) {
646 data_node
= xmlnode_get_child(x_vc_data
, vc_tp
->tag
);
648 gchar
*tag
= g_strdup_printf("%s/%s", vc_tp
->ptag
, vc_tp
->tag
);
649 data_node
= xmlnode_get_child(x_vc_data
, tag
);
653 cdata
= xmlnode_get_data(data_node
);
656 if(purple_strequal(vc_tp
->tag
, "DESC")) {
657 field
= purple_request_field_string_new(vc_tp
->tag
,
658 _(vc_tp
->label
), cdata
,
661 field
= purple_request_field_string_new(vc_tp
->tag
,
662 _(vc_tp
->label
), cdata
,
669 purple_request_field_group_add_field(group
, field
);
672 if(x_vc_data
!= NULL
)
673 xmlnode_free(x_vc_data
);
675 purple_request_fields(gc
, _("Edit XMPP vCard"),
676 _("Edit XMPP vCard"),
677 _("All items below are optional. Enter only the "
678 "information with which you feel comfortable."),
680 _("Save"), G_CALLBACK(jabber_format_info
),
682 purple_connection_get_account(gc
), NULL
, NULL
,
686 /*---------------------------------------*/
687 /* End Jabber "set info" (vCard) support */
688 /*---------------------------------------*/
691 * end of that ancient crap that needs to die
694 static void jabber_buddy_info_destroy(JabberBuddyInfo
*jbi
)
696 /* Remove the timeout, which would otherwise trigger jabber_buddy_get_info_timeout() */
697 if (jbi
->timeout_handle
> 0)
698 purple_timeout_remove(jbi
->timeout_handle
);
701 g_hash_table_destroy(jbi
->resources
);
702 g_free(jbi
->last_message
);
703 purple_notify_user_info_destroy(jbi
->user_info
);
708 add_jbr_info(JabberBuddyInfo
*jbi
, const char *resource
,
709 JabberBuddyResource
*jbr
)
711 JabberBuddyInfoResource
*jbir
;
712 PurpleNotifyUserInfo
*user_info
;
714 jbir
= g_hash_table_lookup(jbi
->resources
, resource
);
715 user_info
= jbi
->user_info
;
717 if (jbr
&& jbr
->client
.name
) {
719 g_strdup_printf("%s%s%s", jbr
->client
.name
,
720 (jbr
->client
.version
? " " : ""),
721 (jbr
->client
.version
? jbr
->client
.version
: ""));
722 purple_notify_user_info_prepend_pair(user_info
, _("Client"), tmp
);
726 purple_notify_user_info_prepend_pair(user_info
, _("Operating System"), jbr
->client
.os
);
729 if (jbr
&& jbr
->tz_off
!= PURPLE_NO_TZ_OFF
) {
734 now_t
+= jbr
->tz_off
;
735 now
= gmtime(&now_t
);
738 g_strdup_printf("%s %c%02d%02d", purple_time_format(now
),
739 jbr
->tz_off
< 0 ? '-' : '+',
740 abs(jbr
->tz_off
/ (60*60)),
741 abs((jbr
->tz_off
% (60*60)) / 60));
742 purple_notify_user_info_prepend_pair(user_info
, _("Local Time"), timestamp
);
746 if (jbir
&& jbir
->idle_seconds
> 0) {
747 char *idle
= purple_str_seconds_to_string(jbir
->idle_seconds
);
748 purple_notify_user_info_prepend_pair(user_info
, _("Idle"), idle
);
756 const char *status_name
= jabber_buddy_state_get_name(jbr
->state
);
759 tmp
= purple_markup_escape_text(jbr
->status
, -1);
760 purdy
= purple_strdup_withhtml(tmp
);
763 if (purple_strequal(status_name
, purdy
))
767 tmp
= g_strdup_printf("%s%s%s", (status_name
? status_name
: ""),
768 ((status_name
&& purdy
) ? ": " : ""),
769 (purdy
? purdy
: ""));
770 purple_notify_user_info_prepend_pair(user_info
, _("Status"), tmp
);
772 g_snprintf(priority
, sizeof(priority
), "%d", jbr
->priority
);
773 purple_notify_user_info_prepend_pair(user_info
, _("Priority"), priority
);
778 purple_notify_user_info_prepend_pair(user_info
, _("Status"), _("Unknown"));
782 static void jabber_buddy_info_show_if_ready(JabberBuddyInfo
*jbi
)
785 JabberBuddyResource
*jbr
;
787 PurpleNotifyUserInfo
*user_info
;
793 user_info
= jbi
->user_info
;
794 resource_name
= jabber_get_resource(jbi
->jid
);
796 /* If we have one or more pairs from the vcard, put a section break above it */
797 if (purple_notify_user_info_get_entries(user_info
))
798 purple_notify_user_info_prepend_section_break(user_info
);
800 /* Add the information about the user's resource(s) */
802 jbr
= jabber_buddy_find_resource(jbi
->jb
, resource_name
);
803 add_jbr_info(jbi
, resource_name
, jbr
);
805 /* TODO: This is in priority-ascending order (lowest prio first), because
806 * everything is prepended. Is that ok? */
807 for (resources
= jbi
->jb
->resources
; resources
; resources
= resources
->next
) {
808 jbr
= resources
->data
;
810 /* put a section break between resources, this is not needed if
811 we are at the first, because one was already added for the vcard
813 if (resources
!= jbi
->jb
->resources
)
814 purple_notify_user_info_prepend_section_break(user_info
);
816 add_jbr_info(jbi
, jbr
->name
, jbr
);
819 purple_notify_user_info_prepend_pair(user_info
, _("Resource"), jbr
->name
);
823 if (!jbi
->jb
->resources
) {
824 /* the buddy is offline */
825 gboolean is_domain
= jabber_jid_is_domain(jbi
->jid
);
827 if (jbi
->last_seconds
> 0) {
828 char *last
= purple_str_seconds_to_string(jbi
->last_seconds
);
829 gchar
*message
= NULL
;
830 const gchar
*title
= NULL
;
836 title
= _("Logged Off");
837 message
= g_strdup_printf(_("%s ago"), last
);
839 purple_notify_user_info_prepend_pair(user_info
, title
, message
);
846 g_strdup_printf("%s%s%s", _("Offline"),
847 jbi
->last_message
? ": " : "",
848 jbi
->last_message
? jbi
->last_message
: "");
849 purple_notify_user_info_prepend_pair(user_info
, _("Status"), status
);
854 g_free(resource_name
);
856 purple_notify_userinfo(jbi
->js
->gc
, jbi
->jid
, user_info
, NULL
, NULL
);
858 while (jbi
->vcard_imgids
) {
859 purple_imgstore_unref_by_id(GPOINTER_TO_INT(jbi
->vcard_imgids
->data
));
860 jbi
->vcard_imgids
= g_slist_delete_link(jbi
->vcard_imgids
, jbi
->vcard_imgids
);
863 jbi
->js
->pending_buddy_info_requests
= g_slist_remove(jbi
->js
->pending_buddy_info_requests
, jbi
);
865 jabber_buddy_info_destroy(jbi
);
868 static void jabber_buddy_info_remove_id(JabberBuddyInfo
*jbi
, const char *id
)
870 GSList
*l
= jbi
->ids
;
878 if(purple_strequal(id
, comp_id
)) {
879 jbi
->ids
= g_slist_remove(jbi
->ids
, comp_id
);
888 set_own_vcard_cb(gpointer data
)
890 JabberStream
*js
= data
;
891 PurpleAccount
*account
= purple_connection_get_account(js
->gc
);
895 jabber_set_info(js
->gc
, purple_account_get_user_info(account
));
900 static void jabber_vcard_save_mine(JabberStream
*js
, const char *from
,
901 JabberIqType type
, const char *id
,
902 xmlnode
*packet
, gpointer data
)
904 xmlnode
*vcard
, *photo
, *binval
;
905 char *txt
, *vcard_hash
= NULL
;
906 PurpleAccount
*account
;
908 if (type
== JABBER_IQ_ERROR
) {
910 purple_debug_warning("jabber", "Server returned error while retrieving vCard\n");
912 error
= xmlnode_get_child(packet
, "error");
913 if (!error
|| !xmlnode_get_child(error
, "item-not-found"))
917 account
= purple_connection_get_account(js
->gc
);
919 if((vcard
= xmlnode_get_child(packet
, "vCard")) ||
920 (vcard
= xmlnode_get_child_with_namespace(packet
, "query", "vcard-temp")))
922 txt
= xmlnode_to_str(vcard
, NULL
);
923 purple_account_set_user_info(account
, txt
);
926 /* if we have no vCard, then lets not overwrite what we might have locally */
929 js
->vcard_fetched
= TRUE
;
931 if (vcard
&& (photo
= xmlnode_get_child(vcard
, "PHOTO")) &&
932 (binval
= xmlnode_get_child(photo
, "BINVAL"))) {
934 char *bintext
= xmlnode_get_data(binval
);
936 guchar
*data
= purple_base64_decode(bintext
, &size
);
940 vcard_hash
= jabber_calculate_data_hash(data
, size
, "sha1");
946 /* Republish our vcard if the photo is different than the server's */
947 if (js
->initial_avatar_hash
&& !purple_strequal(vcard_hash
, js
->initial_avatar_hash
)) {
949 * Google Talk has developed the behavior that it will not accept
950 * a vcard set in the first 10 seconds (or so) of the connection;
951 * it returns an error (namespaces trimmed):
952 * <error code="500" type="wait"><internal-server-error/></error>.
955 js
->vcard_timer
= purple_timeout_add_seconds(10, set_own_vcard_cb
,
958 jabber_set_info(js
->gc
, purple_account_get_user_info(account
));
959 } else if (vcard_hash
) {
960 /* A photo is in the vCard. Advertise its hash */
961 js
->avatar_hash
= vcard_hash
;
964 /* Send presence to update vcard-temp:x:update */
965 jabber_presence_send(js
, FALSE
);
971 void jabber_vcard_fetch_mine(JabberStream
*js
)
973 JabberIq
*iq
= jabber_iq_new(js
, JABBER_IQ_GET
);
975 xmlnode
*vcard
= xmlnode_new_child(iq
->node
, "vCard");
976 xmlnode_set_namespace(vcard
, "vcard-temp");
977 jabber_iq_set_callback(iq
, jabber_vcard_save_mine
, NULL
);
982 static void jabber_vcard_parse(JabberStream
*js
, const char *from
,
983 JabberIqType type
, const char *id
,
984 xmlnode
*packet
, gpointer data
)
988 char *serverside_alias
= NULL
;
990 PurpleAccount
*account
;
991 JabberBuddyInfo
*jbi
= data
;
992 PurpleNotifyUserInfo
*user_info
;
994 g_return_if_fail(jbi
!= NULL
);
996 jabber_buddy_info_remove_id(jbi
, id
);
998 if (type
== JABBER_IQ_ERROR
) {
999 purple_debug_info("jabber", "Got error response for vCard\n");
1000 jabber_buddy_info_show_if_ready(jbi
);
1004 user_info
= jbi
->user_info
;
1005 account
= purple_connection_get_account(js
->gc
);
1006 bare_jid
= jabber_get_bare_jid(from
? from
: purple_account_get_username(account
));
1008 /* TODO: Is the query xmlns='vcard-temp' version of this still necessary? */
1009 if((vcard
= xmlnode_get_child(packet
, "vCard")) ||
1010 (vcard
= xmlnode_get_child_with_namespace(packet
, "query", "vcard-temp"))) {
1012 for(child
= vcard
->child
; child
; child
= child
->next
)
1016 if(child
->type
!= XMLNODE_TYPE_TAG
)
1019 text
= xmlnode_get_data(child
);
1020 if(text
&& purple_strequal(child
->name
, "FN")) {
1021 if (!serverside_alias
)
1022 serverside_alias
= g_strdup(text
);
1024 purple_notify_user_info_add_pair_plaintext(user_info
, _("Full Name"), text
);
1025 } else if(purple_strequal(child
->name
, "N")) {
1026 for(child2
= child
->child
; child2
; child2
= child2
->next
)
1030 if(child2
->type
!= XMLNODE_TYPE_TAG
)
1033 text2
= xmlnode_get_data(child2
);
1034 if(text2
&& purple_strequal(child2
->name
, "FAMILY")) {
1035 purple_notify_user_info_add_pair_plaintext(user_info
, _("Family Name"), text2
);
1036 } else if(text2
&& purple_strequal(child2
->name
, "GIVEN")) {
1037 purple_notify_user_info_add_pair_plaintext(user_info
, _("Given Name"), text2
);
1038 } else if(text2
&& purple_strequal(child2
->name
, "MIDDLE")) {
1039 purple_notify_user_info_add_pair_plaintext(user_info
, _("Middle Name"), text2
);
1043 } else if(text
&& purple_strequal(child
->name
, "NICKNAME")) {
1044 /* Prefer the Nickcname to the Full Name as the serverside alias if it's not just part of the jid.
1045 * Ignore it if it's part of the jid. */
1046 if (strstr(bare_jid
, text
) == NULL
) {
1047 g_free(serverside_alias
);
1048 serverside_alias
= g_strdup(text
);
1050 purple_notify_user_info_add_pair_plaintext(user_info
, _("Nickname"), text
);
1052 } else if(text
&& purple_strequal(child
->name
, "BDAY")) {
1053 purple_notify_user_info_add_pair_plaintext(user_info
, _("Birthday"), text
);
1054 } else if(purple_strequal(child
->name
, "ADR")) {
1055 gboolean address_line_added
= FALSE
;
1057 for(child2
= child
->child
; child2
; child2
= child2
->next
)
1061 if(child2
->type
!= XMLNODE_TYPE_TAG
)
1064 text2
= xmlnode_get_data(child2
);
1068 /* We do this here so that it's not added if all the child
1069 * elements are empty. */
1070 if (!address_line_added
)
1072 purple_notify_user_info_add_section_header(user_info
, _("Address"));
1073 address_line_added
= TRUE
;
1076 if(purple_strequal(child2
->name
, "POBOX")) {
1077 purple_notify_user_info_add_pair_plaintext(user_info
, _("P.O. Box"), text2
);
1078 } else if (purple_strequal(child2
->name
, "EXTADD") || purple_strequal(child2
->name
, "EXTADR")) {
1080 * EXTADD is correct, EXTADR is generated by other
1081 * clients. The next time someone reads this, remove
1084 purple_notify_user_info_add_pair_plaintext(user_info
, _("Extended Address"), text2
);
1085 } else if(purple_strequal(child2
->name
, "STREET")) {
1086 purple_notify_user_info_add_pair_plaintext(user_info
, _("Street Address"), text2
);
1087 } else if(purple_strequal(child2
->name
, "LOCALITY")) {
1088 purple_notify_user_info_add_pair_plaintext(user_info
, _("Locality"), text2
);
1089 } else if(purple_strequal(child2
->name
, "REGION")) {
1090 purple_notify_user_info_add_pair_plaintext(user_info
, _("Region"), text2
);
1091 } else if(purple_strequal(child2
->name
, "PCODE")) {
1092 purple_notify_user_info_add_pair_plaintext(user_info
, _("Postal Code"), text2
);
1093 } else if(purple_strequal(child2
->name
, "CTRY")
1094 || purple_strequal(child2
->name
, "COUNTRY")) {
1095 purple_notify_user_info_add_pair_plaintext(user_info
, _("Country"), text2
);
1100 if (address_line_added
)
1101 purple_notify_user_info_add_section_break(user_info
);
1103 } else if(purple_strequal(child
->name
, "TEL")) {
1105 if((child2
= xmlnode_get_child(child
, "NUMBER"))) {
1106 /* show what kind of number it is */
1107 number
= xmlnode_get_data(child2
);
1109 purple_notify_user_info_add_pair_plaintext(user_info
, _("Telephone"), number
);
1112 } else if((number
= xmlnode_get_data(child
))) {
1113 /* lots of clients (including purple) do this, but it's
1115 purple_notify_user_info_add_pair_plaintext(user_info
, _("Telephone"), number
);
1118 } else if(purple_strequal(child
->name
, "EMAIL")) {
1119 char *userid
, *escaped
;
1120 if((child2
= xmlnode_get_child(child
, "USERID"))) {
1121 /* show what kind of email it is */
1122 userid
= xmlnode_get_data(child2
);
1125 escaped
= g_markup_escape_text(userid
, -1);
1126 mailto
= g_strdup_printf("<a href=\"mailto:%s\">%s</a>", escaped
, escaped
);
1127 purple_notify_user_info_add_pair(user_info
, _("Email"), mailto
);
1133 } else if((userid
= xmlnode_get_data(child
))) {
1134 /* lots of clients (including purple) do this, but it's
1138 escaped
= g_markup_escape_text(userid
, -1);
1139 mailto
= g_strdup_printf("<a href=\"mailto:%s\">%s</a>", escaped
, escaped
);
1140 purple_notify_user_info_add_pair(user_info
, _("Email"), mailto
);
1146 } else if(purple_strequal(child
->name
, "ORG")) {
1147 for(child2
= child
->child
; child2
; child2
= child2
->next
)
1151 if(child2
->type
!= XMLNODE_TYPE_TAG
)
1154 text2
= xmlnode_get_data(child2
);
1155 if(text2
&& purple_strequal(child2
->name
, "ORGNAME")) {
1156 purple_notify_user_info_add_pair_plaintext(user_info
, _("Organization Name"), text2
);
1157 } else if(text2
&& purple_strequal(child2
->name
, "ORGUNIT")) {
1158 purple_notify_user_info_add_pair_plaintext(user_info
, _("Organization Unit"), text2
);
1162 } else if(text
&& purple_strequal(child
->name
, "TITLE")) {
1163 purple_notify_user_info_add_pair_plaintext(user_info
, _("Job Title"), text
);
1164 } else if(text
&& purple_strequal(child
->name
, "ROLE")) {
1165 purple_notify_user_info_add_pair_plaintext(user_info
, _("Role"), text
);
1166 } else if(text
&& purple_strequal(child
->name
, "DESC")) {
1167 purple_notify_user_info_add_pair_plaintext(user_info
, _("Description"), text
);
1168 } else if(purple_strequal(child
->name
, "PHOTO") ||
1169 purple_strequal(child
->name
, "LOGO")) {
1170 char *bintext
= NULL
;
1173 if ((binval
= xmlnode_get_child(child
, "BINVAL")) &&
1174 (bintext
= xmlnode_get_data(binval
))) {
1177 gboolean photo
= purple_strequal(child
->name
, "PHOTO");
1179 data
= purple_base64_decode(bintext
, &size
);
1184 jbi
->vcard_imgids
= g_slist_prepend(jbi
->vcard_imgids
, GINT_TO_POINTER(purple_imgstore_add_with_id(g_memdup(data
, size
), size
, "logo.png")));
1185 img_text
= g_strdup_printf("<img id='%d'>", GPOINTER_TO_INT(jbi
->vcard_imgids
->data
));
1187 purple_notify_user_info_add_pair(user_info
, (photo
? _("Photo") : _("Logo")), img_text
);
1189 hash
= jabber_calculate_data_hash(data
, size
, "sha1");
1190 purple_buddy_icons_set_for_user(account
, bare_jid
, data
, size
, hash
);
1201 if (serverside_alias
) {
1203 /* If we found a serverside alias, set it and tell the core */
1204 serv_got_alias(js
->gc
, bare_jid
, serverside_alias
);
1205 b
= purple_find_buddy(account
, bare_jid
);
1207 purple_blist_node_set_string((PurpleBlistNode
*)b
, "servernick", serverside_alias
);
1210 g_free(serverside_alias
);
1215 jabber_buddy_info_show_if_ready(jbi
);
1218 static void jabber_buddy_info_resource_free(gpointer data
)
1220 JabberBuddyInfoResource
*jbri
= data
;
1224 static guint
jbir_hash(gconstpointer v
)
1227 return g_str_hash(v
);
1232 static gboolean
jbir_equal(gconstpointer v1
, gconstpointer v2
)
1234 const gchar
*resource_1
= v1
;
1235 const gchar
*resource_2
= v2
;
1237 return purple_strequal(resource_1
, resource_2
);
1240 static void jabber_version_parse(JabberStream
*js
, const char *from
,
1241 JabberIqType type
, const char *id
,
1242 xmlnode
*packet
, gpointer data
)
1244 JabberBuddyInfo
*jbi
= data
;
1246 char *resource_name
;
1248 g_return_if_fail(jbi
!= NULL
);
1250 jabber_buddy_info_remove_id(jbi
, id
);
1255 resource_name
= jabber_get_resource(from
);
1258 if (type
== JABBER_IQ_RESULT
) {
1259 if((query
= xmlnode_get_child(packet
, "query"))) {
1260 JabberBuddyResource
*jbr
= jabber_buddy_find_resource(jbi
->jb
, resource_name
);
1263 if((node
= xmlnode_get_child(query
, "name"))) {
1264 jbr
->client
.name
= xmlnode_get_data(node
);
1266 if((node
= xmlnode_get_child(query
, "version"))) {
1267 jbr
->client
.version
= xmlnode_get_data(node
);
1269 if((node
= xmlnode_get_child(query
, "os"))) {
1270 jbr
->client
.os
= xmlnode_get_data(node
);
1275 g_free(resource_name
);
1278 jabber_buddy_info_show_if_ready(jbi
);
1281 static void jabber_last_parse(JabberStream
*js
, const char *from
,
1282 JabberIqType type
, const char *id
,
1283 xmlnode
*packet
, gpointer data
)
1285 JabberBuddyInfo
*jbi
= data
;
1287 char *resource_name
;
1288 const char *seconds
;
1290 g_return_if_fail(jbi
!= NULL
);
1292 jabber_buddy_info_remove_id(jbi
, id
);
1297 resource_name
= jabber_get_resource(from
);
1300 if (type
== JABBER_IQ_RESULT
) {
1301 if((query
= xmlnode_get_child(packet
, "query"))) {
1302 seconds
= xmlnode_get_attrib(query
, "seconds");
1305 long sec
= strtol(seconds
, &end
, 10);
1306 JabberBuddy
*jb
= NULL
;
1307 char *resource
= NULL
;
1308 char *buddy_name
= NULL
;
1309 JabberBuddyResource
*jbr
= NULL
;
1311 if(end
!= seconds
) {
1312 JabberBuddyInfoResource
*jbir
= g_hash_table_lookup(jbi
->resources
, resource_name
);
1314 jbir
->idle_seconds
= sec
;
1317 /* Update the idle time of the buddy resource, if we got it.
1318 This will correct the value when a server doesn't mark
1319 delayed presence and we got the presence when signing on */
1320 jb
= jabber_buddy_find(js
, from
, FALSE
);
1322 resource
= jabber_get_resource(from
);
1323 buddy_name
= jabber_get_bare_jid(from
);
1324 /* if the resource already has an idle time set, we
1325 must have gotten it originally from a presence. In
1326 this case we update it. Otherwise don't update it, to
1327 avoid setting an idle and not getting informed about
1328 the resource getting unidle */
1329 if (resource
&& buddy_name
) {
1330 jbr
= jabber_buddy_find_resource(jb
, resource
);
1334 jbr
->idle
= time(NULL
) - sec
;
1340 jabber_buddy_find_resource(jb
, NULL
)) {
1341 purple_prpl_got_user_idle(js
->gc
->account
,
1342 buddy_name
, jbr
->idle
, jbr
->idle
);
1353 g_free(resource_name
);
1356 jabber_buddy_info_show_if_ready(jbi
);
1359 static void jabber_last_offline_parse(JabberStream
*js
, const char *from
,
1360 JabberIqType type
, const char *id
,
1361 xmlnode
*packet
, gpointer data
)
1363 JabberBuddyInfo
*jbi
= data
;
1365 const char *seconds
;
1367 g_return_if_fail(jbi
!= NULL
);
1369 jabber_buddy_info_remove_id(jbi
, id
);
1371 if (type
== JABBER_IQ_RESULT
) {
1372 if((query
= xmlnode_get_child(packet
, "query"))) {
1373 seconds
= xmlnode_get_attrib(query
, "seconds");
1376 long sec
= strtol(seconds
, &end
, 10);
1377 if(end
!= seconds
) {
1378 jbi
->last_seconds
= sec
;
1381 jbi
->last_message
= xmlnode_get_data(query
);
1385 jabber_buddy_info_show_if_ready(jbi
);
1388 static void jabber_time_parse(JabberStream
*js
, const char *from
,
1389 JabberIqType type
, const char *id
,
1390 xmlnode
*packet
, gpointer data
)
1392 JabberBuddyInfo
*jbi
= data
;
1393 JabberBuddyResource
*jbr
;
1394 char *resource_name
;
1396 g_return_if_fail(jbi
!= NULL
);
1398 jabber_buddy_info_remove_id(jbi
, id
);
1403 resource_name
= jabber_get_resource(from
);
1404 jbr
= resource_name
? jabber_buddy_find_resource(jbi
->jb
, resource_name
) : NULL
;
1405 g_free(resource_name
);
1407 if (type
== JABBER_IQ_RESULT
) {
1408 xmlnode
*time
= xmlnode_get_child(packet
, "time");
1409 xmlnode
*tzo
= time
? xmlnode_get_child(time
, "tzo") : NULL
;
1410 char *tzo_data
= tzo
? xmlnode_get_data(tzo
) : NULL
;
1414 if (tzo_data
[0] == 'Z' && tzo_data
[1] == '\0') {
1417 gboolean offset_positive
= (tzo_data
[0] == '+');
1419 if (((*c
== '+' || *c
== '-') && (c
= c
+ 1)) &&
1420 sscanf(c
, "%02d:%02d", &hours
, &minutes
) == 2) {
1421 jbr
->tz_off
= 60*60*hours
+ 60*minutes
;
1422 if (!offset_positive
)
1425 purple_debug_info("jabber", "Ignoring malformed timezone %s",
1435 jabber_buddy_info_show_if_ready(jbi
);
1438 void jabber_buddy_remove_all_pending_buddy_info_requests(JabberStream
*js
)
1440 if (js
->pending_buddy_info_requests
)
1442 JabberBuddyInfo
*jbi
;
1443 GSList
*l
= js
->pending_buddy_info_requests
;
1447 g_slist_free(jbi
->ids
);
1448 jabber_buddy_info_destroy(jbi
);
1453 g_slist_free(js
->pending_buddy_info_requests
);
1454 js
->pending_buddy_info_requests
= NULL
;
1458 static gboolean
jabber_buddy_get_info_timeout(gpointer data
)
1460 JabberBuddyInfo
*jbi
= data
;
1462 /* remove the pending callbacks */
1464 char *id
= jbi
->ids
->data
;
1465 jabber_iq_remove_callback_by_id(jbi
->js
, id
);
1466 jbi
->ids
= g_slist_remove(jbi
->ids
, id
);
1470 jbi
->js
->pending_buddy_info_requests
= g_slist_remove(jbi
->js
->pending_buddy_info_requests
, jbi
);
1471 jbi
->timeout_handle
= 0;
1473 jabber_buddy_info_show_if_ready(jbi
);
1478 static gboolean
_client_is_blacklisted(JabberBuddyResource
*jbr
, const char *ns
)
1480 /* can't be blacklisted if we don't know what you're running yet */
1481 if(!jbr
->client
.name
)
1484 if(purple_strequal(ns
, NS_LAST_ACTIVITY
)) {
1485 if(purple_strequal(jbr
->client
.name
, "Trillian")) {
1486 /* verified by nwalp 2007/05/09 */
1487 if(purple_strequal(jbr
->client
.version
, "3.1.0.121") ||
1488 /* verified by nwalp 2007/09/19 */
1489 purple_strequal(jbr
->client
.version
, "3.1.7.0")) {
1499 dispatch_queries_for_resource(JabberStream
*js
, JabberBuddyInfo
*jbi
,
1500 gboolean is_bare_jid
, const char *jid
,
1501 JabberBuddyResource
*jbr
)
1504 JabberBuddyInfoResource
*jbir
;
1505 char *full_jid
= NULL
;
1508 if (is_bare_jid
&& jbr
->name
) {
1509 full_jid
= g_strdup_printf("%s/%s", jid
, jbr
->name
);
1514 jbir
= g_new0(JabberBuddyInfoResource
, 1);
1515 g_hash_table_insert(jbi
->resources
, g_strdup(jbr
->name
), jbir
);
1517 if(!jbr
->client
.name
) {
1518 iq
= jabber_iq_new_query(js
, JABBER_IQ_GET
, "jabber:iq:version");
1519 xmlnode_set_attrib(iq
->node
, "to", to
);
1520 jabber_iq_set_callback(iq
, jabber_version_parse
, jbi
);
1521 jbi
->ids
= g_slist_prepend(jbi
->ids
, g_strdup(iq
->id
));
1525 /* this is to fix the feeling of irritation I get when trying
1526 * to get info on a friend running Trillian, which doesn't
1527 * respond (with an error or otherwise) to jabber:iq:last
1528 * requests. There are a number of Trillian users in my
1530 if(!_client_is_blacklisted(jbr
, NS_LAST_ACTIVITY
)) {
1531 iq
= jabber_iq_new_query(js
, JABBER_IQ_GET
, NS_LAST_ACTIVITY
);
1532 xmlnode_set_attrib(iq
->node
, "to", to
);
1533 jabber_iq_set_callback(iq
, jabber_last_parse
, jbi
);
1534 jbi
->ids
= g_slist_prepend(jbi
->ids
, g_strdup(iq
->id
));
1538 if (jbr
->tz_off
== PURPLE_NO_TZ_OFF
&&
1540 jabber_resource_has_capability(jbr
, NS_ENTITY_TIME
))) {
1542 iq
= jabber_iq_new(js
, JABBER_IQ_GET
);
1543 xmlnode_set_attrib(iq
->node
, "to", to
);
1544 child
= xmlnode_new_child(iq
->node
, "time");
1545 xmlnode_set_namespace(child
, NS_ENTITY_TIME
);
1546 jabber_iq_set_callback(iq
, jabber_time_parse
, jbi
);
1547 jbi
->ids
= g_slist_prepend(jbi
->ids
, g_strdup(iq
->id
));
1554 static void jabber_buddy_get_info_for_jid(JabberStream
*js
, const char *jid
)
1560 JabberBuddyInfo
*jbi
;
1562 gboolean is_bare_jid
;
1564 jb
= jabber_buddy_find(js
, jid
, TRUE
);
1570 slash
= strchr(jid
, '/');
1571 is_bare_jid
= (slash
== NULL
);
1573 jbi
= g_new0(JabberBuddyInfo
, 1);
1574 jbi
->jid
= g_strdup(jid
);
1577 jbi
->resources
= g_hash_table_new_full(jbir_hash
, jbir_equal
, g_free
, jabber_buddy_info_resource_free
);
1578 jbi
->user_info
= purple_notify_user_info_new();
1580 iq
= jabber_iq_new(js
, JABBER_IQ_GET
);
1582 xmlnode_set_attrib(iq
->node
, "to", jid
);
1583 vcard
= xmlnode_new_child(iq
->node
, "vCard");
1584 xmlnode_set_namespace(vcard
, "vcard-temp");
1586 jabber_iq_set_callback(iq
, jabber_vcard_parse
, jbi
);
1587 jbi
->ids
= g_slist_prepend(jbi
->ids
, g_strdup(iq
->id
));
1592 if (jb
->resources
) {
1593 for(resources
= jb
->resources
; resources
; resources
= resources
->next
) {
1594 JabberBuddyResource
*jbr
= resources
->data
;
1595 dispatch_queries_for_resource(js
, jbi
, is_bare_jid
, jid
, jbr
);
1598 /* user is offline, send a jabber:iq:last to find out last time online */
1599 iq
= jabber_iq_new_query(js
, JABBER_IQ_GET
, NS_LAST_ACTIVITY
);
1600 xmlnode_set_attrib(iq
->node
, "to", jid
);
1601 jabber_iq_set_callback(iq
, jabber_last_offline_parse
, jbi
);
1602 jbi
->ids
= g_slist_prepend(jbi
->ids
, g_strdup(iq
->id
));
1606 JabberBuddyResource
*jbr
= jabber_buddy_find_resource(jb
, slash
+ 1);
1608 dispatch_queries_for_resource(js
, jbi
, is_bare_jid
, jid
, jbr
);
1610 purple_debug_warning("jabber", "jabber_buddy_get_info_for_jid() "
1611 "was passed JID %s, but there is no corresponding "
1612 "JabberBuddyResource!\n", jid
);
1615 js
->pending_buddy_info_requests
= g_slist_prepend(js
->pending_buddy_info_requests
, jbi
);
1616 jbi
->timeout_handle
= purple_timeout_add_seconds(30, jabber_buddy_get_info_timeout
, jbi
);
1619 void jabber_buddy_get_info(PurpleConnection
*gc
, const char *who
)
1621 JabberStream
*js
= purple_connection_get_protocol_data(gc
);
1622 JabberID
*jid
= jabber_id_new(who
);
1627 if (jid
->node
&& jabber_chat_find(js
, jid
->node
, jid
->domain
)) {
1628 /* For a conversation, include the resource (indicates the user). */
1629 jabber_buddy_get_info_for_jid(js
, who
);
1631 char *bare_jid
= jabber_get_bare_jid(who
);
1632 jabber_buddy_get_info_for_jid(js
, bare_jid
);
1636 jabber_id_free(jid
);
1639 static void jabber_buddy_set_invisibility(JabberStream
*js
, const char *who
,
1642 PurplePresence
*gpresence
;
1643 PurpleAccount
*account
;
1644 PurpleStatus
*status
;
1645 JabberBuddy
*jb
= jabber_buddy_find(js
, who
, TRUE
);
1647 JabberBuddyState state
;
1651 account
= purple_connection_get_account(js
->gc
);
1652 gpresence
= purple_account_get_presence(account
);
1653 status
= purple_presence_get_active_status(gpresence
);
1655 purple_status_to_jabber(status
, &state
, &msg
, &priority
);
1656 presence
= jabber_presence_create_js(js
, state
, msg
, priority
);
1660 xmlnode_set_attrib(presence
, "to", who
);
1662 xmlnode_set_attrib(presence
, "type", "invisible");
1663 jb
->invisible
|= JABBER_INVIS_BUDDY
;
1665 jb
->invisible
&= ~JABBER_INVIS_BUDDY
;
1668 jabber_send(js
, presence
);
1669 xmlnode_free(presence
);
1672 static void jabber_buddy_make_invisible(PurpleBlistNode
*node
, gpointer data
)
1675 PurpleConnection
*gc
;
1678 g_return_if_fail(PURPLE_BLIST_NODE_IS_BUDDY(node
));
1680 buddy
= (PurpleBuddy
*) node
;
1681 gc
= purple_account_get_connection(purple_buddy_get_account(buddy
));
1682 js
= purple_connection_get_protocol_data(gc
);
1684 jabber_buddy_set_invisibility(js
, purple_buddy_get_name(buddy
), TRUE
);
1687 static void jabber_buddy_make_visible(PurpleBlistNode
*node
, gpointer data
)
1690 PurpleConnection
*gc
;
1693 g_return_if_fail(PURPLE_BLIST_NODE_IS_BUDDY(node
));
1695 buddy
= (PurpleBuddy
*) node
;
1696 gc
= purple_account_get_connection(purple_buddy_get_account(buddy
));
1697 js
= purple_connection_get_protocol_data(gc
);
1699 jabber_buddy_set_invisibility(js
, purple_buddy_get_name(buddy
), FALSE
);
1702 static void cancel_presence_notification(gpointer data
)
1705 PurpleConnection
*gc
;
1709 gc
= purple_account_get_connection(purple_buddy_get_account(buddy
));
1710 js
= purple_connection_get_protocol_data(gc
);
1712 jabber_presence_subscription_set(js
, purple_buddy_get_name(buddy
), "unsubscribed");
1716 jabber_buddy_cancel_presence_notification(PurpleBlistNode
*node
,
1720 PurpleAccount
*account
;
1721 PurpleConnection
*gc
;
1725 g_return_if_fail(PURPLE_BLIST_NODE_IS_BUDDY(node
));
1727 buddy
= (PurpleBuddy
*) node
;
1728 name
= purple_buddy_get_name(buddy
);
1729 account
= purple_buddy_get_account(buddy
);
1730 gc
= purple_account_get_connection(account
);
1732 msg
= g_strdup_printf(_("%s will no longer be able to see your status "
1733 "updates. Do you want to continue?"), name
);
1734 purple_request_yes_no(gc
, NULL
, _("Cancel Presence Notification"),
1735 msg
, 0 /* Yes */, account
, name
, NULL
, buddy
,
1736 cancel_presence_notification
, NULL
/* Do nothing */);
1740 static void jabber_buddy_rerequest_auth(PurpleBlistNode
*node
, gpointer data
)
1743 PurpleConnection
*gc
;
1746 g_return_if_fail(PURPLE_BLIST_NODE_IS_BUDDY(node
));
1748 buddy
= (PurpleBuddy
*) node
;
1749 gc
= purple_account_get_connection(purple_buddy_get_account(buddy
));
1750 js
= purple_connection_get_protocol_data(gc
);
1752 jabber_presence_subscription_set(js
, purple_buddy_get_name(buddy
), "subscribe");
1756 static void jabber_buddy_unsubscribe(PurpleBlistNode
*node
, gpointer data
)
1759 PurpleConnection
*gc
;
1762 g_return_if_fail(PURPLE_BLIST_NODE_IS_BUDDY(node
));
1764 buddy
= (PurpleBuddy
*) node
;
1765 gc
= purple_account_get_connection(purple_buddy_get_account(buddy
));
1766 js
= purple_connection_get_protocol_data(gc
);
1768 jabber_presence_subscription_set(js
, purple_buddy_get_name(buddy
), "unsubscribe");
1771 static void jabber_buddy_login(PurpleBlistNode
*node
, gpointer data
) {
1772 if(PURPLE_BLIST_NODE_IS_BUDDY(node
)) {
1773 /* simply create a directed presence of the current status */
1774 PurpleBuddy
*buddy
= (PurpleBuddy
*) node
;
1775 PurpleConnection
*gc
= purple_account_get_connection(purple_buddy_get_account(buddy
));
1776 JabberStream
*js
= purple_connection_get_protocol_data(gc
);
1777 PurpleAccount
*account
= purple_connection_get_account(gc
);
1778 PurplePresence
*gpresence
= purple_account_get_presence(account
);
1779 PurpleStatus
*status
= purple_presence_get_active_status(gpresence
);
1781 JabberBuddyState state
;
1785 purple_status_to_jabber(status
, &state
, &msg
, &priority
);
1786 presence
= jabber_presence_create_js(js
, state
, msg
, priority
);
1790 xmlnode_set_attrib(presence
, "to", purple_buddy_get_name(buddy
));
1792 jabber_send(js
, presence
);
1793 xmlnode_free(presence
);
1797 static void jabber_buddy_logout(PurpleBlistNode
*node
, gpointer data
) {
1798 if(PURPLE_BLIST_NODE_IS_BUDDY(node
)) {
1799 /* simply create a directed unavailable presence */
1800 PurpleBuddy
*buddy
= (PurpleBuddy
*) node
;
1801 PurpleConnection
*gc
= purple_account_get_connection(purple_buddy_get_account(buddy
));
1802 JabberStream
*js
= purple_connection_get_protocol_data(gc
);
1805 presence
= jabber_presence_create_js(js
, JABBER_BUDDY_STATE_UNAVAILABLE
, NULL
, 0);
1807 xmlnode_set_attrib(presence
, "to", purple_buddy_get_name(buddy
));
1809 jabber_send(js
, presence
);
1810 xmlnode_free(presence
);
1814 static GList
*jabber_buddy_menu(PurpleBuddy
*buddy
)
1816 PurpleConnection
*gc
= purple_account_get_connection(purple_buddy_get_account(buddy
));
1817 JabberStream
*js
= purple_connection_get_protocol_data(gc
);
1818 const char *name
= purple_buddy_get_name(buddy
);
1819 JabberBuddy
*jb
= jabber_buddy_find(js
, name
, TRUE
);
1823 PurpleMenuAction
*act
;
1828 if (js
->protocol_version
.major
== 0 && js
->protocol_version
.minor
== 9 &&
1829 jb
!= js
->user_jb
) {
1830 if(jb
->invisible
& JABBER_INVIS_BUDDY
) {
1831 act
= purple_menu_action_new(_("Un-hide From"),
1832 PURPLE_CALLBACK(jabber_buddy_make_visible
),
1835 act
= purple_menu_action_new(_("Temporarily Hide From"),
1836 PURPLE_CALLBACK(jabber_buddy_make_invisible
),
1839 m
= g_list_append(m
, act
);
1842 if(jb
->subscription
& JABBER_SUB_FROM
&& jb
!= js
->user_jb
) {
1843 act
= purple_menu_action_new(_("Cancel Presence Notification"),
1844 PURPLE_CALLBACK(jabber_buddy_cancel_presence_notification
),
1846 m
= g_list_append(m
, act
);
1849 if(!(jb
->subscription
& JABBER_SUB_TO
)) {
1850 act
= purple_menu_action_new(_("(Re-)Request authorization"),
1851 PURPLE_CALLBACK(jabber_buddy_rerequest_auth
),
1853 m
= g_list_append(m
, act
);
1855 } else if (jb
!= js
->user_jb
) {
1857 /* shouldn't this just happen automatically when the buddy is
1859 act
= purple_menu_action_new(_("Unsubscribe"),
1860 PURPLE_CALLBACK(jabber_buddy_unsubscribe
),
1862 m
= g_list_append(m
, act
);
1865 if (js
->googletalk
) {
1866 act
= purple_menu_action_new(_("Initiate _Chat"),
1867 PURPLE_CALLBACK(google_buddy_node_chat
),
1869 m
= g_list_append(m
, act
);
1873 * This if-condition implements parts of XEP-0100: Gateway Interaction
1875 * According to stpeter, there is no way to know if a jid on the roster is a gateway without sending a disco#info.
1876 * However, since the gateway might appear offline to us, we cannot get that information. Therefore, I just assume
1877 * that gateways on the roster can be identified by having no '@' in their jid. This is a faily safe assumption, since
1878 * people don't tend to have a server or other service there.
1880 * TODO: Use disco#info...
1882 if (strchr(name
, '@') == NULL
) {
1883 act
= purple_menu_action_new(_("Log In"),
1884 PURPLE_CALLBACK(jabber_buddy_login
),
1886 m
= g_list_append(m
, act
);
1887 act
= purple_menu_action_new(_("Log Out"),
1888 PURPLE_CALLBACK(jabber_buddy_logout
),
1890 m
= g_list_append(m
, act
);
1893 /* add all ad hoc commands to the action menu */
1894 for(jbrs
= jb
->resources
; jbrs
; jbrs
= g_list_next(jbrs
)) {
1895 JabberBuddyResource
*jbr
= jbrs
->data
;
1899 for(commands
= jbr
->commands
; commands
; commands
= g_list_next(commands
)) {
1900 JabberAdHocCommands
*cmd
= commands
->data
;
1901 act
= purple_menu_action_new(cmd
->name
, PURPLE_CALLBACK(jabber_adhoc_execute_action
), cmd
, NULL
);
1902 m
= g_list_append(m
, act
);
1910 jabber_blist_node_menu(PurpleBlistNode
*node
)
1912 if(PURPLE_BLIST_NODE_IS_BUDDY(node
)) {
1913 return jabber_buddy_menu((PurpleBuddy
*) node
);
1920 static void user_search_result_add_buddy_cb(PurpleConnection
*gc
, GList
*row
, void *user_data
)
1922 /* XXX find out the jid */
1923 purple_blist_request_add_buddy(purple_connection_get_account(gc
),
1924 g_list_nth_data(row
, 0), NULL
, NULL
);
1927 static void user_search_result_cb(JabberStream
*js
, const char *from
,
1928 JabberIqType type
, const char *id
,
1929 xmlnode
*packet
, gpointer data
)
1931 PurpleNotifySearchResults
*results
;
1932 PurpleNotifySearchColumn
*column
;
1933 xmlnode
*x
, *query
, *item
, *field
;
1935 /* XXX error checking? */
1936 if(!(query
= xmlnode_get_child(packet
, "query")))
1939 results
= purple_notify_searchresults_new();
1940 if((x
= xmlnode_get_child_with_namespace(query
, "x", "jabber:x:data"))) {
1942 GSList
*column_vars
= NULL
;
1944 purple_debug_info("jabber", "new-skool\n");
1946 if((reported
= xmlnode_get_child(x
, "reported"))) {
1947 xmlnode
*field
= xmlnode_get_child(reported
, "field");
1949 const char *var
= xmlnode_get_attrib(field
, "var");
1950 const char *label
= xmlnode_get_attrib(field
, "label");
1952 column
= purple_notify_searchresults_column_new(label
? label
: var
);
1953 purple_notify_searchresults_column_add(results
, column
);
1954 column_vars
= g_slist_append(column_vars
, (char *)var
);
1956 field
= xmlnode_get_next_twin(field
);
1960 item
= xmlnode_get_child(x
, "item");
1967 for (l
= column_vars
; l
!= NULL
; l
= l
->next
) {
1969 * Build a row containing the strings that correspond
1970 * to each column of the search results.
1972 for (field
= xmlnode_get_child(item
, "field");
1974 field
= xmlnode_get_next_twin(field
))
1976 if ((var
= xmlnode_get_attrib(field
, "var")) &&
1977 purple_strequal(var
, l
->data
) &&
1978 (valuenode
= xmlnode_get_child(field
, "value")))
1980 char *value
= xmlnode_get_data(valuenode
);
1981 row
= g_list_append(row
, value
);
1986 /* No data for this column */
1987 row
= g_list_append(row
, NULL
);
1989 purple_notify_searchresults_row_add(results
, row
);
1990 item
= xmlnode_get_next_twin(item
);
1993 g_slist_free(column_vars
);
1996 purple_debug_info("jabber", "old-skool\n");
1998 column
= purple_notify_searchresults_column_new(_("JID"));
1999 purple_notify_searchresults_column_add(results
, column
);
2000 column
= purple_notify_searchresults_column_new(_("First Name"));
2001 purple_notify_searchresults_column_add(results
, column
);
2002 column
= purple_notify_searchresults_column_new(_("Last Name"));
2003 purple_notify_searchresults_column_add(results
, column
);
2004 column
= purple_notify_searchresults_column_new(_("Nickname"));
2005 purple_notify_searchresults_column_add(results
, column
);
2006 column
= purple_notify_searchresults_column_new(_("Email"));
2007 purple_notify_searchresults_column_add(results
, column
);
2009 for(item
= xmlnode_get_child(query
, "item"); item
; item
= xmlnode_get_next_twin(item
)) {
2014 if(!(jid
= xmlnode_get_attrib(item
, "jid")))
2017 row
= g_list_append(row
, g_strdup(jid
));
2018 node
= xmlnode_get_child(item
, "first");
2019 row
= g_list_append(row
, node
? xmlnode_get_data(node
) : NULL
);
2020 node
= xmlnode_get_child(item
, "last");
2021 row
= g_list_append(row
, node
? xmlnode_get_data(node
) : NULL
);
2022 node
= xmlnode_get_child(item
, "nick");
2023 row
= g_list_append(row
, node
? xmlnode_get_data(node
) : NULL
);
2024 node
= xmlnode_get_child(item
, "email");
2025 row
= g_list_append(row
, node
? xmlnode_get_data(node
) : NULL
);
2026 purple_debug_info("jabber", "row=%p\n", row
);
2027 purple_notify_searchresults_row_add(results
, row
);
2031 purple_notify_searchresults_button_add(results
, PURPLE_NOTIFY_BUTTON_ADD
,
2032 user_search_result_add_buddy_cb
);
2034 purple_notify_searchresults(js
->gc
, NULL
, NULL
, _("The following are the results of your search"), results
, NULL
, NULL
);
2037 static void user_search_x_data_cb(JabberStream
*js
, xmlnode
*result
, gpointer data
)
2041 char *dir_server
= data
;
2044 /* if they've cancelled the search, we're
2045 * just going to get an error if we send
2046 * a cancel, so skip it */
2047 type
= xmlnode_get_attrib(result
, "type");
2048 if(purple_strequal(type
, "cancel")) {
2053 iq
= jabber_iq_new_query(js
, JABBER_IQ_SET
, "jabber:iq:search");
2054 query
= xmlnode_get_child(iq
->node
, "query");
2056 xmlnode_insert_child(query
, result
);
2058 jabber_iq_set_callback(iq
, user_search_result_cb
, NULL
);
2059 xmlnode_set_attrib(iq
->node
, "to", dir_server
);
2064 struct user_search_info
{
2066 char *directory_server
;
2069 static void user_search_cancel_cb(struct user_search_info
*usi
, PurpleRequestFields
*fields
)
2071 g_free(usi
->directory_server
);
2075 static void user_search_cb(struct user_search_info
*usi
, PurpleRequestFields
*fields
)
2077 JabberStream
*js
= usi
->js
;
2080 GList
*groups
, *flds
;
2082 iq
= jabber_iq_new_query(js
, JABBER_IQ_SET
, "jabber:iq:search");
2083 query
= xmlnode_get_child(iq
->node
, "query");
2085 for(groups
= purple_request_fields_get_groups(fields
); groups
; groups
= groups
->next
) {
2086 for(flds
= purple_request_field_group_get_fields(groups
->data
);
2087 flds
; flds
= flds
->next
) {
2088 PurpleRequestField
*field
= flds
->data
;
2089 const char *id
= purple_request_field_get_id(field
);
2090 const char *value
= purple_request_field_string_get_value(field
);
2092 if(value
&& (purple_strequal(id
, "first") || purple_strequal(id
, "last") || purple_strequal(id
, "nick") || purple_strequal(id
, "email"))) {
2093 xmlnode
*y
= xmlnode_new_child(query
, id
);
2094 xmlnode_insert_data(y
, value
, -1);
2099 jabber_iq_set_callback(iq
, user_search_result_cb
, NULL
);
2100 xmlnode_set_attrib(iq
->node
, "to", usi
->directory_server
);
2103 g_free(usi
->directory_server
);
2108 /* This is for gettext only -- it will see this even though there's an #if 0. */
2111 * An incomplete list of server generated original language search
2112 * comments for Jabber User Directories
2114 * See discussion thread "Search comment for Jabber is not translatable"
2115 * in purple-i18n@lists.sourceforge.net (March 2006)
2117 static const char * jabber_user_dir_comments
[] = {
2118 /* current comment from Jabber User Directory users.jabber.org */
2119 N_("Find a contact by entering the search criteria in the given fields. "
2120 "Note: Each field supports wild card searches (%)"),
2125 static void user_search_fields_result_cb(JabberStream
*js
, const char *from
,
2126 JabberIqType type
, const char *id
,
2127 xmlnode
*packet
, gpointer data
)
2134 if (type
== JABBER_IQ_ERROR
) {
2135 char *msg
= jabber_parse_error(js
, packet
, NULL
);
2138 msg
= g_strdup(_("Unknown error"));
2140 purple_notify_error(js
->gc
, _("Directory Query Failed"),
2141 _("Could not query the directory server."), msg
);
2148 if(!(query
= xmlnode_get_child(packet
, "query")))
2151 if((x
= xmlnode_get_child_with_namespace(query
, "x", "jabber:x:data"))) {
2152 jabber_x_data_request(js
, x
, user_search_x_data_cb
, g_strdup(from
));
2155 struct user_search_info
*usi
;
2157 char *instructions
= NULL
;
2158 PurpleRequestFields
*fields
;
2159 PurpleRequestFieldGroup
*group
;
2160 PurpleRequestField
*field
;
2163 fields
= purple_request_fields_new();
2164 group
= purple_request_field_group_new(NULL
);
2165 purple_request_fields_add_group(fields
, group
);
2167 if((instnode
= xmlnode_get_child(query
, "instructions")))
2169 char *tmp
= xmlnode_get_data(instnode
);
2173 /* Try to translate the message (see static message
2174 list in jabber_user_dir_comments[]) */
2175 instructions
= g_strdup_printf(_("Server Instructions: %s"), _(tmp
));
2182 instructions
= g_strdup(_("Fill in one or more fields to search "
2183 "for any matching XMPP users."));
2186 if(xmlnode_get_child(query
, "first")) {
2187 field
= purple_request_field_string_new("first", _("First Name"),
2189 purple_request_field_group_add_field(group
, field
);
2191 if(xmlnode_get_child(query
, "last")) {
2192 field
= purple_request_field_string_new("last", _("Last Name"),
2194 purple_request_field_group_add_field(group
, field
);
2196 if(xmlnode_get_child(query
, "nick")) {
2197 field
= purple_request_field_string_new("nick", _("Nickname"),
2199 purple_request_field_group_add_field(group
, field
);
2201 if(xmlnode_get_child(query
, "email")) {
2202 field
= purple_request_field_string_new("email", _("Email Address"),
2204 purple_request_field_group_add_field(group
, field
);
2207 usi
= g_new0(struct user_search_info
, 1);
2209 usi
->directory_server
= g_strdup(from
);
2211 purple_request_fields(js
->gc
, _("Search for XMPP users"),
2212 _("Search for XMPP users"), instructions
, fields
,
2213 _("Search"), G_CALLBACK(user_search_cb
),
2214 _("Cancel"), G_CALLBACK(user_search_cancel_cb
),
2215 purple_connection_get_account(js
->gc
), NULL
, NULL
,
2218 g_free(instructions
);
2222 void jabber_user_search(JabberStream
*js
, const char *directory
)
2226 /* XXX: should probably better validate the directory we're given */
2227 if(!directory
|| !*directory
) {
2228 purple_notify_error(js
->gc
, _("Invalid Directory"), _("Invalid Directory"), NULL
);
2232 /* If the value provided isn't the disco#info default, persist it. Otherwise,
2233 make sure we aren't persisting an old value */
2234 if(js
->user_directories
&& js
->user_directories
->data
&&
2235 purple_strequal(directory
, js
->user_directories
->data
)) {
2236 purple_account_set_string(js
->gc
->account
, "user_directory", "");
2239 purple_account_set_string(js
->gc
->account
, "user_directory", directory
);
2242 iq
= jabber_iq_new_query(js
, JABBER_IQ_GET
, "jabber:iq:search");
2243 xmlnode_set_attrib(iq
->node
, "to", directory
);
2245 jabber_iq_set_callback(iq
, user_search_fields_result_cb
, NULL
);
2250 void jabber_user_search_begin(PurplePluginAction
*action
)
2252 PurpleConnection
*gc
= (PurpleConnection
*) action
->context
;
2253 JabberStream
*js
= purple_connection_get_protocol_data(gc
);
2254 const char *def_val
= purple_account_get_string(js
->gc
->account
, "user_directory", "");
2255 if(!*def_val
&& js
->user_directories
)
2256 def_val
= js
->user_directories
->data
;
2258 purple_request_input(gc
, _("Enter a User Directory"), _("Enter a User Directory"),
2259 _("Select a user directory to search"),
2262 _("Search Directory"), PURPLE_CALLBACK(jabber_user_search
),
2269 jabber_resource_know_capabilities(const JabberBuddyResource
*jbr
)
2271 return jbr
->caps
.info
!= NULL
;
2275 jabber_resource_has_capability(const JabberBuddyResource
*jbr
, const gchar
*cap
)
2277 const GList
*node
= NULL
;
2278 const JabberCapsNodeExts
*exts
;
2280 if (!jbr
->caps
.info
) {
2281 purple_debug_info("jabber",
2282 "Unable to find caps: nothing known about buddy\n");
2286 node
= g_list_find_custom(jbr
->caps
.info
->features
, cap
, (GCompareFunc
)strcmp
);
2287 if (!node
&& jbr
->caps
.exts
&& jbr
->caps
.info
->exts
) {
2289 exts
= jbr
->caps
.info
->exts
;
2290 /* Walk through all the enabled caps, checking each list for the cap.
2291 * Don't check it twice, though. */
2292 for (ext
= jbr
->caps
.exts
; ext
&& !node
; ext
= ext
->next
) {
2293 GList
*features
= g_hash_table_lookup(exts
->exts
, ext
->data
);
2295 node
= g_list_find_custom(features
, cap
, (GCompareFunc
)strcmp
);
2299 return (node
!= NULL
);
2303 jabber_buddy_has_capability(const JabberBuddy
*jb
, const gchar
*cap
)
2305 JabberBuddyResource
*jbr
= jabber_buddy_find_resource((JabberBuddy
*)jb
, NULL
);
2308 purple_debug_info("jabber",
2309 "Unable to find caps: buddy might be offline\n");
2313 return jabber_resource_has_capability(jbr
, cap
);
2317 jabber_resource_get_identity_category_type(const JabberBuddyResource
*jbr
,
2318 const gchar
*category
)
2320 const GList
*iter
= NULL
;
2322 if (jbr
->caps
.info
) {
2323 for (iter
= jbr
->caps
.info
->identities
; iter
; iter
= g_list_next(iter
)) {
2324 const JabberIdentity
*identity
=
2325 (JabberIdentity
*) iter
->data
;
2327 if (purple_strequal(identity
->category
, category
)) {
2328 return identity
->type
;