pidgin/pidgin

closing merged branch
port-changes-from-branch-2.x.y-to-default
2020-02-03, Gary Kramlich
2f836435c33c
closing merged branch
/* purple
*
* 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.
*
* Rewritten from scratch during Google Summer of Code 2012
* by Tomek Wasilczyk (http://www.wasilczyk.pl).
*
* Previously implemented by:
* - Arkadiusz Miskiewicz <misiek@pld.org.pl> - first implementation (2001);
* - Bartosz Oler <bartosz@bzimage.us> - reimplemented during GSoC 2005;
* - Krzysztof Klinikowski <grommasher@gmail.com> - some parts (2009-2011).
*
* 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 "roster.h"
#include "gg.h"
#include "xml.h"
#include "utils.h"
#include "purplew.h"
#include <debug.h>
#define GGP_ROSTER_SYNC_SETT "gg-synchronized"
#define GGP_ROSTER_DEBUG 0
#define GGP_ROSTER_GROUPID_DEFAULT "00000000-0000-0000-0000-000000000000"
#define GGP_ROSTER_GROUPID_BOTS "0b345af6-0001-0000-0000-000000000004"
/* TODO: ignored contacts synchronization (?) */
typedef struct
{
int version;
PurpleXmlNode *xml;
PurpleXmlNode *groups_node, *contacts_node;
/**
* Key: (uin_t) user identifier
* Value: (PurpleXmlNode*) xml node for contact
*/
GHashTable *contact_nodes;
/**
* Key: (gchar*) group id
* Value: (PurpleXmlNode*) xml node for group
*/
GHashTable *group_nodes;
/**
* Key: (gchar*) group name
* Value: (gchar*) group id
*/
GHashTable *group_ids;
/**
* Key: (gchar*) group id
* Value: (gchar*) group name
*/
GHashTable *group_names;
gchar *bots_group_id;
gboolean needs_update;
} ggp_roster_content;
typedef struct
{
enum
{
GGP_ROSTER_CHANGE_CONTACT_UPDATE,
GGP_ROSTER_CHANGE_CONTACT_REMOVE,
GGP_ROSTER_CHANGE_GROUP_RENAME,
} type;
union
{
uin_t uin;
struct
{
gchar *old_name;
gchar *new_name;
} group_rename;
} data;
} ggp_roster_change;
static inline ggp_roster_session_data *
ggp_roster_get_rdata(PurpleConnection *gc);
static void ggp_roster_content_free(ggp_roster_content *content);
static void ggp_roster_change_free(gpointer change);
static int ggp_roster_get_version(PurpleConnection *gc);
static gboolean ggp_roster_timer_cb(gpointer _gc);
#if GGP_ROSTER_DEBUG
static void ggp_roster_dump(ggp_roster_content *content);
#endif
/* synchronization control */
static gboolean ggp_roster_is_synchronized(PurpleBuddy *buddy);
static void ggp_roster_set_synchronized(PurpleConnection *gc,
PurpleBuddy *buddy, gboolean synchronized);
/* buddy list import */
static gboolean ggp_roster_reply_list_read_group(PurpleXmlNode *node,
ggp_roster_content *content);
static gboolean ggp_roster_reply_list_read_buddy(PurpleConnection *gc,
PurpleXmlNode *node, ggp_roster_content *content, GHashTable *remove_buddies);
static void ggp_roster_reply_list(PurpleConnection *gc, uint32_t version,
const char *reply);
/* buddy list export */
static const gchar * ggp_roster_send_update_group_add(
ggp_roster_content *content, PurpleGroup *group);
static gboolean ggp_roster_send_update_contact_update(PurpleConnection *gc,
ggp_roster_change *change);
static gboolean ggp_roster_send_update_contact_remove(PurpleConnection *gc,
ggp_roster_change *change);
static gboolean ggp_roster_send_update_group_rename(PurpleConnection *gc,
ggp_roster_change *change);
static void ggp_roster_send_update(PurpleConnection *gc);
static void ggp_roster_reply_ack(PurpleConnection *gc, uint32_t version);
static void ggp_roster_reply_reject(PurpleConnection *gc, uint32_t version);
/******************************************************************************/
static inline ggp_roster_session_data *
ggp_roster_get_rdata(PurpleConnection *gc)
{
GGPInfo *accdata = purple_connection_get_protocol_data(gc);
return &accdata->roster_data;
}
static void ggp_roster_content_free(ggp_roster_content *content)
{
if (content == NULL)
return;
if (content->xml)
purple_xmlnode_free(content->xml);
if (content->contact_nodes)
g_hash_table_destroy(content->contact_nodes);
if (content->group_nodes)
g_hash_table_destroy(content->group_nodes);
if (content->group_ids)
g_hash_table_destroy(content->group_ids);
if (content->group_names)
g_hash_table_destroy(content->group_names);
g_free(content->bots_group_id);
g_free(content);
}
static void ggp_roster_change_free(gpointer _change)
{
ggp_roster_change *change = _change;
if (change->type == GGP_ROSTER_CHANGE_GROUP_RENAME) {
g_free(change->data.group_rename.old_name);
g_free(change->data.group_rename.new_name);
}
g_free(change);
}
static int ggp_roster_get_version(PurpleConnection *gc)
{
ggp_roster_content *content = ggp_roster_get_rdata(gc)->content;
if (content == NULL)
return 0;
return content->version;
}
static gboolean ggp_roster_timer_cb(gpointer _gc)
{
PurpleConnection *gc = _gc;
PURPLE_ASSERT_CONNECTION_IS_VALID(gc);
ggp_roster_send_update(gc);
return TRUE;
}
#if GGP_ROSTER_DEBUG
static void ggp_roster_dump(ggp_roster_content *content)
{
char *str;
int len;
g_return_if_fail(content != NULL);
g_return_if_fail(content->xml != NULL);
str = purple_xmlnode_to_formatted_str(content->xml, &len);
purple_debug_misc("gg", "ggp_roster_dump: [%s]\n", str);
g_free(str);
}
#endif
/*******************************************************************************
* Setup.
******************************************************************************/
gboolean ggp_roster_enabled(void)
{
static gboolean checked = FALSE;
static gboolean enabled;
if (!checked) {
enabled = gg_libgadu_check_feature(
GG_LIBGADU_FEATURE_USERLIST100);
checked = TRUE;
}
return enabled;
}
void ggp_roster_setup(PurpleConnection *gc)
{
ggp_roster_session_data *rdata = ggp_roster_get_rdata(gc);
rdata->content = NULL;
rdata->sent_updates = NULL;
rdata->pending_updates = NULL;
rdata->timer = 0;
rdata->is_updating = FALSE;
if (ggp_roster_enabled())
rdata->timer = g_timeout_add_seconds(2,
ggp_roster_timer_cb, gc);
}
void ggp_roster_cleanup(PurpleConnection *gc)
{
ggp_roster_session_data *rdata = ggp_roster_get_rdata(gc);
if (rdata->timer)
g_source_remove(rdata->timer);
ggp_roster_content_free(rdata->content);
g_list_free_full(rdata->sent_updates, ggp_roster_change_free);
g_list_free_full(rdata->pending_updates, ggp_roster_change_free);
}
/*******************************************************************************
* Synchronization control.
******************************************************************************/
static gboolean ggp_roster_is_synchronized(PurpleBuddy *buddy)
{
gboolean ret = purple_blist_node_get_bool(PURPLE_BLIST_NODE(buddy),
GGP_ROSTER_SYNC_SETT);
return ret;
}
static void ggp_roster_set_synchronized(PurpleConnection *gc,
PurpleBuddy *buddy, gboolean synchronized)
{
ggp_roster_session_data *rdata = ggp_roster_get_rdata(gc);
uin_t uin = ggp_str_to_uin(purple_buddy_get_name(buddy));
ggp_roster_change *change;
purple_blist_node_set_bool(PURPLE_BLIST_NODE(buddy),
GGP_ROSTER_SYNC_SETT, synchronized);
if (!synchronized) {
change = g_new0(ggp_roster_change, 1);
change->type = GGP_ROSTER_CHANGE_CONTACT_UPDATE;
change->data.uin = uin;
rdata->pending_updates =
g_list_append(rdata->pending_updates, change);
}
}
void ggp_roster_request_update(PurpleConnection *gc)
{
GGPInfo *accdata = purple_connection_get_protocol_data(gc);
int local_version = ggp_roster_get_version(gc);
if (!ggp_roster_enabled()) {
purple_debug_warning("gg", "ggp_roster_request_update: "
"feature disabled\n");
return;
}
purple_debug_info("gg", "ggp_roster_request_update: local=%u\n",
local_version);
gg_userlist100_request(accdata->session, GG_USERLIST100_GET,
local_version, GG_USERLIST100_FORMAT_TYPE_GG100, NULL);
}
/*******************************************************************************
* Libgadu callbacks.
******************************************************************************/
void ggp_roster_reply(PurpleConnection *gc,
struct gg_event_userlist100_reply *reply)
{
if (GG_USERLIST100_FORMAT_TYPE_GG100 != reply->format_type) {
purple_debug_warning("gg", "ggp_roster_reply: "
"unsupported format type (%x)\n", reply->format_type);
return;
}
if (reply->type == GG_USERLIST100_REPLY_LIST)
ggp_roster_reply_list(gc, reply->version, reply->reply);
else if (reply->type == 0x01) /* list up to date (TODO: push to libgadu) */
purple_debug_info("gg", "ggp_roster_reply: list up to date\n");
else if (reply->type == GG_USERLIST100_REPLY_ACK)
ggp_roster_reply_ack(gc, reply->version);
else if (reply->type == GG_USERLIST100_REPLY_REJECT)
ggp_roster_reply_reject(gc, reply->version);
else
purple_debug_error("gg", "ggp_roster_reply: "
"unsupported reply (%x)\n", reply->type);
}
void ggp_roster_version(PurpleConnection *gc,
struct gg_event_userlist100_version *version)
{
int local_version = ggp_roster_get_version(gc);
int remote_version = version->version;
purple_debug_info("gg", "ggp_roster_version: local=%u, remote=%u\n",
local_version, remote_version);
if (local_version < remote_version)
ggp_roster_request_update(gc);
}
/*******************************************************************************
* Libpurple callbacks.
******************************************************************************/
void ggp_roster_alias_buddy(PurpleConnection *gc, const char *who,
const char *alias)
{
PurpleBuddy *buddy;
g_return_if_fail(who != NULL);
if (!ggp_roster_enabled())
return;
purple_debug_misc("gg", "ggp_roster_alias_buddy(\"%s\", \"%s\")\n",
who, alias);
buddy = purple_blist_find_buddy(purple_connection_get_account(gc), who);
g_return_if_fail(buddy != NULL);
ggp_roster_set_synchronized(gc, buddy, FALSE);
}
void ggp_roster_group_buddy(PurpleConnection *gc, const char *who,
const char *old_group, const char *new_group)
{
ggp_roster_session_data *rdata = ggp_roster_get_rdata(gc);
ggp_roster_change *change;
if (!ggp_roster_enabled())
return;
if (rdata->is_updating)
return;
purple_debug_misc("gg", "ggp_roster_group_buddy: "
"who=\"%s\", group=\"%s\" -> \"%s\")\n",
who, old_group, new_group);
/* purple_blist_find_buddy(..., who) is not accessible at this moment */
change = g_new0(ggp_roster_change, 1);
change->type = GGP_ROSTER_CHANGE_CONTACT_UPDATE;
change->data.uin = ggp_str_to_uin(who);
rdata->pending_updates = g_list_append(rdata->pending_updates, change);
}
void ggp_roster_rename_group(PurpleConnection *gc, const char *old_name,
PurpleGroup *group, GList *moved_buddies)
{
ggp_roster_session_data *rdata = ggp_roster_get_rdata(gc);
ggp_roster_change *change;
if (!ggp_roster_enabled())
return;
change = g_new0(ggp_roster_change, 1);
change->type = GGP_ROSTER_CHANGE_GROUP_RENAME;
change->data.group_rename.old_name = g_strdup(old_name);
change->data.group_rename.new_name =
g_strdup(purple_group_get_name(group));
rdata->pending_updates = g_list_append(rdata->pending_updates, change);
}
void ggp_roster_add_buddy(PurpleConnection *gc, PurpleBuddy *buddy,
PurpleGroup *group, const char *message)
{
g_return_if_fail(gc != NULL);
g_return_if_fail(buddy != NULL);
if (!ggp_roster_enabled())
return;
ggp_roster_set_synchronized(gc, buddy, FALSE);
}
void ggp_roster_remove_buddy(PurpleConnection *gc, PurpleBuddy *buddy,
PurpleGroup *group)
{
ggp_roster_session_data *rdata = ggp_roster_get_rdata(gc);
ggp_roster_change *change;
if (!ggp_roster_enabled())
return;
change = g_new0(ggp_roster_change, 1);
change->type = GGP_ROSTER_CHANGE_CONTACT_REMOVE;
change->data.uin = ggp_str_to_uin(purple_buddy_get_name(buddy));
rdata->pending_updates = g_list_append(rdata->pending_updates, change);
}
/*******************************************************************************
* Buddy list import.
******************************************************************************/
static gboolean ggp_roster_reply_list_read_group(PurpleXmlNode *node,
ggp_roster_content *content)
{
char *name, *id;
gboolean removable;
gboolean succ = TRUE, is_bot, is_default;
succ &= ggp_xml_get_string(node, "Id", &id);
succ &= ggp_xml_get_string(node, "Name", &name);
succ &= ggp_xml_get_bool(node, "IsRemovable", &removable);
if (!succ) {
g_free(id);
g_free(name);
g_return_val_if_reached(FALSE);
}
is_bot = (strcmp(id, GGP_ROSTER_GROUPID_BOTS) == 0 ||
g_strcmp0(name, "Pomocnicy") == 0);
is_default = (strcmp(id, GGP_ROSTER_GROUPID_DEFAULT) == 0 ||
g_strcmp0(name, PURPLE_BLIST_DEFAULT_GROUP_NAME) == 0 ||
g_strcmp0(name, "[default]") == 0);
if (!content->bots_group_id && is_bot)
content->bots_group_id = g_strdup(id);
if (!removable || is_bot || is_default) {
g_free(id);
g_free(name);
return TRUE;
}
g_hash_table_insert(content->group_nodes, g_strdup(id), node);
g_hash_table_insert(content->group_ids, g_strdup(name), g_strdup(id));
g_hash_table_insert(content->group_names, id, name);
return TRUE;
}
static gboolean ggp_roster_reply_list_read_buddy(PurpleConnection *gc,
PurpleXmlNode *node, ggp_roster_content *content, GHashTable *remove_buddies)
{
gchar *alias, *group_name = NULL;
uin_t uin;
gboolean succ = TRUE;
PurpleXmlNode *group_list, *group_elem;
PurpleBuddy *buddy = NULL;
PurpleGroup *group = NULL;
PurpleGroup *currentGroup;
gboolean alias_changed;
PurpleAccount *account = purple_connection_get_account(gc);
succ &= ggp_xml_get_string(node, "ShowName", &alias);
succ &= ggp_xml_get_uint(node, "GGNumber", &uin);
group_list = purple_xmlnode_get_child(node, "Groups");
succ &= (group_list != NULL);
if (!succ) {
g_free(alias);
g_return_val_if_reached(FALSE);
}
g_hash_table_insert(content->contact_nodes, GINT_TO_POINTER(uin), node);
/* check, if alias is set */
if (*alias == '\0' ||
strcmp(alias, ggp_uin_to_str(uin)) == 0)
{
g_free(alias);
alias = NULL;
}
/* getting (eventually creating) group */
group_elem = purple_xmlnode_get_child(group_list, "GroupId");
while (group_elem != NULL) {
gchar *id;
gboolean isbot;
if (!ggp_xml_get_string(group_elem, NULL, &id))
continue;
isbot = (0 == g_strcmp0(id, content->bots_group_id));
group_name = g_hash_table_lookup(content->group_names, id);
g_free(id);
/* we don't want to import bots;
* they are inserted to roster by default
*/
if (isbot) {
g_free(alias);
return TRUE;
}
if (group_name != NULL)
break;
group_elem = purple_xmlnode_get_next_twin(group_elem);
}
if (group_name) {
group = purple_blist_find_group(group_name);
if (!group) {
group = purple_group_new(group_name);
purple_blist_add_group(group, NULL);
}
}
/* add buddy, if doesn't exists */
buddy = purple_blist_find_buddy(account, ggp_uin_to_str(uin));
g_hash_table_remove(remove_buddies, GINT_TO_POINTER(uin));
if (!buddy) {
purple_debug_info("gg", "ggp_roster_reply_list_read_buddy: "
"adding %u (%s) to buddy list\n", uin, alias);
buddy = purple_buddy_new(account, ggp_uin_to_str(uin), alias);
purple_blist_add_buddy(buddy, NULL, group, NULL);
ggp_roster_set_synchronized(gc, buddy, TRUE);
g_free(alias);
return TRUE;
}
/* buddy exists, but is not synchronized - local list has priority */
if (!ggp_roster_is_synchronized(buddy)) {
purple_debug_misc("gg", "ggp_roster_reply_list_read_buddy: "
"ignoring not synchronized %u (%s)\n",
uin, purple_buddy_get_name(buddy));
g_free(alias);
return TRUE;
}
currentGroup = ggp_purplew_buddy_get_group_only(buddy);
alias_changed =
(0 != g_strcmp0(alias, purple_buddy_get_alias_only(buddy)));
if (currentGroup == group && !alias_changed) {
g_free(alias);
return TRUE;
}
purple_debug_misc("gg", "ggp_roster_reply_list_read_buddy: "
"updating %u (%s) - alias=\"%s\"->\"%s\", group=%p->%p (%s)\n",
uin, purple_buddy_get_name(buddy),
purple_buddy_get_alias(buddy), alias,
currentGroup, group, group_name);
if (alias_changed)
purple_buddy_set_local_alias(buddy, alias);
if (currentGroup != group)
purple_blist_add_buddy(buddy, NULL, group, NULL);
g_free(alias);
return TRUE;
}
static void ggp_roster_reply_list(PurpleConnection *gc, uint32_t version,
const char *data)
{
ggp_roster_session_data *rdata = ggp_roster_get_rdata(gc);
PurpleXmlNode *xml, *xml_it;
PurpleAccount *account;
GSList *local_buddies;
GHashTable *remove_buddies;
GList *update_buddies = NULL, *local_groups, *it, *table_values;
ggp_roster_content *content;
g_return_if_fail(gc != NULL);
g_return_if_fail(data != NULL);
account = purple_connection_get_account(gc);
purple_debug_info("gg", "ggp_roster_reply_list: got list, version=%u\n",
version);
xml = purple_xmlnode_from_str(data, -1);
if (xml == NULL) {
purple_debug_warning("gg", "ggp_roster_reply_list: "
"invalid xml\n");
return;
}
ggp_roster_content_free(rdata->content);
rdata->content = NULL;
rdata->is_updating = TRUE;
content = g_new0(ggp_roster_content, 1);
content->version = version;
content->xml = xml;
content->contact_nodes = g_hash_table_new(NULL, NULL);
content->group_nodes = g_hash_table_new_full(
g_str_hash, g_str_equal, g_free, NULL);
content->group_ids = g_hash_table_new_full(
g_str_hash, g_str_equal, g_free, g_free);
content->group_names = g_hash_table_new_full(
g_str_hash, g_str_equal, g_free, g_free);
#if GGP_ROSTER_DEBUG
ggp_roster_dump(content);
#endif
/* reading groups */
content->groups_node = purple_xmlnode_get_child(xml, "Groups");
if (content->groups_node == NULL) {
ggp_roster_content_free(content);
g_return_if_reached();
}
xml_it = purple_xmlnode_get_child(content->groups_node, "Group");
while (xml_it != NULL) {
if (!ggp_roster_reply_list_read_group(xml_it, content)) {
ggp_roster_content_free(content);
g_return_if_reached();
}
xml_it = purple_xmlnode_get_next_twin(xml_it);
}
/* dumping current group list */
local_groups = ggp_purplew_account_get_groups(account, TRUE);
/* dumping current buddy list
* we will:
* - remove synchronized ones, if not found in list at server
* - upload not synchronized ones
*/
local_buddies = purple_blist_find_buddies(account, NULL);
remove_buddies = g_hash_table_new(g_direct_hash, g_direct_equal);
while (local_buddies) {
PurpleBuddy *buddy = local_buddies->data;
uin_t uin = ggp_str_to_uin(purple_buddy_get_name(buddy));
local_buddies =
g_slist_delete_link(local_buddies, local_buddies);
if (!uin)
continue;
if (ggp_roster_is_synchronized(buddy))
g_hash_table_insert(remove_buddies,
GINT_TO_POINTER(uin), buddy);
else
update_buddies = g_list_append(update_buddies, buddy);
}
/* reading buddies */
content->contacts_node = purple_xmlnode_get_child(xml, "Contacts");
if (content->contacts_node == NULL) {
g_hash_table_destroy(remove_buddies);
g_list_free(update_buddies);
ggp_roster_content_free(content);
g_return_if_reached();
}
xml_it = purple_xmlnode_get_child(content->contacts_node, "Contact");
while (xml_it != NULL) {
if (!ggp_roster_reply_list_read_buddy(gc, xml_it, content,
remove_buddies))
{
g_hash_table_destroy(remove_buddies);
g_list_free(update_buddies);
ggp_roster_content_free(content);
g_return_if_reached();
}
xml_it = purple_xmlnode_get_next_twin(xml_it);
}
/* removing buddies, which are not present in roster */
table_values = g_hash_table_get_values(remove_buddies);
it = g_list_first(table_values);
while (it) {
PurpleBuddy *buddy = it->data;
it = g_list_next(it);
if (!ggp_roster_is_synchronized(buddy))
continue;
purple_debug_info("gg", "ggp_roster_reply_list: "
"removing %s from buddy list\n",
purple_buddy_get_name(buddy));
purple_blist_remove_buddy(buddy);
}
g_list_free(table_values);
g_hash_table_destroy(remove_buddies);
/* remove groups, which are empty, but had contacts before
* synchronization
*/
it = g_list_first(local_groups);
while (it) {
PurpleGroup *group = it->data;
it = g_list_next(it);
if (purple_counting_node_get_total_size(PURPLE_COUNTING_NODE(group)) != 0)
continue;
purple_debug_info("gg", "ggp_roster_reply_list: "
"removing group %s\n", purple_group_get_name(group));
purple_blist_remove_group(group);
}
g_list_free(local_groups);
/* adding not synchronized buddies */
it = g_list_first(update_buddies);
while (it) {
PurpleBuddy *buddy = it->data;
uin_t uin = ggp_str_to_uin(purple_buddy_get_name(buddy));
ggp_roster_change *change;
it = g_list_next(it);
g_assert(uin > 0);
purple_debug_misc("gg", "ggp_roster_reply_list: "
"adding change of %u for roster\n", uin);
change = g_new0(ggp_roster_change, 1);
change->type = GGP_ROSTER_CHANGE_CONTACT_UPDATE;
change->data.uin = uin;
rdata->pending_updates =
g_list_append(rdata->pending_updates, change);
}
g_list_free(update_buddies);
rdata->content = content;
rdata->is_updating = FALSE;
purple_debug_info("gg", "ggp_roster_reply_list: "
"import done, version=%u\n", version);
}
/*******************************************************************************
* Buddy list export.
******************************************************************************/
static const gchar * ggp_roster_send_update_group_add(
ggp_roster_content *content, PurpleGroup *group)
{
gchar *id;
const char *id_existing, *group_name;
PurpleXmlNode *group_node;
gboolean succ = TRUE;
if (group) {
group_name = purple_group_get_name(group);
id_existing =
g_hash_table_lookup(content->group_ids, group_name);
} else
id_existing = GGP_ROSTER_GROUPID_DEFAULT;
if (id_existing)
return id_existing;
purple_debug_info("gg", "ggp_roster_send_update_group_add: adding %s\n",
purple_group_get_name(group));
id = purple_uuid_random();
group_node = purple_xmlnode_new_child(content->groups_node, "Group");
succ &= ggp_xml_set_string(group_node, "Id", id);
succ &= ggp_xml_set_string(group_node, "Name", group_name);
succ &= ggp_xml_set_string(group_node, "IsExpanded", "true");
succ &= ggp_xml_set_string(group_node, "IsRemovable", "true");
content->needs_update = TRUE;
g_hash_table_insert(content->group_ids, g_strdup(group_name),
g_strdup(id));
g_hash_table_replace(content->group_nodes, id, group_node);
g_return_val_if_fail(succ, NULL);
return id;
}
static gboolean ggp_roster_send_update_contact_update(PurpleConnection *gc,
ggp_roster_change *change)
{
PurpleAccount *account = purple_connection_get_account(gc);
ggp_roster_content *content = ggp_roster_get_rdata(gc)->content;
uin_t uin = change->data.uin;
PurpleBuddy *buddy;
PurpleXmlNode *buddy_node, *contact_groups;
gboolean succ = TRUE;
const char *group_id;
g_return_val_if_fail(change->type == GGP_ROSTER_CHANGE_CONTACT_UPDATE,
FALSE);
buddy = purple_blist_find_buddy(account, ggp_uin_to_str(uin));
if (!buddy)
return TRUE;
buddy_node = g_hash_table_lookup(content->contact_nodes,
GINT_TO_POINTER(uin));
group_id = ggp_roster_send_update_group_add(content,
ggp_purplew_buddy_get_group_only(buddy));
if (buddy_node) { /* update existing */
purple_debug_misc("gg", "ggp_roster_send_update_contact_update:"
" updating %u...\n", uin);
succ &= ggp_xml_set_string(buddy_node, "ShowName",
purple_buddy_get_alias(buddy));
contact_groups = purple_xmlnode_get_child(buddy_node, "Groups");
g_assert(contact_groups);
ggp_xmlnode_remove_children(contact_groups);
succ &= ggp_xml_set_string(contact_groups, "GroupId", group_id);
g_return_val_if_fail(succ, FALSE);
return TRUE;
}
/* add new */
purple_debug_misc("gg", "ggp_roster_send_update_contact_update: "
"adding %u...\n", uin);
buddy_node = purple_xmlnode_new_child(content->contacts_node, "Contact");
succ &= ggp_xml_set_string(buddy_node, "Guid", purple_uuid_random());
succ &= ggp_xml_set_uint(buddy_node, "GGNumber", uin);
succ &= ggp_xml_set_string(buddy_node, "ShowName",
purple_buddy_get_alias(buddy));
contact_groups = purple_xmlnode_new_child(buddy_node, "Groups");
g_assert(contact_groups);
succ &= ggp_xml_set_string(contact_groups, "GroupId", group_id);
purple_xmlnode_new_child(buddy_node, "Avatars");
succ &= ggp_xml_set_bool(buddy_node, "FlagBuddy", TRUE);
succ &= ggp_xml_set_bool(buddy_node, "FlagNormal", TRUE);
succ &= ggp_xml_set_bool(buddy_node, "FlagFriend", TRUE);
/* we don't use Guid, so update is not needed
* content->needs_update = TRUE;
*/
g_hash_table_insert(content->contact_nodes, GINT_TO_POINTER(uin),
buddy_node);
g_return_val_if_fail(succ, FALSE);
return TRUE;
}
static gboolean ggp_roster_send_update_contact_remove(PurpleConnection *gc,
ggp_roster_change *change)
{
PurpleAccount *account = purple_connection_get_account(gc);
ggp_roster_content *content = ggp_roster_get_rdata(gc)->content;
uin_t uin = change->data.uin;
PurpleBuddy *buddy;
PurpleXmlNode *buddy_node;
g_return_val_if_fail(change->type == GGP_ROSTER_CHANGE_CONTACT_REMOVE,
FALSE);
buddy = purple_blist_find_buddy(account, ggp_uin_to_str(uin));
if (buddy) {
purple_debug_info("gg", "ggp_roster_send_update_contact_remove:"
" contact %u re-added\n", uin);
return TRUE;
}
buddy_node = g_hash_table_lookup(content->contact_nodes,
GINT_TO_POINTER(uin));
if (!buddy_node) /* already removed */
return TRUE;
purple_debug_info("gg", "ggp_roster_send_update_contact_remove: "
"removing %u\n", uin);
purple_xmlnode_free(buddy_node);
g_hash_table_remove(content->contact_nodes, GINT_TO_POINTER(uin));
return TRUE;
}
static gboolean ggp_roster_send_update_group_rename(PurpleConnection *gc,
ggp_roster_change *change)
{
PurpleAccount *account = purple_connection_get_account(gc);
ggp_roster_content *content = ggp_roster_get_rdata(gc)->content;
const char *old_name = change->data.group_rename.old_name;
const char *new_name = change->data.group_rename.new_name;
PurpleXmlNode *group_node;
const char *group_id;
g_return_val_if_fail(change->type == GGP_ROSTER_CHANGE_GROUP_RENAME,
FALSE);
purple_debug_misc("gg", "ggp_roster_send_update_group_rename: "
"\"%s\"->\"%s\"\n", old_name, new_name);
/* moving to or from default group instead of renaming it */
if (0 == g_strcmp0(old_name, PURPLE_BLIST_DEFAULT_GROUP_NAME) ||
0 == g_strcmp0(new_name, PURPLE_BLIST_DEFAULT_GROUP_NAME))
{
PurpleGroup *group;
GList *group_buddies;
group = purple_blist_find_group(new_name);
if (!group)
return TRUE;
purple_debug_info("gg", "ggp_roster_send_update_group_rename: "
"invalidating buddies in default group\n");
group_buddies = ggp_purplew_group_get_buddies(group, account);
while (group_buddies) {
ggp_roster_set_synchronized(gc, group_buddies->data,
FALSE);
group_buddies = g_list_delete_link(group_buddies,
group_buddies);
}
return TRUE;
}
group_id = g_hash_table_lookup(content->group_ids, old_name);
if (!group_id) {
purple_debug_info("gg", "ggp_roster_send_update_group_rename: "
"%s is not present at roster\n", old_name);
return TRUE;
}
group_node = g_hash_table_lookup(content->group_nodes, group_id);
if (!group_node) {
purple_debug_error("gg", "ggp_roster_send_update_group_rename: "
"node for %s not found, id=%s\n", old_name, group_id);
g_hash_table_remove(content->group_ids, old_name);
return TRUE;
}
g_hash_table_remove(content->group_ids, old_name);
g_hash_table_insert(content->group_ids, g_strdup(new_name),
g_strdup(group_id));
g_hash_table_insert(content->group_nodes, g_strdup(group_id),
group_node);
return ggp_xml_set_string(group_node, "Name", new_name);
}
static void ggp_roster_send_update(PurpleConnection *gc)
{
GGPInfo *accdata = purple_connection_get_protocol_data(gc);
ggp_roster_session_data *rdata = ggp_roster_get_rdata(gc);
ggp_roster_content *content = rdata->content;
GList *updates_it;
gchar *str;
int len;
/* an update is running now */
if (rdata->sent_updates)
return;
/* no pending updates found */
if (!rdata->pending_updates)
return;
/* not initialized */
if (!content)
return;
purple_debug_info("gg", "ggp_roster_send_update: "
"pending updates found\n");
rdata->sent_updates = rdata->pending_updates;
rdata->pending_updates = NULL;
updates_it = g_list_first(rdata->sent_updates);
while (updates_it) {
ggp_roster_change *change = updates_it->data;
gboolean succ = FALSE;
updates_it = g_list_next(updates_it);
if (change->type == GGP_ROSTER_CHANGE_CONTACT_UPDATE)
succ = ggp_roster_send_update_contact_update(gc,
change);
else if (change->type == GGP_ROSTER_CHANGE_CONTACT_REMOVE)
succ = ggp_roster_send_update_contact_remove(gc,
change);
else if (change->type == GGP_ROSTER_CHANGE_GROUP_RENAME)
succ = ggp_roster_send_update_group_rename(gc, change);
else
purple_debug_fatal("gg", "ggp_roster_send_update: "
"not handled\n");
g_return_if_fail(succ);
}
#if GGP_ROSTER_DEBUG
ggp_roster_dump(content);
#endif
str = purple_xmlnode_to_str(content->xml, &len);
gg_userlist100_request(accdata->session, GG_USERLIST100_PUT,
content->version, GG_USERLIST100_FORMAT_TYPE_GG100, str);
g_free(str);
}
static void ggp_roster_reply_ack(PurpleConnection *gc, uint32_t version)
{
PurpleAccount *account = purple_connection_get_account(gc);
ggp_roster_session_data *rdata = ggp_roster_get_rdata(gc);
ggp_roster_content *content = rdata->content;
GList *updates_it;
purple_debug_info("gg", "ggp_roster_reply_ack: version=%u\n", version);
/* set synchronization flag for all buddies, that were updated at roster */
updates_it = g_list_first(rdata->sent_updates);
while (updates_it) {
ggp_roster_change *change = updates_it->data;
PurpleBuddy *buddy;
updates_it = g_list_next(updates_it);
if (change->type != GGP_ROSTER_CHANGE_CONTACT_UPDATE)
continue;
buddy = purple_blist_find_buddy(account,
ggp_uin_to_str(change->data.uin));
if (buddy)
ggp_roster_set_synchronized(gc, buddy, TRUE);
}
/* we need to remove "synchronized" flag for all contacts, that have
* beed modified between roster update start and now
*/
updates_it = g_list_first(rdata->pending_updates);
while (updates_it) {
ggp_roster_change *change = updates_it->data;
PurpleBuddy *buddy;
updates_it = g_list_next(updates_it);
if (change->type != GGP_ROSTER_CHANGE_CONTACT_UPDATE)
continue;
buddy = purple_blist_find_buddy(account,
ggp_uin_to_str(change->data.uin));
if (buddy && ggp_roster_is_synchronized(buddy))
ggp_roster_set_synchronized(gc, buddy, FALSE);
}
g_list_free_full(rdata->sent_updates, ggp_roster_change_free);
rdata->sent_updates = NULL;
/* bump roster version or update it, if needed */
g_return_if_fail(content != NULL);
if (content->needs_update) {
ggp_roster_content_free(rdata->content);
rdata->content = NULL;
/* we have to wait for gg_event_userlist100_version
* ggp_roster_request_update(gc);
*/
}
else
content->version = version;
}
static void ggp_roster_reply_reject(PurpleConnection *gc, uint32_t version)
{
ggp_roster_session_data *rdata = ggp_roster_get_rdata(gc);
purple_debug_info("gg", "ggp_roster_reply_reject: version=%u\n",
version);
g_return_if_fail(rdata->sent_updates);
rdata->pending_updates = g_list_concat(rdata->pending_updates,
rdata->sent_updates);
rdata->sent_updates = NULL;
ggp_roster_content_free(rdata->content);
rdata->content = NULL;
ggp_roster_request_update(gc);
}
/******************************************************************************/