Files
cproof/src/xmpp/roster_list.c
Michael Vetter b210fb3603 statusbar: check if roster exists
We destory the roster in ev_disconnect_cleanup().
Adding a function to test if the roster has been destroyed and testing
for it in the statusbar.

So now when the connection is lost 'Lost connection' is printed in all
open windows.
We can then reconnect with `/connect accountname`.

Should fix https://github.com/profanity-im/profanity/issues/1083
2019-06-04 16:19:04 +02:00

718 lines
20 KiB
C

/*
* roster_list.c
*
* Copyright (C) 2012 - 2019 James Booth <boothj5@gmail.com>
*
* This file is part of Profanity.
*
* Profanity 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 3 of the License, or
* (at your option) any later version.
*
* Profanity 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 Profanity. If not, see <https://www.gnu.org/licenses/>.
*
* In addition, as a special exception, the copyright holders give permission to
* link the code of portions of this program with the OpenSSL library under
* certain conditions as described in each individual source file, and
* distribute linked combinations including the two.
*
* You must obey the GNU General Public License in all respects for all of the
* code used other than OpenSSL. If you modify file(s) with this exception, you
* may extend this exception to your version of the file(s), but you are not
* obligated to do so. If you do not wish to do so, delete this exception
* statement from your version. If you delete this exception statement from all
* source files in the program, then also delete it here.
*
*/
#include <string.h>
#include <stdlib.h>
#include <assert.h>
#include <glib.h>
#include <assert.h>
#include "config/preferences.h"
#include "tools/autocomplete.h"
#include "xmpp/roster_list.h"
#include "xmpp/resource.h"
#include "xmpp/contact.h"
#include "xmpp/jid.h"
typedef struct prof_roster_t {
// contacts, indexed on barejid
GHashTable *contacts;
// nicknames
Autocomplete name_ac;
// barejids
Autocomplete barejid_ac;
// fulljids
Autocomplete fulljid_ac;
// nickname to barejid map
GHashTable *name_to_barejid;
// groups
Autocomplete groups_ac;
GHashTable *group_count;
} ProfRoster;
typedef struct pending_presence {
char *barejid;
Resource *resource;
GDateTime *last_activity;
} ProfPendingPresence;
static ProfRoster *roster = NULL;
static gboolean roster_received = FALSE;
static GSList *roster_pending_presence = NULL;
static gboolean _key_equals(void *key1, void *key2);
static gboolean _datetimes_equal(GDateTime *dt1, GDateTime *dt2);
static void _replace_name(const char *const current_name, const char *const new_name, const char *const barejid);
static void _add_name_and_barejid(const char *const name, const char *const barejid);
void
roster_create(void)
{
assert(roster == NULL);
roster = malloc(sizeof(ProfRoster));
roster->contacts = g_hash_table_new_full(g_str_hash, (GEqualFunc)_key_equals, g_free, (GDestroyNotify)p_contact_free);
roster->name_ac = autocomplete_new();
roster->barejid_ac = autocomplete_new();
roster->fulljid_ac = autocomplete_new();
roster->name_to_barejid = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free);
roster->groups_ac = autocomplete_new();
roster->group_count = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL);
roster_received = FALSE;
roster_pending_presence = NULL;
}
void
roster_destroy(void)
{
assert(roster != NULL);
g_hash_table_destroy(roster->contacts);
autocomplete_free(roster->name_ac);
autocomplete_free(roster->barejid_ac);
autocomplete_free(roster->fulljid_ac);
g_hash_table_destroy(roster->name_to_barejid);
autocomplete_free(roster->groups_ac);
g_hash_table_destroy(roster->group_count);
free(roster);
roster = NULL;
}
gboolean
roster_update_presence(const char *const barejid, Resource *resource, GDateTime *last_activity)
{
assert(roster != NULL);
assert(barejid != NULL);
assert(resource != NULL);
if (!roster_received) {
ProfPendingPresence *presence = malloc(sizeof(ProfPendingPresence));
presence->barejid = strdup(barejid);
presence->resource = resource;
presence->last_activity = last_activity;
if (last_activity) {
g_date_time_ref(last_activity);
}
roster_pending_presence = g_slist_append(roster_pending_presence, presence);
return FALSE;
}
PContact contact = roster_get_contact(barejid);
if (contact == NULL) {
return FALSE;
}
if (!_datetimes_equal(p_contact_last_activity(contact), last_activity)) {
p_contact_set_last_activity(contact, last_activity);
}
p_contact_set_presence(contact, resource);
Jid *jid = jid_create_from_bare_and_resource(barejid, resource->name);
autocomplete_add(roster->fulljid_ac, jid->fulljid);
jid_destroy(jid);
return TRUE;
}
PContact
roster_get_contact(const char *const barejid)
{
assert(roster != NULL);
gchar *barejidlower = g_utf8_strdown(barejid, -1);
PContact contact = g_hash_table_lookup(roster->contacts, barejidlower);
g_free(barejidlower);
return contact;
}
char*
roster_get_msg_display_name(const char *const barejid, const char *const resource)
{
assert(roster != NULL);
GString *result = g_string_new("");
PContact contact = roster_get_contact(barejid);
if (contact) {
if (p_contact_name(contact)) {
g_string_append(result, p_contact_name(contact));
} else {
g_string_append(result, barejid);
}
} else {
g_string_append(result, barejid);
}
if (resource && prefs_get_boolean(PREF_RESOURCE_MESSAGE)) {
g_string_append(result, "/");
g_string_append(result, resource);
}
char *result_str = result->str;
g_string_free(result, FALSE);
return result_str;
}
gboolean
roster_contact_offline(const char *const barejid, const char *const resource, const char *const status)
{
assert(roster != NULL);
PContact contact = roster_get_contact(barejid);
if (contact == NULL) {
return FALSE;
}
if (resource == NULL) {
return TRUE;
} else {
gboolean result = p_contact_remove_resource(contact, resource);
if (result == TRUE) {
Jid *jid = jid_create_from_bare_and_resource(barejid, resource);
autocomplete_remove(roster->fulljid_ac, jid->fulljid);
jid_destroy(jid);
}
return result;
}
}
void
roster_reset_search_attempts(void)
{
assert(roster != NULL);
autocomplete_reset(roster->name_ac);
autocomplete_reset(roster->barejid_ac);
autocomplete_reset(roster->fulljid_ac);
autocomplete_reset(roster->groups_ac);
}
void
roster_change_name(PContact contact, const char *const new_name)
{
assert(roster != NULL);
assert(contact != NULL);
const char *current_name = NULL;
const char *barejid = p_contact_barejid(contact);
if (p_contact_name(contact)) {
current_name = strdup(p_contact_name(contact));
}
p_contact_set_name(contact, new_name);
_replace_name(current_name, new_name, barejid);
}
void
roster_remove(const char *const name, const char *const barejid)
{
assert(roster != NULL);
autocomplete_remove(roster->barejid_ac, barejid);
autocomplete_remove(roster->name_ac, name);
g_hash_table_remove(roster->name_to_barejid, name);
// remove each fulljid
PContact contact = roster_get_contact(barejid);
if (contact) {
GList *resources = p_contact_get_available_resources(contact);
while (resources) {
GString *fulljid = g_string_new(strdup(barejid));
g_string_append(fulljid, "/");
g_string_append(fulljid, resources->data);
autocomplete_remove(roster->fulljid_ac, fulljid->str);
g_string_free(fulljid, TRUE);
resources = g_list_next(resources);
}
g_list_free(resources);
GSList *groups = p_contact_groups(contact);
GSList *curr = groups;
while (curr) {
gchar *group = curr->data;
if (g_hash_table_contains(roster->group_count, group)) {
int count = GPOINTER_TO_INT(g_hash_table_lookup(roster->group_count, group));
count--;
if (count < 1) {
g_hash_table_remove(roster->group_count, group);
autocomplete_remove(roster->groups_ac, group);
} else {
g_hash_table_insert(roster->group_count, strdup(group), GINT_TO_POINTER(count));
}
}
curr = g_slist_next(curr);
}
}
// remove the contact
g_hash_table_remove(roster->contacts, barejid);
}
void
roster_update(const char *const barejid, const char *const name, GSList *groups, const char *const subscription,
gboolean pending_out)
{
assert(roster != NULL);
PContact contact = roster_get_contact(barejid);
assert(contact != NULL);
p_contact_set_subscription(contact, subscription);
p_contact_set_pending_out(contact, pending_out);
const char * const new_name = name;
const char * current_name = NULL;
if (p_contact_name(contact)) {
current_name = strdup(p_contact_name(contact));
}
p_contact_set_name(contact, new_name);
_replace_name(current_name, new_name, barejid);
GSList *curr_new_group = groups;
while (curr_new_group) {
char *new_group = curr_new_group->data;
// contact added to group
if (!p_contact_in_group(contact, new_group)) {
// group doesn't yet exist
if (!g_hash_table_contains(roster->group_count, new_group)) {
g_hash_table_insert(roster->group_count, strdup(new_group), GINT_TO_POINTER(1));
autocomplete_add(roster->groups_ac, curr_new_group->data);
// increment count
} else {
int count = GPOINTER_TO_INT(g_hash_table_lookup(roster->group_count, new_group));
g_hash_table_insert(roster->group_count, strdup(new_group), GINT_TO_POINTER(count + 1));
}
}
curr_new_group = g_slist_next(curr_new_group);
}
GSList *old_groups = p_contact_groups(contact);
GSList *curr_old_group = old_groups;
while (curr_old_group) {
char *old_group = curr_old_group->data;
// removed from group
if (!g_slist_find_custom(groups, old_group, (GCompareFunc)g_strcmp0)) {
if (g_hash_table_contains(roster->group_count, old_group)) {
int count = GPOINTER_TO_INT(g_hash_table_lookup(roster->group_count, old_group));
count--;
if (count < 1) {
g_hash_table_remove(roster->group_count, old_group);
autocomplete_remove(roster->groups_ac, old_group);
} else {
g_hash_table_insert(roster->group_count, strdup(old_group), GINT_TO_POINTER(count));
}
}
}
curr_old_group = g_slist_next(curr_old_group);
}
p_contact_set_groups(contact, groups);
}
gboolean
roster_add(const char *const barejid, const char *const name, GSList *groups, const char *const subscription,
gboolean pending_out)
{
assert(roster != NULL);
PContact contact = roster_get_contact(barejid);
if (contact) {
return FALSE;
}
contact = p_contact_new(barejid, name, groups, subscription, NULL, pending_out);
// add groups
GSList *curr_new_group = groups;
while (curr_new_group) {
char *new_group = curr_new_group->data;
if (g_hash_table_contains(roster->group_count, new_group)) {
int count = GPOINTER_TO_INT(g_hash_table_lookup(roster->group_count, new_group));
g_hash_table_insert(roster->group_count, strdup(new_group), GINT_TO_POINTER(count + 1));
} else {
g_hash_table_insert(roster->group_count, strdup(new_group), GINT_TO_POINTER(1));
autocomplete_add(roster->groups_ac, new_group);
}
curr_new_group = g_slist_next(curr_new_group);
}
g_hash_table_insert(roster->contacts, strdup(barejid), contact);
autocomplete_add(roster->barejid_ac, barejid);
_add_name_and_barejid(name, barejid);
return TRUE;
}
char*
roster_barejid_from_name(const char *const name)
{
assert(roster != NULL);
if (name) {
return g_hash_table_lookup(roster->name_to_barejid, name);
} else {
return NULL;
}
}
GSList*
roster_get_contacts_by_presence(const char *const presence)
{
assert(roster != NULL);
GSList *result = NULL;
GHashTableIter iter;
gpointer key;
gpointer value;
g_hash_table_iter_init(&iter, roster->contacts);
while (g_hash_table_iter_next(&iter, &key, &value)) {
PContact contact = (PContact)value;
if (g_strcmp0(p_contact_presence(contact), presence) == 0) {
result = g_slist_insert_sorted(result, value, (GCompareFunc)roster_compare_name);
}
}
// return all contact structs
return result;
}
GSList*
roster_get_contacts(roster_ord_t order)
{
assert(roster != NULL);
GSList *result = NULL;
GHashTableIter iter;
gpointer key;
gpointer value;
GCompareFunc cmp_func;
if (order == ROSTER_ORD_PRESENCE) {
cmp_func = (GCompareFunc) roster_compare_presence;
} else {
cmp_func = (GCompareFunc) roster_compare_name;
}
g_hash_table_iter_init(&iter, roster->contacts);
while (g_hash_table_iter_next(&iter, &key, &value)) {
result = g_slist_insert_sorted(result, value, cmp_func);
}
// return all contact structs
return result;
}
GSList*
roster_get_contacts_online(void)
{
assert(roster != NULL);
GSList *result = NULL;
GHashTableIter iter;
gpointer key;
gpointer value;
g_hash_table_iter_init(&iter, roster->contacts);
while (g_hash_table_iter_next(&iter, &key, &value)) {
if(strcmp(p_contact_presence(value), "offline"))
result = g_slist_insert_sorted(result, value, (GCompareFunc)roster_compare_name);
}
// return all contact structs
return result;
}
gboolean
roster_has_pending_subscriptions(void)
{
assert(roster != NULL);
GHashTableIter iter;
gpointer key;
gpointer value;
g_hash_table_iter_init(&iter, roster->contacts);
while (g_hash_table_iter_next(&iter, &key, &value)) {
PContact contact = (PContact) value;
if (p_contact_pending_out(contact)) {
return TRUE;
}
}
return FALSE;
}
char*
roster_contact_autocomplete(const char *const search_str, gboolean previous)
{
assert(roster != NULL);
return autocomplete_complete(roster->name_ac, search_str, TRUE, previous);
}
char*
roster_fulljid_autocomplete(const char *const search_str, gboolean previous)
{
assert(roster != NULL);
return autocomplete_complete(roster->fulljid_ac, search_str, TRUE, previous);
}
GSList*
roster_get_group(const char *const group, roster_ord_t order)
{
assert(roster != NULL);
GSList *result = NULL;
GHashTableIter iter;
gpointer key;
gpointer value;
GCompareFunc cmp_func;
if (order == ROSTER_ORD_PRESENCE) {
cmp_func = (GCompareFunc) roster_compare_presence;
} else {
cmp_func = (GCompareFunc) roster_compare_name;
}
g_hash_table_iter_init(&iter, roster->contacts);
while (g_hash_table_iter_next(&iter, &key, &value)) {
GSList *groups = p_contact_groups(value);
if (group == NULL) {
if (groups == NULL) {
result = g_slist_insert_sorted(result, value, cmp_func);
}
} else {
while (groups) {
if (strcmp(groups->data, group) == 0) {
result = g_slist_insert_sorted(result, value, cmp_func);
break;
}
groups = g_slist_next(groups);
}
}
}
// return all contact structs
return result;
}
GList*
roster_get_groups(void)
{
assert(roster != NULL);
return autocomplete_create_list(roster->groups_ac);
}
char*
roster_group_autocomplete(const char *const search_str, gboolean previous)
{
assert(roster != NULL);
return autocomplete_complete(roster->groups_ac, search_str, TRUE, previous);
}
char*
roster_barejid_autocomplete(const char *const search_str, gboolean previous)
{
assert(roster != NULL);
return autocomplete_complete(roster->barejid_ac, search_str, TRUE, previous);
}
static gboolean
_key_equals(void *key1, void *key2)
{
gchar *str1 = (gchar *) key1;
gchar *str2 = (gchar *) key2;
return (g_strcmp0(str1, str2) == 0);
}
static gboolean
_datetimes_equal(GDateTime *dt1, GDateTime *dt2)
{
if ((dt1 == NULL) && (dt2 == NULL)) {
return TRUE;
} else if ((dt1 == NULL) && (dt2 != NULL)) {
return FALSE;
} else if ((dt1 != NULL) && (dt2 == NULL)) {
return FALSE;
} else {
return g_date_time_equal(dt1, dt2);
}
}
static void
_replace_name(const char *const current_name, const char *const new_name, const char *const barejid)
{
assert(roster != NULL);
// current handle exists already
if (current_name) {
autocomplete_remove(roster->name_ac, current_name);
g_hash_table_remove(roster->name_to_barejid, current_name);
_add_name_and_barejid(new_name, barejid);
// no current handle
} else if (new_name) {
autocomplete_remove(roster->name_ac, barejid);
g_hash_table_remove(roster->name_to_barejid, barejid);
_add_name_and_barejid(new_name, barejid);
}
}
static void
_add_name_and_barejid(const char *const name, const char *const barejid)
{
assert(roster != NULL);
if (name) {
autocomplete_add(roster->name_ac, name);
g_hash_table_insert(roster->name_to_barejid, strdup(name), strdup(barejid));
} else {
autocomplete_add(roster->name_ac, barejid);
g_hash_table_insert(roster->name_to_barejid, strdup(barejid), strdup(barejid));
}
}
gint
roster_compare_name(PContact a, PContact b)
{
const char * utf8_str_a = NULL;
const char * utf8_str_b = NULL;
if (p_contact_name_collate_key(a)) {
utf8_str_a = p_contact_name_collate_key(a);
} else {
utf8_str_a = p_contact_barejid_collate_key(a);
}
if (p_contact_name_collate_key(b)) {
utf8_str_b = p_contact_name_collate_key(b);
} else {
utf8_str_b = p_contact_barejid_collate_key(b);
}
gint result = g_strcmp0(utf8_str_a, utf8_str_b);
return result;
}
static gint
_get_presence_weight(const char *presence)
{
if (g_strcmp0(presence, "chat") == 0) {
return 0;
} else if (g_strcmp0(presence, "online") == 0) {
return 1;
} else if (g_strcmp0(presence, "away") == 0) {
return 2;
} else if (g_strcmp0(presence, "xa") == 0) {
return 3;
} else if (g_strcmp0(presence, "dnd") == 0) {
return 4;
} else { // offline
return 5;
}
}
gint
roster_compare_presence(PContact a, PContact b)
{
const char *presence_a = p_contact_presence(a);
const char *presence_b = p_contact_presence(b);
// if presence different, order by presence
if (g_strcmp0(presence_a, presence_b) != 0) {
int weight_a = _get_presence_weight(presence_a);
int weight_b = _get_presence_weight(presence_b);
if (weight_a < weight_b) {
return -1;
} else {
return 1;
}
// otherwise order by name
} else {
return roster_compare_name(a, b);
}
}
void
roster_process_pending_presence(void)
{
roster_received = TRUE;
GSList *iter;
for (iter = roster_pending_presence; iter != NULL; iter = iter->next) {
ProfPendingPresence *presence = iter->data;
roster_update_presence(presence->barejid, presence->resource, presence->last_activity);
free(presence->barejid);
/* seems like resource isn't free on the calling side */
if (presence->last_activity) {
g_date_time_unref(presence->last_activity);
}
}
g_slist_free(roster_pending_presence);
roster_pending_presence = NULL;
}
gboolean
roster_exists(void) {
if (roster != NULL) {
return TRUE;
}
return FALSE;
}