pidgin/pidgin

closing merged branch
port-changes-from-branch-2.x.y-to-default
2020-02-03, Gary Kramlich
2f836435c33c
closing merged branch
/*
* purple - Jabber Protocol Plugin
*
* Purple is the legal property of its developers, whose names are too numerous
* to list here. Please refer to the COPYRIGHT file distributed with this
* source distribution.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02111-1301 USA
*
*/
#include "internal.h"
#include "debug.h"
#include "image-store.h"
#include "protocol.h"
#include "notify.h"
#include "request.h"
#include "util.h"
#include "xmlnode.h"
#include "action.h"
#include "buddy.h"
#include "chat.h"
#include "jabber.h"
#include "iq.h"
#include "presence.h"
#include "useravatar.h"
#include "xdata.h"
#include "pep.h"
#include "adhoccommands.h"
#include "google/google.h"
typedef struct {
long idle_seconds;
} JabberBuddyInfoResource;
typedef struct {
JabberStream *js;
JabberBuddy *jb;
char *jid;
GSList *ids;
GHashTable *resources;
guint timeout_handle;
GSList *vcard_images;
PurpleNotifyUserInfo *user_info;
long last_seconds;
gchar *last_message;
} JabberBuddyInfo;
void
jabber_adhoc_commands_free(JabberAdHocCommands *cmd)
{
g_return_if_fail(cmd != NULL);
g_free(cmd->jid);
g_free(cmd->node);
g_free(cmd->name);
g_free(cmd);
}
static void
jabber_buddy_resource_free(JabberBuddyResource *jbr)
{
g_return_if_fail(jbr != NULL);
g_list_free_full(jbr->commands, (GDestroyNotify)jabber_adhoc_commands_free);
g_list_free_full(jbr->caps.exts, g_free);
g_free(jbr->name);
g_free(jbr->status);
g_free(jbr->thread_id);
g_free(jbr->client.name);
g_free(jbr->client.version);
g_free(jbr->client.os);
g_free(jbr);
}
void jabber_buddy_free(JabberBuddy *jb)
{
g_return_if_fail(jb != NULL);
g_free(jb->error_msg);
g_list_free_full(jb->resources, (GDestroyNotify)jabber_buddy_resource_free);
g_free(jb);
}
JabberBuddy *jabber_buddy_find(JabberStream *js, const char *name,
gboolean create)
{
JabberBuddy *jb;
char *realname;
if (js->buddies == NULL)
return NULL;
if(!(realname = jabber_get_bare_jid(name)))
return NULL;
jb = g_hash_table_lookup(js->buddies, realname);
if(!jb && create) {
jb = g_new0(JabberBuddy, 1);
g_hash_table_insert(js->buddies, realname, jb);
} else
g_free(realname);
return jb;
}
/* Returns -1 if a is a higher priority resource than b, or is
* "more available" than b. 0 if they're the same, and 1 if b is
* higher priority/more available than a.
*/
static gint resource_compare_cb(gconstpointer a, gconstpointer b)
{
const JabberBuddyResource *jbra = a;
const JabberBuddyResource *jbrb = b;
JabberBuddyState state_a, state_b;
if (jbra->priority != jbrb->priority)
return jbra->priority > jbrb->priority ? -1 : 1;
/* Fold the states for easier comparison */
/* TODO: Differentiate online/chat and away/dnd? */
switch (jbra->state) {
case JABBER_BUDDY_STATE_ONLINE:
case JABBER_BUDDY_STATE_CHAT:
state_a = JABBER_BUDDY_STATE_ONLINE;
break;
case JABBER_BUDDY_STATE_AWAY:
case JABBER_BUDDY_STATE_DND:
state_a = JABBER_BUDDY_STATE_AWAY;
break;
case JABBER_BUDDY_STATE_XA:
state_a = JABBER_BUDDY_STATE_XA;
break;
case JABBER_BUDDY_STATE_UNAVAILABLE:
state_a = JABBER_BUDDY_STATE_UNAVAILABLE;
break;
default:
state_a = JABBER_BUDDY_STATE_UNKNOWN;
break;
}
switch (jbrb->state) {
case JABBER_BUDDY_STATE_ONLINE:
case JABBER_BUDDY_STATE_CHAT:
state_b = JABBER_BUDDY_STATE_ONLINE;
break;
case JABBER_BUDDY_STATE_AWAY:
case JABBER_BUDDY_STATE_DND:
state_b = JABBER_BUDDY_STATE_AWAY;
break;
case JABBER_BUDDY_STATE_XA:
state_b = JABBER_BUDDY_STATE_XA;
break;
case JABBER_BUDDY_STATE_UNAVAILABLE:
state_b = JABBER_BUDDY_STATE_UNAVAILABLE;
break;
default:
state_b = JABBER_BUDDY_STATE_UNKNOWN;
break;
}
if (state_a == state_b) {
if (jbra->idle == jbrb->idle)
return 0;
else if ((jbra->idle && !jbrb->idle) ||
(jbra->idle && jbrb->idle && jbra->idle < jbrb->idle))
return 1;
else
return -1;
}
if (state_a == JABBER_BUDDY_STATE_ONLINE)
return -1;
else if (state_a == JABBER_BUDDY_STATE_AWAY &&
(state_b == JABBER_BUDDY_STATE_XA ||
state_b == JABBER_BUDDY_STATE_UNAVAILABLE ||
state_b == JABBER_BUDDY_STATE_UNKNOWN))
return -1;
else if (state_a == JABBER_BUDDY_STATE_XA &&
(state_b == JABBER_BUDDY_STATE_UNAVAILABLE ||
state_b == JABBER_BUDDY_STATE_UNKNOWN))
return -1;
else if (state_a == JABBER_BUDDY_STATE_UNAVAILABLE &&
state_b == JABBER_BUDDY_STATE_UNKNOWN)
return -1;
return 1;
}
JabberBuddyResource *jabber_buddy_find_resource(JabberBuddy *jb,
const char *resource)
{
GList *l;
if (!jb)
return NULL;
if (resource == NULL)
return jb->resources ? jb->resources->data : NULL;
for (l = jb->resources; l; l = l->next)
{
JabberBuddyResource *jbr = l->data;
if (purple_strequal(resource, jbr->name))
return jbr;
}
return NULL;
}
JabberBuddyResource *jabber_buddy_track_resource(JabberBuddy *jb, const char *resource,
int priority, JabberBuddyState state, const char *status)
{
/* TODO: Optimization: Only reinsert if priority+state changed */
JabberBuddyResource *jbr = jabber_buddy_find_resource(jb, resource);
if (jbr) {
jb->resources = g_list_remove(jb->resources, jbr);
} else {
jbr = g_new0(JabberBuddyResource, 1);
jbr->jb = jb;
jbr->name = g_strdup(resource);
jbr->capabilities = JABBER_CAP_NONE;
jbr->tz_off = PURPLE_NO_TZ_OFF;
}
jbr->priority = priority;
jbr->state = state;
g_free(jbr->status);
jbr->status = g_strdup(status);
jb->resources = g_list_insert_sorted(jb->resources, jbr,
resource_compare_cb);
return jbr;
}
void jabber_buddy_remove_resource(JabberBuddy *jb, const char *resource)
{
JabberBuddyResource *jbr = jabber_buddy_find_resource(jb, resource);
if(!jbr)
return;
jbr->jb->resources = g_list_remove(jbr->jb->resources, jbr);
jabber_buddy_resource_free(jbr);
}
/*******
* This is the old vCard stuff taken from the old prpl. vCards, by definition
* are a temporary thing until jabber can get its act together and come up
* with a format for user information, hence the namespace of 'vcard-temp'
*
* Since I don't feel like putting that much work into something that's
* _supposed_ to go away, i'm going to just copy the kludgy old code here,
* and make it purdy when jabber comes up with a standards-track JEP to
* replace vcard-temp
* --Nathan
*******/
/*---------------------------------------*/
/* Jabber "set info" (vCard) support */
/*---------------------------------------*/
/*
* V-Card format:
*
* <vCard prodid='' version='' xmlns=''>
* <FN></FN>
* <N>
* <FAMILY/>
* <GIVEN/>
* </N>
* <NICKNAME/>
* <URL/>
* <ADR>
* <STREET/>
* <EXTADD/>
* <LOCALITY/>
* <REGION/>
* <PCODE/>
* <COUNTRY/>
* </ADR>
* <TEL/>
* <EMAIL/>
* <ORG>
* <ORGNAME/>
* <ORGUNIT/>
* </ORG>
* <TITLE/>
* <ROLE/>
* <DESC/>
* <BDAY/>
* </vCard>
*
* See also:
*
* http://docs.jabber.org/proto/html/vcard-temp.html
* http://www.vcard-xml.org/dtd/vCard-XML-v2-20010520.dtd
*/
/*
* Cross-reference user-friendly V-Card entry labels to vCard XML tags
* and attributes.
*
* Order is (or should be) unimportant. For example: we have no way of
* knowing in what order real data will arrive.
*
* Format: Label, Pre-set text, "visible" flag, "editable" flag, XML tag
* name, XML tag's parent tag "path" (relative to vCard node).
*
* List is terminated by a NULL label pointer.
*
* Entries with no label text, but with XML tag and parent tag
* entries, are used by V-Card XML construction routines to
* "automagically" construct the appropriate XML node tree.
*
* Thoughts on future direction/expansion
*
* This is a "simple" vCard.
*
* It is possible for nodes other than the "vCard" node to have
* attributes. Should that prove necessary/desirable, add an
* "attributes" pointer to the vcard_template struct, create the
* necessary tag_attr structs, and add 'em to the vcard_dflt_data
* array.
*
* The above changes will (obviously) require changes to the vCard
* construction routines.
*/
struct vcard_template {
char *label; /* label text pointer */
char *tag; /* tag text */
char *ptag; /* parent tag "path" text */
} const vcard_template_data[] = {
{N_("Full Name"), "FN", NULL},
{N_("Family Name"), "FAMILY", "N"},
{N_("Given Name"), "GIVEN", "N"},
{N_("Nickname"), "NICKNAME", NULL},
{N_("URL"), "URL", NULL},
{N_("Street Address"), "STREET", "ADR"},
{N_("Extended Address"), "EXTADD", "ADR"},
{N_("Locality"), "LOCALITY", "ADR"},
{N_("Region"), "REGION", "ADR"},
{N_("Postal Code"), "PCODE", "ADR"},
{N_("Country"), "CTRY", "ADR"},
{N_("Telephone"), "NUMBER", "TEL"},
{N_("Email"), "USERID", "EMAIL"},
{N_("Organization Name"), "ORGNAME", "ORG"},
{N_("Organization Unit"), "ORGUNIT", "ORG"},
{N_("Job Title"), "TITLE", NULL},
{N_("Role"), "ROLE", NULL},
{N_("Birthday"), "BDAY", NULL},
{N_("Description"), "DESC", NULL},
{"", "N", NULL},
{"", "ADR", NULL},
{"", "ORG", NULL},
{NULL, NULL, NULL}
};
/*
* The "vCard" tag's attribute list...
*/
struct tag_attr {
char *attr;
char *value;
} const vcard_tag_attr_list[] = {
{"prodid", "-//HandGen//NONSGML vGen v1.0//EN"},
{"version", "2.0", },
{"xmlns", "vcard-temp", },
{NULL, NULL},
};
/*
* Insert a tag node into an PurpleXmlNode tree, recursively inserting parent tag
* nodes as necessary
*
* Returns pointer to inserted node
*
* Note to hackers: this code is designed to be re-entrant (it's recursive--it
* calls itself), so don't put any "static"s in here!
*/
static PurpleXmlNode *insert_tag_to_parent_tag(PurpleXmlNode *start, const char *parent_tag, const char *new_tag)
{
PurpleXmlNode *x = NULL;
/*
* If the parent tag wasn't specified, see if we can get it
* from the vCard template struct.
*/
if(parent_tag == NULL) {
const struct vcard_template *vc_tp = vcard_template_data;
while(vc_tp->label != NULL) {
if(purple_strequal(vc_tp->tag, new_tag)) {
parent_tag = vc_tp->ptag;
break;
}
++vc_tp;
}
}
/*
* If we have a parent tag...
*/
if(parent_tag != NULL ) {
/*
* Try to get the parent node for a tag
*/
if((x = purple_xmlnode_get_child(start, parent_tag)) == NULL) {
/*
* Descend?
*/
char *grand_parent = g_strdup(parent_tag);
char *parent;
if((parent = strrchr(grand_parent, '/')) != NULL) {
*(parent++) = '\0';
x = insert_tag_to_parent_tag(start, grand_parent, parent);
} else {
x = purple_xmlnode_new_child(start, grand_parent);
}
g_free(grand_parent);
} else {
/*
* We found *something* to be the parent node.
* Note: may be the "root" node!
*/
PurpleXmlNode *y;
if((y = purple_xmlnode_get_child(x, new_tag)) != NULL) {
return(y);
}
}
}
/*
* insert the new tag into its parent node
*/
return(purple_xmlnode_new_child((x == NULL? start : x), new_tag));
}
/*
* Send vCard info to Jabber server
*/
void jabber_set_info(PurpleConnection *gc, const char *info)
{
PurpleImage *img;
JabberIq *iq;
JabberStream *js = purple_connection_get_protocol_data(gc);
PurpleXmlNode *vc_node;
const struct tag_attr *tag_attr;
/* if we haven't grabbed the remote vcard yet, we can't
* assume that what we have here is correct */
if(!js->vcard_fetched) {
PurpleImage *image;
g_free(js->initial_avatar_hash);
image = purple_buddy_icons_find_account_icon(purple_connection_get_account(gc));
if (image != NULL) {
js->initial_avatar_hash = g_compute_checksum_for_data(
G_CHECKSUM_SHA1,
purple_image_get_data(image),
purple_image_get_data_size(image)
);
g_object_unref(image);
} else {
js->initial_avatar_hash = NULL;
}
return;
}
if (js->vcard_timer) {
g_source_remove(js->vcard_timer);
js->vcard_timer = 0;
}
g_free(js->avatar_hash);
js->avatar_hash = NULL;
/*
* Send only if there's actually any *information* to send
*/
vc_node = info ? purple_xmlnode_from_str(info, -1) : NULL;
if (vc_node && (!vc_node->name ||
g_ascii_strncasecmp(vc_node->name, "vCard", 5))) {
purple_xmlnode_free(vc_node);
vc_node = NULL;
}
if ((img = purple_buddy_icons_find_account_icon(purple_connection_get_account(gc)))) {
gconstpointer avatar_data;
gsize avatar_len;
PurpleXmlNode *photo, *binval, *type;
gchar *enc;
if(!vc_node) {
vc_node = purple_xmlnode_new("vCard");
for(tag_attr = vcard_tag_attr_list; tag_attr->attr != NULL; ++tag_attr)
purple_xmlnode_set_attrib(vc_node, tag_attr->attr, tag_attr->value);
}
avatar_data = purple_image_get_data(img);
avatar_len = purple_image_get_data_size(img);
/* Get rid of an old PHOTO if one exists.
* TODO: This may want to be modified to remove all old PHOTO
* children, at the moment some people have managed to get
* multiple PHOTO entries in their vCard. */
if((photo = purple_xmlnode_get_child(vc_node, "PHOTO"))) {
purple_xmlnode_free(photo);
}
photo = purple_xmlnode_new_child(vc_node, "PHOTO");
type = purple_xmlnode_new_child(photo, "TYPE");
purple_xmlnode_insert_data(type, "image/png", -1);
binval = purple_xmlnode_new_child(photo, "BINVAL");
enc = g_base64_encode(avatar_data, avatar_len);
js->avatar_hash = g_compute_checksum_for_data(G_CHECKSUM_SHA1,
avatar_data, avatar_len);
purple_xmlnode_insert_data(binval, enc, -1);
g_free(enc);
g_object_unref(img);
} else if (vc_node) {
PurpleXmlNode *photo;
/* TODO: Remove all PHOTO children? (see above note) */
if ((photo = purple_xmlnode_get_child(vc_node, "PHOTO"))) {
purple_xmlnode_free(photo);
}
}
if (vc_node != NULL) {
iq = jabber_iq_new(js, JABBER_IQ_SET);
purple_xmlnode_insert_child(iq->node, vc_node);
jabber_iq_send(iq);
/* Send presence to update vcard-temp:x:update */
jabber_presence_send(js, FALSE);
}
}
void jabber_set_buddy_icon(PurpleConnection *gc, PurpleImage *img)
{
PurpleAccount *account = purple_connection_get_account(gc);
/* Publish the avatar as specified in XEP-0084 */
jabber_avatar_set(purple_connection_get_protocol_data(gc), img);
/* Set the image in our vCard */
jabber_set_info(gc, purple_account_get_user_info(account));
/* TODO: Fake image to ourselves, since a number of servers do not echo
* back our presence to us. To do this without uselessly copying the data
* of the image, we need purple_buddy_icons_set_for_user_image (i.e. takes
* an existing icon/stored image). */
}
/*
* This is the callback from the "ok clicked" for "set vCard"
*
* Sets the vCard with data from PurpleRequestFields.
*/
static void
jabber_format_info(PurpleConnection *gc, PurpleRequestFields *fields)
{
PurpleXmlNode *vc_node;
PurpleRequestField *field;
const char *text;
char *p;
const struct vcard_template *vc_tp;
const struct tag_attr *tag_attr;
vc_node = purple_xmlnode_new("vCard");
for(tag_attr = vcard_tag_attr_list; tag_attr->attr != NULL; ++tag_attr)
purple_xmlnode_set_attrib(vc_node, tag_attr->attr, tag_attr->value);
for (vc_tp = vcard_template_data; vc_tp->label != NULL; vc_tp++) {
if (*vc_tp->label == '\0')
continue;
field = purple_request_fields_get_field(fields, vc_tp->tag);
text = purple_request_field_string_get_value(field);
if (text != NULL && *text != '\0') {
PurpleXmlNode *xp;
purple_debug_info("jabber", "Setting %s to '%s'\n", vc_tp->tag, text);
if ((xp = insert_tag_to_parent_tag(vc_node,
NULL, vc_tp->tag)) != NULL) {
purple_xmlnode_insert_data(xp, text, -1);
}
}
}
p = purple_xmlnode_to_str(vc_node, NULL);
purple_xmlnode_free(vc_node);
purple_account_set_user_info(purple_connection_get_account(gc), p);
purple_serv_set_info(gc, p);
g_free(p);
}
/*
* This gets executed by the proto action
*
* Creates a new PurpleRequestFields struct, gets the XML-formatted user_info
* string (if any) into GSLists for the (multi-entry) edit dialog and
* calls the set_vcard dialog.
*/
void jabber_setup_set_info(PurpleProtocolAction *action)
{
PurpleConnection *gc = (PurpleConnection *) action->connection;
PurpleRequestFields *fields;
PurpleRequestFieldGroup *group;
PurpleRequestField *field;
const struct vcard_template *vc_tp;
const char *user_info;
char *cdata = NULL;
PurpleXmlNode *x_vc_data = NULL;
fields = purple_request_fields_new();
group = purple_request_field_group_new(NULL);
purple_request_fields_add_group(fields, group);
/*
* Get existing, XML-formatted, user info
*/
if((user_info = purple_account_get_user_info(purple_connection_get_account(gc))) != NULL)
x_vc_data = purple_xmlnode_from_str(user_info, -1);
/*
* Set up GSLists for edit with labels from "template," data from user info
*/
for(vc_tp = vcard_template_data; vc_tp->label != NULL; ++vc_tp) {
PurpleXmlNode *data_node;
if((vc_tp->label)[0] == '\0')
continue;
if (x_vc_data != NULL) {
if(vc_tp->ptag == NULL) {
data_node = purple_xmlnode_get_child(x_vc_data, vc_tp->tag);
} else {
gchar *tag = g_strdup_printf("%s/%s", vc_tp->ptag, vc_tp->tag);
data_node = purple_xmlnode_get_child(x_vc_data, tag);
g_free(tag);
}
if(data_node)
cdata = purple_xmlnode_get_data(data_node);
}
if(purple_strequal(vc_tp->tag, "DESC")) {
field = purple_request_field_string_new(vc_tp->tag,
_(vc_tp->label), cdata,
TRUE);
} else {
field = purple_request_field_string_new(vc_tp->tag,
_(vc_tp->label), cdata,
FALSE);
}
g_free(cdata);
cdata = NULL;
purple_request_field_group_add_field(group, field);
}
if(x_vc_data != NULL)
purple_xmlnode_free(x_vc_data);
purple_request_fields(gc, _("Edit XMPP vCard"),
_("Edit XMPP vCard"),
_("All items below are optional. Enter only the "
"information with which you feel comfortable."),
fields,
_("Save"), G_CALLBACK(jabber_format_info),
_("Cancel"), NULL,
purple_request_cpar_from_connection(gc),
gc);
}
/*---------------------------------------*/
/* End Jabber "set info" (vCard) support */
/*---------------------------------------*/
/******
* end of that ancient crap that needs to die
******/
static void jabber_buddy_info_destroy(JabberBuddyInfo *jbi)
{
/* Remove the timeout, which would otherwise trigger jabber_buddy_get_info_timeout() */
if (jbi->timeout_handle > 0)
g_source_remove(jbi->timeout_handle);
g_slist_free(jbi->ids);
g_free(jbi->jid);
g_hash_table_destroy(jbi->resources);
g_free(jbi->last_message);
purple_notify_user_info_destroy(jbi->user_info);
g_free(jbi);
}
static void
add_jbr_info(JabberBuddyInfo *jbi, const char *resource,
JabberBuddyResource *jbr)
{
JabberBuddyInfoResource *jbir;
PurpleNotifyUserInfo *user_info;
jbir = g_hash_table_lookup(jbi->resources, resource);
user_info = jbi->user_info;
if (jbr && jbr->client.name) {
char *tmp =
g_strdup_printf("%s%s%s", jbr->client.name,
(jbr->client.version ? " " : ""),
(jbr->client.version ? jbr->client.version : ""));
/* TODO: Check whether it's correct to call prepend_pair_html,
or if we should be using prepend_pair_plaintext */
purple_notify_user_info_prepend_pair_html(user_info, _("Client"), tmp);
g_free(tmp);
if (jbr->client.os) {
/* TODO: Check whether it's correct to call prepend_pair_html,
or if we should be using prepend_pair_plaintext */
purple_notify_user_info_prepend_pair_html(user_info, _("Operating System"), jbr->client.os);
}
}
if (jbr && jbr->tz_off != PURPLE_NO_TZ_OFF) {
time_t now_t;
struct tm *now;
char *timestamp;
time(&now_t);
now_t += jbr->tz_off;
now = gmtime(&now_t);
timestamp = g_strdup_printf("%s %c%02d%02d", purple_time_format(now),
jbr->tz_off < 0 ? '-' : '+',
abs((int)(jbr->tz_off / (60 * 60))),
abs((int)((jbr->tz_off % (60 * 60)) / 60)));
purple_notify_user_info_prepend_pair_plaintext(user_info, _("Local Time"), timestamp);
g_free(timestamp);
}
if (jbir && jbir->idle_seconds > 0) {
char *idle = purple_str_seconds_to_string(jbir->idle_seconds);
purple_notify_user_info_prepend_pair_plaintext(user_info, _("Idle"), idle);
g_free(idle);
}
if (jbr) {
char *purdy = NULL;
char *tmp;
char priority[12];
const char *status_name = jabber_buddy_state_get_name(jbr->state);
if (jbr->status) {
tmp = purple_markup_escape_text(jbr->status, -1);
purdy = purple_strdup_withhtml(tmp);
g_free(tmp);
if (purple_strequal(status_name, purdy))
status_name = NULL;
}
tmp = g_strdup_printf("%s%s%s", (status_name ? status_name : ""),
((status_name && purdy) ? ": " : ""),
(purdy ? purdy : ""));
purple_notify_user_info_prepend_pair_html(user_info, _("Status"), tmp);
g_snprintf(priority, sizeof(priority), "%d", jbr->priority);
purple_notify_user_info_prepend_pair_plaintext(user_info, _("Priority"), priority);
g_free(tmp);
g_free(purdy);
} else {
purple_notify_user_info_prepend_pair_plaintext(user_info, _("Status"), _("Unknown"));
}
}
static void jabber_buddy_info_show_if_ready(JabberBuddyInfo *jbi)
{
char *resource_name;
JabberBuddyResource *jbr;
GList *resources;
PurpleNotifyUserInfo *user_info;
/* not yet */
if (jbi->ids)
return;
user_info = jbi->user_info;
resource_name = jabber_get_resource(jbi->jid);
/* If we have one or more pairs from the vcard, put a section break above it */
if (g_queue_get_length(purple_notify_user_info_get_entries(user_info)))
purple_notify_user_info_prepend_section_break(user_info);
/* Add the information about the user's resource(s) */
if (resource_name) {
jbr = jabber_buddy_find_resource(jbi->jb, resource_name);
add_jbr_info(jbi, resource_name, jbr);
} else {
/* TODO: This is in priority-ascending order (lowest prio first), because
* everything is prepended. Is that ok? */
for (resources = jbi->jb->resources; resources; resources = resources->next) {
jbr = resources->data;
/* put a section break between resources, this is not needed if
we are at the first, because one was already added for the vcard
section */
if (resources != jbi->jb->resources)
purple_notify_user_info_prepend_section_break(user_info);
add_jbr_info(jbi, jbr->name, jbr);
if (jbr->name) {
/* TODO: Check whether it's correct to call prepend_pair_html,
or if we should be using prepend_pair_plaintext */
purple_notify_user_info_prepend_pair_html(user_info, _("Resource"), jbr->name);
}
}
}
if (!jbi->jb->resources) {
/* the buddy is offline */
gboolean is_domain = jabber_jid_is_domain(jbi->jid);
if (jbi->last_seconds > 0) {
char *last = purple_str_seconds_to_string(jbi->last_seconds);
gchar *message = NULL;
const gchar *title = NULL;
if (is_domain) {
title = _("Uptime");
message = last;
last = NULL;
} else {
title = _("Logged Off");
message = g_strdup_printf(_("%s ago"), last);
}
purple_notify_user_info_prepend_pair_plaintext(user_info, title, message);
g_free(last);
g_free(message);
}
if (!is_domain) {
gchar *status =
g_strdup_printf("%s%s%s", _("Offline"),
jbi->last_message ? ": " : "",
jbi->last_message ? jbi->last_message : "");
/* TODO: Check whether it's correct to call prepend_pair_html,
or if we should be using prepend_pair_plaintext */
purple_notify_user_info_prepend_pair_html(user_info, _("Status"), status);
g_free(status);
}
}
g_free(resource_name);
purple_notify_userinfo(jbi->js->gc, jbi->jid, user_info, NULL, NULL);
g_slist_free_full(jbi->vcard_images, g_object_unref);
jbi->js->pending_buddy_info_requests = g_slist_remove(jbi->js->pending_buddy_info_requests, jbi);
jabber_buddy_info_destroy(jbi);
}
static void jabber_buddy_info_remove_id(JabberBuddyInfo *jbi, const char *id)
{
GSList *l;
char *comp_id;
if(!id)
return;
l = g_slist_find_custom(jbi->ids, id, (GCompareFunc)g_strcmp0);
if(l) {
comp_id = l->data;
jbi->ids = g_slist_delete_link(jbi->ids, l);
g_free(comp_id);
}
}
static gboolean
set_own_vcard_cb(gpointer data)
{
JabberStream *js = data;
PurpleAccount *account = purple_connection_get_account(js->gc);
js->vcard_timer = 0;
jabber_set_info(js->gc, purple_account_get_user_info(account));
return FALSE;
}
static void jabber_vcard_save_mine(JabberStream *js, const char *from,
JabberIqType type, const char *id,
PurpleXmlNode *packet, gpointer data)
{
PurpleXmlNode *vcard, *photo, *binval;
char *txt, *vcard_hash = NULL;
PurpleAccount *account;
if (type == JABBER_IQ_ERROR) {
PurpleXmlNode *error;
purple_debug_warning("jabber", "Server returned error while retrieving vCard\n");
error = purple_xmlnode_get_child(packet, "error");
if (!error || !purple_xmlnode_get_child(error, "item-not-found"))
return;
}
account = purple_connection_get_account(js->gc);
if((vcard = purple_xmlnode_get_child(packet, "vCard")) ||
(vcard = purple_xmlnode_get_child_with_namespace(packet, "query", "vcard-temp")))
{
txt = purple_xmlnode_to_str(vcard, NULL);
purple_account_set_user_info(account, txt);
g_free(txt);
} else {
/* if we have no vCard, then lets not overwrite what we might have locally */
}
js->vcard_fetched = TRUE;
if (vcard && (photo = purple_xmlnode_get_child(vcard, "PHOTO")) &&
(binval = purple_xmlnode_get_child(photo, "BINVAL"))) {
gsize size;
char *bintext = purple_xmlnode_get_data(binval);
if (bintext) {
guchar *data = g_base64_decode(bintext, &size);
g_free(bintext);
if (data) {
vcard_hash = g_compute_checksum_for_data(
G_CHECKSUM_SHA1, data, size);
g_free(data);
}
}
}
/* Republish our vcard if the photo is different than the server's */
if (js->initial_avatar_hash && !purple_strequal(vcard_hash, js->initial_avatar_hash)) {
/*
* Google Talk has developed the behavior that it will not accept
* a vcard set in the first 10 seconds (or so) of the connection;
* it returns an error (namespaces trimmed):
* <error code="500" type="wait"><internal-server-error/></error>.
*/
if (js->googletalk)
js->vcard_timer = g_timeout_add_seconds(10, set_own_vcard_cb,
js);
else
jabber_set_info(js->gc, purple_account_get_user_info(account));
} else if (vcard_hash) {
/* A photo is in the vCard. Advertise its hash */
js->avatar_hash = vcard_hash;
vcard_hash = NULL;
/* Send presence to update vcard-temp:x:update */
jabber_presence_send(js, FALSE);
}
g_free(vcard_hash);
}
void jabber_vcard_fetch_mine(JabberStream *js)
{
JabberIq *iq = jabber_iq_new(js, JABBER_IQ_GET);
PurpleXmlNode *vcard = purple_xmlnode_new_child(iq->node, "vCard");
purple_xmlnode_set_namespace(vcard, "vcard-temp");
jabber_iq_set_callback(iq, jabber_vcard_save_mine, NULL);
jabber_iq_send(iq);
}
static void jabber_vcard_parse(JabberStream *js, const char *from,
JabberIqType type, const char *id,
PurpleXmlNode *packet, gpointer data)
{
char *bare_jid;
char *text;
char *serverside_alias = NULL;
PurpleXmlNode *vcard;
PurpleAccount *account;
JabberBuddyInfo *jbi = data;
PurpleNotifyUserInfo *user_info;
g_return_if_fail(jbi != NULL);
jabber_buddy_info_remove_id(jbi, id);
if (type == JABBER_IQ_ERROR) {
purple_debug_info("jabber", "Got error response for vCard\n");
jabber_buddy_info_show_if_ready(jbi);
return;
}
user_info = jbi->user_info;
account = purple_connection_get_account(js->gc);
bare_jid = jabber_get_bare_jid(from ? from : purple_account_get_username(account));
/* TODO: Is the query xmlns='vcard-temp' version of this still necessary? */
if((vcard = purple_xmlnode_get_child(packet, "vCard")) ||
(vcard = purple_xmlnode_get_child_with_namespace(packet, "query", "vcard-temp"))) {
PurpleXmlNode *child;
for(child = vcard->child; child; child = child->next)
{
PurpleXmlNode *child2;
if(child->type != PURPLE_XMLNODE_TYPE_TAG)
continue;
text = purple_xmlnode_get_data(child);
if(text && purple_strequal(child->name, "FN")) {
if (!serverside_alias)
serverside_alias = g_strdup(text);
purple_notify_user_info_add_pair_plaintext(user_info, _("Full Name"), text);
} else if(purple_strequal(child->name, "N")) {
for(child2 = child->child; child2; child2 = child2->next)
{
char *text2;
if(child2->type != PURPLE_XMLNODE_TYPE_TAG)
continue;
text2 = purple_xmlnode_get_data(child2);
if(text2 && purple_strequal(child2->name, "FAMILY")) {
purple_notify_user_info_add_pair_plaintext(user_info, _("Family Name"), text2);
} else if(text2 && purple_strequal(child2->name, "GIVEN")) {
purple_notify_user_info_add_pair_plaintext(user_info, _("Given Name"), text2);
} else if(text2 && purple_strequal(child2->name, "MIDDLE")) {
purple_notify_user_info_add_pair_plaintext(user_info, _("Middle Name"), text2);
}
g_free(text2);
}
} else if(text && purple_strequal(child->name, "NICKNAME")) {
/* Prefer the Nickcname to the Full Name as the serverside alias if it's not just part of the jid.
* Ignore it if it's part of the jid. */
if (strstr(bare_jid, text) == NULL) {
g_free(serverside_alias);
serverside_alias = g_strdup(text);
purple_notify_user_info_add_pair_plaintext(user_info, _("Nickname"), text);
}
} else if(text && purple_strequal(child->name, "BDAY")) {
purple_notify_user_info_add_pair_plaintext(user_info, _("Birthday"), text);
} else if(purple_strequal(child->name, "ADR")) {
gboolean address_line_added = FALSE;
for(child2 = child->child; child2; child2 = child2->next)
{
char *text2;
if(child2->type != PURPLE_XMLNODE_TYPE_TAG)
continue;
text2 = purple_xmlnode_get_data(child2);
if (text2 == NULL)
continue;
/* We do this here so that it's not added if all the child
* elements are empty. */
if (!address_line_added)
{
purple_notify_user_info_add_section_header(user_info, _("Address"));
address_line_added = TRUE;
}
if(purple_strequal(child2->name, "POBOX")) {
purple_notify_user_info_add_pair_plaintext(user_info, _("P.O. Box"), text2);
} else if (purple_strequal(child2->name, "EXTADD") || purple_strequal(child2->name, "EXTADR")) {
/*
* EXTADD is correct, EXTADR is generated by other
* clients. The next time someone reads this, remove
* EXTADR.
*/
purple_notify_user_info_add_pair_plaintext(user_info, _("Extended Address"), text2);
} else if(purple_strequal(child2->name, "STREET")) {
purple_notify_user_info_add_pair_plaintext(user_info, _("Street Address"), text2);
} else if(purple_strequal(child2->name, "LOCALITY")) {
purple_notify_user_info_add_pair_plaintext(user_info, _("Locality"), text2);
} else if(purple_strequal(child2->name, "REGION")) {
purple_notify_user_info_add_pair_plaintext(user_info, _("Region"), text2);
} else if(purple_strequal(child2->name, "PCODE")) {
purple_notify_user_info_add_pair_plaintext(user_info, _("Postal Code"), text2);
} else if(purple_strequal(child2->name, "CTRY")
|| purple_strequal(child2->name, "COUNTRY")) {
purple_notify_user_info_add_pair_plaintext(user_info, _("Country"), text2);
}
g_free(text2);
}
if (address_line_added)
purple_notify_user_info_add_section_break(user_info);
} else if(purple_strequal(child->name, "TEL")) {
char *number;
if((child2 = purple_xmlnode_get_child(child, "NUMBER"))) {
/* show what kind of number it is */
number = purple_xmlnode_get_data(child2);
if(number) {
purple_notify_user_info_add_pair_plaintext(user_info, _("Telephone"), number);
g_free(number);
}
} else if((number = purple_xmlnode_get_data(child))) {
/* lots of clients (including purple) do this, but it's
* out of spec */
purple_notify_user_info_add_pair_plaintext(user_info, _("Telephone"), number);
g_free(number);
}
} else if(purple_strequal(child->name, "EMAIL")) {
char *userid, *escaped;
if((child2 = purple_xmlnode_get_child(child, "USERID"))) {
/* show what kind of email it is */
userid = purple_xmlnode_get_data(child2);
if(userid) {
char *mailto;
escaped = g_markup_escape_text(userid, -1);
mailto = g_strdup_printf("<a href=\"mailto:%s\">%s</a>", escaped, escaped);
purple_notify_user_info_add_pair_html(user_info, _("Email"), mailto);
g_free(mailto);
g_free(escaped);
g_free(userid);
}
} else if((userid = purple_xmlnode_get_data(child))) {
/* lots of clients (including purple) do this, but it's
* out of spec */
char *mailto;
escaped = g_markup_escape_text(userid, -1);
mailto = g_strdup_printf("<a href=\"mailto:%s\">%s</a>", escaped, escaped);
purple_notify_user_info_add_pair_html(user_info, _("Email"), mailto);
g_free(mailto);
g_free(escaped);
g_free(userid);
}
} else if(purple_strequal(child->name, "ORG")) {
for(child2 = child->child; child2; child2 = child2->next)
{
char *text2;
if(child2->type != PURPLE_XMLNODE_TYPE_TAG)
continue;
text2 = purple_xmlnode_get_data(child2);
if(text2 && purple_strequal(child2->name, "ORGNAME")) {
purple_notify_user_info_add_pair_plaintext(user_info, _("Organization Name"), text2);
} else if(text2 && purple_strequal(child2->name, "ORGUNIT")) {
purple_notify_user_info_add_pair_plaintext(user_info, _("Organization Unit"), text2);
}
g_free(text2);
}
} else if(text && purple_strequal(child->name, "TITLE")) {
purple_notify_user_info_add_pair_plaintext(user_info, _("Job Title"), text);
} else if(text && purple_strequal(child->name, "ROLE")) {
purple_notify_user_info_add_pair_plaintext(user_info, _("Role"), text);
} else if(text && purple_strequal(child->name, "DESC")) {
purple_notify_user_info_add_pair_plaintext(user_info, _("Description"), text);
} else if(purple_strequal(child->name, "PHOTO") ||
purple_strequal(child->name, "LOGO")) {
char *bintext = NULL;
PurpleXmlNode *binval;
if ((binval = purple_xmlnode_get_child(child, "BINVAL")) &&
(bintext = purple_xmlnode_get_data(binval))) {
gsize size;
guchar *data;
gboolean photo = purple_strequal(child->name, "PHOTO");
data = g_base64_decode(bintext, &size);
if (data) {
PurpleImage *img;
guint img_id;
char *img_text;
char *hash;
img = purple_image_new_from_data(data, size);
img_id = purple_image_store_add(img);
jbi->vcard_images = g_slist_prepend(jbi->vcard_images, img);
img_text = g_strdup_printf("<img src='"
PURPLE_IMAGE_STORE_PROTOCOL "%u'>", img_id);
purple_notify_user_info_add_pair_html(user_info, (photo ? _("Photo") : _("Logo")), img_text);
hash = g_compute_checksum_for_data(G_CHECKSUM_SHA1, data, size);
purple_buddy_icons_set_for_user(account, bare_jid, data, size, hash);
g_free(hash);
g_free(img_text);
}
g_free(bintext);
}
}
g_free(text);
}
}
if (serverside_alias) {
PurpleBuddy *b;
/* If we found a serverside alias, set it and tell the core */
purple_serv_got_alias(js->gc, bare_jid, serverside_alias);
b = purple_blist_find_buddy(account, bare_jid);
if (b) {
purple_blist_node_set_string((PurpleBlistNode*)b, "servernick", serverside_alias);
}
g_free(serverside_alias);
}
g_free(bare_jid);
jabber_buddy_info_show_if_ready(jbi);
}
static void jabber_buddy_info_resource_free(gpointer data)
{
JabberBuddyInfoResource *jbri = data;
g_free(jbri);
}
static guint jbir_hash(gconstpointer v)
{
if (v)
return g_str_hash(v);
else
return 0;
}
static gboolean jbir_equal(gconstpointer v1, gconstpointer v2)
{
const gchar *resource_1 = v1;
const gchar *resource_2 = v2;
return purple_strequal(resource_1, resource_2);
}
static void jabber_version_parse(JabberStream *js, const char *from,
JabberIqType type, const char *id,
PurpleXmlNode *packet, gpointer data)
{
JabberBuddyInfo *jbi = data;
PurpleXmlNode *query;
char *resource_name;
g_return_if_fail(jbi != NULL);
jabber_buddy_info_remove_id(jbi, id);
if(!from)
return;
resource_name = jabber_get_resource(from);
if(resource_name) {
if (type == JABBER_IQ_RESULT) {
if((query = purple_xmlnode_get_child(packet, "query"))) {
JabberBuddyResource *jbr = jabber_buddy_find_resource(jbi->jb, resource_name);
if(jbr) {
PurpleXmlNode *node;
if((node = purple_xmlnode_get_child(query, "name"))) {
jbr->client.name = purple_xmlnode_get_data(node);
}
if((node = purple_xmlnode_get_child(query, "version"))) {
jbr->client.version = purple_xmlnode_get_data(node);
}
if((node = purple_xmlnode_get_child(query, "os"))) {
jbr->client.os = purple_xmlnode_get_data(node);
}
}
}
}
g_free(resource_name);
}
jabber_buddy_info_show_if_ready(jbi);
}
static void jabber_last_parse(JabberStream *js, const char *from,
JabberIqType type, const char *id,
PurpleXmlNode *packet, gpointer data)
{
JabberBuddyInfo *jbi = data;
PurpleXmlNode *query;
char *resource_name;
const char *seconds;
g_return_if_fail(jbi != NULL);
jabber_buddy_info_remove_id(jbi, id);
if(!from)
return;
resource_name = jabber_get_resource(from);
if(resource_name) {
if (type == JABBER_IQ_RESULT) {
if((query = purple_xmlnode_get_child(packet, "query"))) {
seconds = purple_xmlnode_get_attrib(query, "seconds");
if(seconds) {
char *end = NULL;
long sec = strtol(seconds, &end, 10);
JabberBuddy *jb = NULL;
char *resource = NULL;
char *buddy_name = NULL;
JabberBuddyResource *jbr = NULL;
if(end != seconds) {
JabberBuddyInfoResource *jbir = g_hash_table_lookup(jbi->resources, resource_name);
if(jbir) {
jbir->idle_seconds = sec;
}
}
/* Update the idle time of the buddy resource, if we got it.
This will correct the value when a server doesn't mark
delayed presence and we got the presence when signing on */
jb = jabber_buddy_find(js, from, FALSE);
if (jb) {
resource = jabber_get_resource(from);
buddy_name = jabber_get_bare_jid(from);
/* if the resource already has an idle time set, we
must have gotten it originally from a presence. In
this case we update it. Otherwise don't update it, to
avoid setting an idle and not getting informed about
the resource getting unidle */
if (resource && buddy_name) {
jbr = jabber_buddy_find_resource(jb, resource);
if (jbr) {
if (jbr->idle) {
if (sec) {
jbr->idle = time(NULL) - sec;
} else {
jbr->idle = 0;
}
if (jbr ==
jabber_buddy_find_resource(jb, NULL)) {
purple_protocol_got_user_idle(purple_connection_get_account(js->gc),
buddy_name, jbr->idle, jbr->idle);
}
}
}
}
g_free(resource);
g_free(buddy_name);
}
}
}
}
g_free(resource_name);
}
jabber_buddy_info_show_if_ready(jbi);
}
static void jabber_last_offline_parse(JabberStream *js, const char *from,
JabberIqType type, const char *id,
PurpleXmlNode *packet, gpointer data)
{
JabberBuddyInfo *jbi = data;
PurpleXmlNode *query;
const char *seconds;
g_return_if_fail(jbi != NULL);
jabber_buddy_info_remove_id(jbi, id);
if (type == JABBER_IQ_RESULT) {
if((query = purple_xmlnode_get_child(packet, "query"))) {
seconds = purple_xmlnode_get_attrib(query, "seconds");
if(seconds) {
char *end = NULL;
long sec = strtol(seconds, &end, 10);
if(end != seconds) {
jbi->last_seconds = sec;
}
}
jbi->last_message = purple_xmlnode_get_data(query);
}
}
jabber_buddy_info_show_if_ready(jbi);
}
static void jabber_time_parse(JabberStream *js, const char *from,
JabberIqType type, const char *id,
PurpleXmlNode *packet, gpointer data)
{
JabberBuddyInfo *jbi = data;
JabberBuddyResource *jbr;
char *resource_name;
g_return_if_fail(jbi != NULL);
jabber_buddy_info_remove_id(jbi, id);
if (!from)
return;
resource_name = jabber_get_resource(from);
jbr = resource_name ? jabber_buddy_find_resource(jbi->jb, resource_name) : NULL;
g_free(resource_name);
if (jbr) {
if (type == JABBER_IQ_RESULT) {
PurpleXmlNode *time = purple_xmlnode_get_child(packet, "time");
PurpleXmlNode *tzo = time ? purple_xmlnode_get_child(time, "tzo") : NULL;
char *tzo_data = tzo ? purple_xmlnode_get_data(tzo) : NULL;
if (tzo_data) {
char *c = tzo_data;
int hours, minutes;
if (tzo_data[0] == 'Z' && tzo_data[1] == '\0') {
jbr->tz_off = 0;
} else {
gboolean offset_positive = (tzo_data[0] == '+');
/* [+-]HH:MM */
if (((*c == '+' || *c == '-') && (c = c + 1)) &&
sscanf(c, "%02d:%02d", &hours, &minutes) == 2) {
jbr->tz_off = 60*60*hours + 60*minutes;
if (!offset_positive)
jbr->tz_off *= -1;
} else {
purple_debug_info("jabber", "Ignoring malformed timezone %s",
tzo_data);
}
}
g_free(tzo_data);
}
}
}
jabber_buddy_info_show_if_ready(jbi);
}
void jabber_buddy_remove_all_pending_buddy_info_requests(JabberStream *js)
{
g_slist_free_full(js->pending_buddy_info_requests, (GDestroyNotify)jabber_buddy_info_destroy);
js->pending_buddy_info_requests = NULL;
}
static gboolean jabber_buddy_get_info_timeout(gpointer data)
{
JabberBuddyInfo *jbi = data;
/* remove the pending callbacks */
while(jbi->ids) {
char *id = jbi->ids->data;
jabber_iq_remove_callback_by_id(jbi->js, id);
jbi->ids = g_slist_delete_link(jbi->ids, jbi->ids);
g_free(id);
}
jbi->js->pending_buddy_info_requests = g_slist_remove(jbi->js->pending_buddy_info_requests, jbi);
jbi->timeout_handle = 0;
jabber_buddy_info_show_if_ready(jbi);
return FALSE;
}
static gboolean _client_is_blacklisted(JabberBuddyResource *jbr, const char *ns)
{
/* can't be blacklisted if we don't know what you're running yet */
if(!jbr->client.name)
return FALSE;
if(purple_strequal(ns, NS_LAST_ACTIVITY)) {
if(purple_strequal(jbr->client.name, "Trillian")) {
/* verified by nwalp 2007/05/09 */
if(purple_strequal(jbr->client.version, "3.1.0.121") ||
/* verified by nwalp 2007/09/19 */
purple_strequal(jbr->client.version, "3.1.7.0")) {
return TRUE;
}
}
}
return FALSE;
}
static void
dispatch_queries_for_resource(JabberStream *js, JabberBuddyInfo *jbi,
gboolean is_bare_jid, const char *jid,
JabberBuddyResource *jbr)
{
JabberIq *iq;
JabberBuddyInfoResource *jbir;
char *full_jid = NULL;
const char *to;
if (is_bare_jid && jbr->name) {
full_jid = g_strdup_printf("%s/%s", jid, jbr->name);
to = full_jid;
} else
to = jid;
jbir = g_new0(JabberBuddyInfoResource, 1);
g_hash_table_insert(jbi->resources, g_strdup(jbr->name), jbir);
if(!jbr->client.name) {
iq = jabber_iq_new_query(js, JABBER_IQ_GET, "jabber:iq:version");
purple_xmlnode_set_attrib(iq->node, "to", to);
jabber_iq_set_callback(iq, jabber_version_parse, jbi);
jbi->ids = g_slist_prepend(jbi->ids, g_strdup(iq->id));
jabber_iq_send(iq);
}
/* this is to fix the feeling of irritation I get when trying
* to get info on a friend running Trillian, which doesn't
* respond (with an error or otherwise) to jabber:iq:last
* requests. There are a number of Trillian users in my
* office. */
if(!_client_is_blacklisted(jbr, NS_LAST_ACTIVITY)) {
iq = jabber_iq_new_query(js, JABBER_IQ_GET, NS_LAST_ACTIVITY);
purple_xmlnode_set_attrib(iq->node, "to", to);
jabber_iq_set_callback(iq, jabber_last_parse, jbi);
jbi->ids = g_slist_prepend(jbi->ids, g_strdup(iq->id));
jabber_iq_send(iq);
}
if (jbr->tz_off == PURPLE_NO_TZ_OFF &&
(!jbr->caps.info ||
jabber_resource_has_capability(jbr, NS_ENTITY_TIME))) {
PurpleXmlNode *child;
iq = jabber_iq_new(js, JABBER_IQ_GET);
purple_xmlnode_set_attrib(iq->node, "to", to);
child = purple_xmlnode_new_child(iq->node, "time");
purple_xmlnode_set_namespace(child, NS_ENTITY_TIME);
jabber_iq_set_callback(iq, jabber_time_parse, jbi);
jbi->ids = g_slist_prepend(jbi->ids, g_strdup(iq->id));
jabber_iq_send(iq);
}
g_free(full_jid);
}
static void jabber_buddy_get_info_for_jid(JabberStream *js, const char *jid)
{
JabberIq *iq;
PurpleXmlNode *vcard;
GList *resources;
JabberBuddy *jb;
JabberBuddyInfo *jbi;
const char *slash;
gboolean is_bare_jid;
jb = jabber_buddy_find(js, jid, TRUE);
/* invalid JID */
if(!jb)
return;
slash = strchr(jid, '/');
is_bare_jid = (slash == NULL);
jbi = g_new0(JabberBuddyInfo, 1);
jbi->jid = g_strdup(jid);
jbi->js = js;
jbi->jb = jb;
jbi->resources = g_hash_table_new_full(jbir_hash, jbir_equal, g_free, jabber_buddy_info_resource_free);
jbi->user_info = purple_notify_user_info_new();
iq = jabber_iq_new(js, JABBER_IQ_GET);
purple_xmlnode_set_attrib(iq->node, "to", jid);
vcard = purple_xmlnode_new_child(iq->node, "vCard");
purple_xmlnode_set_namespace(vcard, "vcard-temp");
jabber_iq_set_callback(iq, jabber_vcard_parse, jbi);
jbi->ids = g_slist_prepend(jbi->ids, g_strdup(iq->id));
jabber_iq_send(iq);
if (is_bare_jid) {
if (jb->resources) {
for(resources = jb->resources; resources; resources = resources->next) {
JabberBuddyResource *jbr = resources->data;
dispatch_queries_for_resource(js, jbi, is_bare_jid, jid, jbr);
}
} else {
/* user is offline, send a jabber:iq:last to find out last time online */
iq = jabber_iq_new_query(js, JABBER_IQ_GET, NS_LAST_ACTIVITY);
purple_xmlnode_set_attrib(iq->node, "to", jid);
jabber_iq_set_callback(iq, jabber_last_offline_parse, jbi);
jbi->ids = g_slist_prepend(jbi->ids, g_strdup(iq->id));
jabber_iq_send(iq);
}
} else {
JabberBuddyResource *jbr = jabber_buddy_find_resource(jb, slash + 1);
if (jbr)
dispatch_queries_for_resource(js, jbi, is_bare_jid, jid, jbr);
else
purple_debug_warning("jabber", "jabber_buddy_get_info_for_jid() "
"was passed JID %s, but there is no corresponding "
"JabberBuddyResource!\n", jid);
}
js->pending_buddy_info_requests = g_slist_prepend(js->pending_buddy_info_requests, jbi);
jbi->timeout_handle = g_timeout_add_seconds(30, jabber_buddy_get_info_timeout, jbi);
}
void jabber_buddy_get_info(PurpleConnection *gc, const char *who)
{
JabberStream *js = purple_connection_get_protocol_data(gc);
JabberID *jid = jabber_id_new(who);
if (!jid)
return;
if (jid->node && jabber_chat_find(js, jid->node, jid->domain)) {
/* For a conversation, include the resource (indicates the user). */
jabber_buddy_get_info_for_jid(js, who);
} else {
char *bare_jid = jabber_get_bare_jid(who);
jabber_buddy_get_info_for_jid(js, bare_jid);
g_free(bare_jid);
}
jabber_id_free(jid);
}
static void jabber_buddy_set_invisibility(JabberStream *js, const char *who,
gboolean invisible)
{
PurplePresence *gpresence;
PurpleAccount *account;
PurpleStatus *status;
JabberBuddy *jb = jabber_buddy_find(js, who, TRUE);
PurpleXmlNode *presence;
JabberBuddyState state;
char *msg;
int priority;
account = purple_connection_get_account(js->gc);
gpresence = purple_account_get_presence(account);
status = purple_presence_get_active_status(gpresence);
purple_status_to_jabber(status, &state, &msg, &priority);
presence = jabber_presence_create_js(js, state, msg, priority);
g_free(msg);
purple_xmlnode_set_attrib(presence, "to", who);
if(invisible) {
purple_xmlnode_set_attrib(presence, "type", "invisible");
jb->invisible |= JABBER_INVIS_BUDDY;
} else {
jb->invisible &= ~JABBER_INVIS_BUDDY;
}
jabber_send(js, presence);
purple_xmlnode_free(presence);
}
static void jabber_buddy_make_invisible(PurpleBlistNode *node, gpointer data)
{
PurpleBuddy *buddy;
PurpleConnection *gc;
JabberStream *js;
g_return_if_fail(PURPLE_IS_BUDDY(node));
buddy = (PurpleBuddy *) node;
gc = purple_account_get_connection(purple_buddy_get_account(buddy));
js = purple_connection_get_protocol_data(gc);
jabber_buddy_set_invisibility(js, purple_buddy_get_name(buddy), TRUE);
}
static void jabber_buddy_make_visible(PurpleBlistNode *node, gpointer data)
{
PurpleBuddy *buddy;
PurpleConnection *gc;
JabberStream *js;
g_return_if_fail(PURPLE_IS_BUDDY(node));
buddy = (PurpleBuddy *) node;
gc = purple_account_get_connection(purple_buddy_get_account(buddy));
js = purple_connection_get_protocol_data(gc);
jabber_buddy_set_invisibility(js, purple_buddy_get_name(buddy), FALSE);
}
static void cancel_presence_notification(gpointer data)
{
PurpleBuddy *buddy;
PurpleConnection *gc;
JabberStream *js;
buddy = data;
gc = purple_account_get_connection(purple_buddy_get_account(buddy));
js = purple_connection_get_protocol_data(gc);
jabber_presence_subscription_set(js, purple_buddy_get_name(buddy), "unsubscribed");
}
static void
jabber_buddy_cancel_presence_notification(PurpleBlistNode *node,
gpointer data)
{
PurpleBuddy *buddy;
PurpleAccount *account;
PurpleConnection *gc;
const gchar *name;
char *msg;
g_return_if_fail(PURPLE_IS_BUDDY(node));
buddy = (PurpleBuddy *) node;
name = purple_buddy_get_name(buddy);
account = purple_buddy_get_account(buddy);
gc = purple_account_get_connection(account);
msg = g_strdup_printf(_("%s will no longer be able to see your status "
"updates. Do you want to continue?"), name);
purple_request_yes_no(gc, NULL, _("Cancel Presence Notification"),
msg, 0 /* Yes */, purple_request_cpar_from_account(account), buddy,
cancel_presence_notification, NULL /* Do nothing */);
g_free(msg);
}
static void jabber_buddy_rerequest_auth(PurpleBlistNode *node, gpointer data)
{
PurpleBuddy *buddy;
PurpleConnection *gc;
JabberStream *js;
g_return_if_fail(PURPLE_IS_BUDDY(node));
buddy = (PurpleBuddy *) node;
gc = purple_account_get_connection(purple_buddy_get_account(buddy));
js = purple_connection_get_protocol_data(gc);
jabber_presence_subscription_set(js, purple_buddy_get_name(buddy), "subscribe");
}
static void jabber_buddy_unsubscribe(PurpleBlistNode *node, gpointer data)
{
PurpleBuddy *buddy;
PurpleConnection *gc;
JabberStream *js;
g_return_if_fail(PURPLE_IS_BUDDY(node));
buddy = (PurpleBuddy *) node;
gc = purple_account_get_connection(purple_buddy_get_account(buddy));
js = purple_connection_get_protocol_data(gc);
jabber_presence_subscription_set(js, purple_buddy_get_name(buddy), "unsubscribe");
}
static void jabber_buddy_login(PurpleBlistNode *node, gpointer data) {
if(PURPLE_IS_BUDDY(node)) {
/* simply create a directed presence of the current status */
PurpleBuddy *buddy = (PurpleBuddy *) node;
PurpleConnection *gc = purple_account_get_connection(purple_buddy_get_account(buddy));
JabberStream *js = purple_connection_get_protocol_data(gc);
PurpleAccount *account = purple_connection_get_account(gc);
PurplePresence *gpresence = purple_account_get_presence(account);
PurpleStatus *status = purple_presence_get_active_status(gpresence);
PurpleXmlNode *presence;
JabberBuddyState state;
char *msg;
int priority;
purple_status_to_jabber(status, &state, &msg, &priority);
presence = jabber_presence_create_js(js, state, msg, priority);
g_free(msg);
purple_xmlnode_set_attrib(presence, "to", purple_buddy_get_name(buddy));
jabber_send(js, presence);
purple_xmlnode_free(presence);
}
}
static void jabber_buddy_logout(PurpleBlistNode *node, gpointer data) {
if(PURPLE_IS_BUDDY(node)) {
/* simply create a directed unavailable presence */
PurpleBuddy *buddy = (PurpleBuddy *) node;
PurpleConnection *gc = purple_account_get_connection(purple_buddy_get_account(buddy));
JabberStream *js = purple_connection_get_protocol_data(gc);
PurpleXmlNode *presence;
presence = jabber_presence_create_js(js, JABBER_BUDDY_STATE_UNAVAILABLE, NULL, 0);
purple_xmlnode_set_attrib(presence, "to", purple_buddy_get_name(buddy));
jabber_send(js, presence);
purple_xmlnode_free(presence);
}
}
static GList *jabber_buddy_menu(PurpleBuddy *buddy)
{
PurpleConnection *gc = purple_account_get_connection(purple_buddy_get_account(buddy));
JabberStream *js = purple_connection_get_protocol_data(gc);
const char *name = purple_buddy_get_name(buddy);
JabberBuddy *jb = jabber_buddy_find(js, name, TRUE);
GList *jbrs;
GList *m = NULL;
PurpleActionMenu *act;
if(!jb)
return m;
if (js->protocol_version.major == 0 && js->protocol_version.minor == 9 &&
jb != js->user_jb) {
if(jb->invisible & JABBER_INVIS_BUDDY) {
act = purple_action_menu_new(_("Un-hide From"),
PURPLE_CALLBACK(jabber_buddy_make_visible),
NULL, NULL);
} else {
act = purple_action_menu_new(_("Temporarily Hide From"),
PURPLE_CALLBACK(jabber_buddy_make_invisible),
NULL, NULL);
}
m = g_list_append(m, act);
}
if(jb->subscription & JABBER_SUB_FROM && jb != js->user_jb) {
act = purple_action_menu_new(_("Cancel Presence Notification"),
PURPLE_CALLBACK(jabber_buddy_cancel_presence_notification),
NULL, NULL);
m = g_list_append(m, act);
}
if(!(jb->subscription & JABBER_SUB_TO)) {
act = purple_action_menu_new(_("(Re-)Request authorization"),
PURPLE_CALLBACK(jabber_buddy_rerequest_auth),
NULL, NULL);
m = g_list_append(m, act);
} else if (jb != js->user_jb) {
/* shouldn't this just happen automatically when the buddy is
removed? */
act = purple_action_menu_new(_("Unsubscribe"),
PURPLE_CALLBACK(jabber_buddy_unsubscribe),
NULL, NULL);
m = g_list_append(m, act);
}
if (js->googletalk) {
act = purple_action_menu_new(_("Initiate _Chat"),
PURPLE_CALLBACK(google_buddy_node_chat),
NULL, NULL);
m = g_list_append(m, act);
}
/*
* This if-condition implements parts of XEP-0100: Gateway Interaction
*
* According to stpeter, there is no way to know if a jid on the roster is a gateway without sending a disco#info.
* However, since the gateway might appear offline to us, we cannot get that information. Therefore, I just assume
* that gateways on the roster can be identified by having no '@' in their jid. This is a faily safe assumption, since
* people don't tend to have a server or other service there.
*
* TODO: Use disco#info...
*/
if (strchr(name, '@') == NULL) {
act = purple_action_menu_new(_("Log In"),
PURPLE_CALLBACK(jabber_buddy_login),
NULL, NULL);
m = g_list_append(m, act);
act = purple_action_menu_new(_("Log Out"),
PURPLE_CALLBACK(jabber_buddy_logout),
NULL, NULL);
m = g_list_append(m, act);
}
/* add all ad hoc commands to the action menu */
for(jbrs = jb->resources; jbrs; jbrs = g_list_next(jbrs)) {
JabberBuddyResource *jbr = jbrs->data;
GList *commands;
if (!jbr->commands)
continue;
for(commands = jbr->commands; commands; commands = g_list_next(commands)) {
JabberAdHocCommands *cmd = commands->data;
act = purple_action_menu_new(cmd->name, PURPLE_CALLBACK(jabber_adhoc_execute_action), cmd, NULL);
m = g_list_append(m, act);
}
}
return m;
}
GList *
jabber_blist_node_menu(PurpleBlistNode *node)
{
if(PURPLE_IS_BUDDY(node)) {
return jabber_buddy_menu((PurpleBuddy *) node);
} else {
return NULL;
}
}
static void user_search_result_add_buddy_cb(PurpleConnection *gc, GList *row, void *user_data)
{
/* XXX find out the jid */
purple_blist_request_add_buddy(purple_connection_get_account(gc),
g_list_nth_data(row, 0), NULL, NULL);
}
static void user_search_result_cb(JabberStream *js, const char *from,
JabberIqType type, const char *id,
PurpleXmlNode *packet, gpointer data)
{
PurpleNotifySearchResults *results;
PurpleNotifySearchColumn *column;
PurpleXmlNode *x, *query, *item, *field;
/* XXX error checking? */
if(!(query = purple_xmlnode_get_child(packet, "query")))
return;
results = purple_notify_searchresults_new();
if((x = purple_xmlnode_get_child_with_namespace(query, "x", "jabber:x:data"))) {
PurpleXmlNode *reported;
GSList *column_vars = NULL;
purple_debug_info("jabber", "new-skool\n");
if((reported = purple_xmlnode_get_child(x, "reported"))) {
PurpleXmlNode *field = purple_xmlnode_get_child(reported, "field");
while(field) {
const char *var = purple_xmlnode_get_attrib(field, "var");
const char *label = purple_xmlnode_get_attrib(field, "label");
if(var) {
column = purple_notify_searchresults_column_new(label ? label : var);
purple_notify_searchresults_column_add(results, column);
column_vars = g_slist_append(column_vars, (char *)var);
}
field = purple_xmlnode_get_next_twin(field);
}
}
item = purple_xmlnode_get_child(x, "item");
while(item) {
GList *row = NULL;
GSList *l;
PurpleXmlNode *valuenode;
const char *var;
for (l = column_vars; l != NULL; l = l->next) {
/*
* Build a row containing the strings that correspond
* to each column of the search results.
*/
for (field = purple_xmlnode_get_child(item, "field");
field != NULL;
field = purple_xmlnode_get_next_twin(field))
{
if ((var = purple_xmlnode_get_attrib(field, "var")) &&
purple_strequal(var, l->data) &&
(valuenode = purple_xmlnode_get_child(field, "value")))
{
char *value = purple_xmlnode_get_data(valuenode);
row = g_list_append(row, value);
break;
}
}
if (field == NULL)
/* No data for this column */
row = g_list_append(row, NULL);
}
purple_notify_searchresults_row_add(results, row);
item = purple_xmlnode_get_next_twin(item);
}
g_slist_free(column_vars);
} else {
/* old skool */
purple_debug_info("jabber", "old-skool\n");
column = purple_notify_searchresults_column_new(_("JID"));
purple_notify_searchresults_column_add(results, column);
column = purple_notify_searchresults_column_new(_("First Name"));
purple_notify_searchresults_column_add(results, column);
column = purple_notify_searchresults_column_new(_("Last Name"));
purple_notify_searchresults_column_add(results, column);
column = purple_notify_searchresults_column_new(_("Nickname"));
purple_notify_searchresults_column_add(results, column);
column = purple_notify_searchresults_column_new(_("Email"));
purple_notify_searchresults_column_add(results, column);
for(item = purple_xmlnode_get_child(query, "item"); item; item = purple_xmlnode_get_next_twin(item)) {
const char *jid;
PurpleXmlNode *node;
GList *row = NULL;
if(!(jid = purple_xmlnode_get_attrib(item, "jid")))
continue;
row = g_list_append(row, g_strdup(jid));
node = purple_xmlnode_get_child(item, "first");
row = g_list_append(row, node ? purple_xmlnode_get_data(node) : NULL);
node = purple_xmlnode_get_child(item, "last");
row = g_list_append(row, node ? purple_xmlnode_get_data(node) : NULL);
node = purple_xmlnode_get_child(item, "nick");
row = g_list_append(row, node ? purple_xmlnode_get_data(node) : NULL);
node = purple_xmlnode_get_child(item, "email");
row = g_list_append(row, node ? purple_xmlnode_get_data(node) : NULL);
purple_debug_info("jabber", "row=%p\n", row);
purple_notify_searchresults_row_add(results, row);
}
}
purple_notify_searchresults_button_add(results, PURPLE_NOTIFY_BUTTON_ADD,
user_search_result_add_buddy_cb);
purple_notify_searchresults(js->gc, NULL, NULL, _("The following are the results of your search"), results, NULL, NULL);
}
static void user_search_x_data_cb(JabberStream *js, PurpleXmlNode *result, gpointer data)
{
PurpleXmlNode *query;
JabberIq *iq;
char *dir_server = data;
const char *type;
/* if they've cancelled the search, we're
* just going to get an error if we send
* a cancel, so skip it */
type = purple_xmlnode_get_attrib(result, "type");
if(purple_strequal(type, "cancel")) {
g_free(dir_server);
return;
}
iq = jabber_iq_new_query(js, JABBER_IQ_SET, "jabber:iq:search");
query = purple_xmlnode_get_child(iq->node, "query");
purple_xmlnode_insert_child(query, result);
jabber_iq_set_callback(iq, user_search_result_cb, NULL);
purple_xmlnode_set_attrib(iq->node, "to", dir_server);
jabber_iq_send(iq);
g_free(dir_server);
}
struct user_search_info {
JabberStream *js;
char *directory_server;
};
static void user_search_cancel_cb(struct user_search_info *usi, PurpleRequestFields *fields)
{
g_free(usi->directory_server);
g_free(usi);
}
static void user_search_cb(struct user_search_info *usi, PurpleRequestFields *fields)
{
JabberStream *js = usi->js;
JabberIq *iq;
PurpleXmlNode *query;
GList *groups, *flds;
iq = jabber_iq_new_query(js, JABBER_IQ_SET, "jabber:iq:search");
query = purple_xmlnode_get_child(iq->node, "query");
for(groups = purple_request_fields_get_groups(fields); groups; groups = groups->next) {
for(flds = purple_request_field_group_get_fields(groups->data);
flds; flds = flds->next) {
PurpleRequestField *field = flds->data;
const char *id = purple_request_field_get_id(field);
const char *value = purple_request_field_string_get_value(field);
if(value && (purple_strequal(id, "first") || purple_strequal(id, "last") || purple_strequal(id, "nick") || purple_strequal(id, "email"))) {
PurpleXmlNode *y = purple_xmlnode_new_child(query, id);
purple_xmlnode_insert_data(y, value, -1);
}
}
}
jabber_iq_set_callback(iq, user_search_result_cb, NULL);
purple_xmlnode_set_attrib(iq->node, "to", usi->directory_server);
jabber_iq_send(iq);
g_free(usi->directory_server);
g_free(usi);
}
static void user_search_fields_result_cb(JabberStream *js, const char *from,
JabberIqType type, const char *id,
PurpleXmlNode *packet, gpointer data)
{
PurpleXmlNode *query, *x;
if (!from)
return;
if (type == JABBER_IQ_ERROR) {
char *msg = jabber_parse_error(js, packet, NULL);
if(!msg)
msg = g_strdup(_("Unknown error"));
purple_notify_error(js->gc, _("Directory Query Failed"),
_("Could not query the directory server."), msg,
purple_request_cpar_from_connection(js->gc));
g_free(msg);
return;
}
if(!(query = purple_xmlnode_get_child(packet, "query")))
return;
if((x = purple_xmlnode_get_child_with_namespace(query, "x", "jabber:x:data"))) {
jabber_x_data_request(js, x, user_search_x_data_cb, g_strdup(from));
return;
} else {
struct user_search_info *usi;
PurpleXmlNode *instnode;
char *instructions = NULL;
PurpleRequestFields *fields;
PurpleRequestFieldGroup *group;
PurpleRequestField *field;
/* old skool */
fields = purple_request_fields_new();
group = purple_request_field_group_new(NULL);
purple_request_fields_add_group(fields, group);
if((instnode = purple_xmlnode_get_child(query, "instructions")))
{
char *tmp = purple_xmlnode_get_data(instnode);
if(tmp)
{
/* Try to translate the message (see static message
list in jabber_user_dir_comments[]) */
instructions = g_strdup_printf(_("Server Instructions: %s"), _(tmp));
g_free(tmp);
}
}
if(!instructions)
{
instructions = g_strdup(_("Fill in one or more fields to search "
"for any matching XMPP users."));
}
if(purple_xmlnode_get_child(query, "first")) {
field = purple_request_field_string_new("first", _("First Name"),
NULL, FALSE);
purple_request_field_group_add_field(group, field);
}
if(purple_xmlnode_get_child(query, "last")) {
field = purple_request_field_string_new("last", _("Last Name"),
NULL, FALSE);
purple_request_field_group_add_field(group, field);
}
if(purple_xmlnode_get_child(query, "nick")) {
field = purple_request_field_string_new("nick", _("Nickname"),
NULL, FALSE);
purple_request_field_group_add_field(group, field);
}
if(purple_xmlnode_get_child(query, "email")) {
field = purple_request_field_string_new("email", _("Email Address"),
NULL, FALSE);
purple_request_field_group_add_field(group, field);
}
usi = g_new0(struct user_search_info, 1);
usi->js = js;
usi->directory_server = g_strdup(from);
purple_request_fields(js->gc, _("Search for XMPP users"),
_("Search for XMPP users"), instructions, fields,
_("Search"), G_CALLBACK(user_search_cb),
_("Cancel"), G_CALLBACK(user_search_cancel_cb),
purple_request_cpar_from_connection(js->gc),
usi);
g_free(instructions);
}
}
void jabber_user_search(JabberStream *js, const char *directory)
{
JabberIq *iq;
/* XXX: should probably better validate the directory we're given */
if(!directory || !*directory) {
purple_notify_error(js->gc, _("Invalid Directory"),
_("Invalid Directory"), NULL,
purple_request_cpar_from_connection(js->gc));
return;
}
/* If the value provided isn't the disco#info default, persist it. Otherwise,
make sure we aren't persisting an old value */
if(js->user_directories && js->user_directories->data &&
purple_strequal(directory, js->user_directories->data)) {
purple_account_set_string(purple_connection_get_account(js->gc), "user_directory", "");
}
else {
purple_account_set_string(purple_connection_get_account(js->gc), "user_directory", directory);
}
iq = jabber_iq_new_query(js, JABBER_IQ_GET, "jabber:iq:search");
purple_xmlnode_set_attrib(iq->node, "to", directory);
jabber_iq_set_callback(iq, user_search_fields_result_cb, NULL);
jabber_iq_send(iq);
}
void jabber_user_search_begin(PurpleProtocolAction *action)
{
PurpleConnection *gc = (PurpleConnection *) action->connection;
JabberStream *js = purple_connection_get_protocol_data(gc);
const char *def_val = purple_account_get_string(purple_connection_get_account(js->gc), "user_directory", "");
if(!*def_val && js->user_directories)
def_val = js->user_directories->data;
purple_request_input(gc, _("Enter a User Directory"), _("Enter a User Directory"),
_("Select a user directory to search"),
def_val,
FALSE, FALSE, NULL,
_("Search Directory"), PURPLE_CALLBACK(jabber_user_search),
_("Cancel"), NULL,
NULL, js);
}
gboolean
jabber_resource_know_capabilities(const JabberBuddyResource *jbr)
{
return jbr->caps.info != NULL;
}
gboolean
jabber_resource_has_capability(const JabberBuddyResource *jbr, const gchar *cap)
{
const GList *node = NULL;
const JabberCapsNodeExts *exts;
if (!jbr->caps.info) {
purple_debug_info("jabber",
"Unable to find caps: nothing known about buddy\n");
return FALSE;
}
node = g_list_find_custom(jbr->caps.info->features, cap, (GCompareFunc)strcmp);
if (!node && jbr->caps.exts && jbr->caps.info->exts) {
const GList *ext;
exts = jbr->caps.info->exts;
/* Walk through all the enabled caps, checking each list for the cap.
* Don't check it twice, though. */
for (ext = jbr->caps.exts; ext && !node; ext = ext->next) {
GList *features = g_hash_table_lookup(exts->exts, ext->data);
if (features)
node = g_list_find_custom(features, cap, (GCompareFunc)strcmp);
}
}
return (node != NULL);
}
gboolean
jabber_buddy_has_capability(const JabberBuddy *jb, const gchar *cap)
{
JabberBuddyResource *jbr = jabber_buddy_find_resource((JabberBuddy*)jb, NULL);
if (!jbr) {
purple_debug_info("jabber",
"Unable to find caps: buddy might be offline\n");
return FALSE;
}
return jabber_resource_has_capability(jbr, cap);
}
const gchar *
jabber_resource_get_identity_category_type(const JabberBuddyResource *jbr,
const gchar *category)
{
const GList *iter = NULL;
if (jbr->caps.info) {
for (iter = jbr->caps.info->identities ; iter ; iter = g_list_next(iter)) {
const JabberIdentity *identity =
(JabberIdentity *) iter->data;
if (purple_strequal(identity->category, category)) {
return identity->type;
}
}
}
return NULL;
}