1 /*
2  * roster_list.c
3  * vim: expandtab:ts=4:sts=4:sw=4
4  *
5  * Copyright (C) 2012 - 2019 James Booth <boothj5@gmail.com>
6  *
7  * This file is part of Profanity.
8  *
9  * Profanity is free software: you can redistribute it and/or modify
10  * it under the terms of the GNU General Public License as published by
11  * the Free Software Foundation, either version 3 of the License, or
12  * (at your option) any later version.
13  *
14  * Profanity is distributed in the hope that it will be useful,
15  * but WITHOUT ANY WARRANTY; without even the implied warranty of
16  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17  * GNU General Public License for more details.
18  *
19  * You should have received a copy of the GNU General Public License
20  * along with Profanity.  If not, see <https://www.gnu.org/licenses/>.
21  *
22  * In addition, as a special exception, the copyright holders give permission to
23  * link the code of portions of this program with the OpenSSL library under
24  * certain conditions as described in each individual source file, and
25  * distribute linked combinations including the two.
26  *
27  * You must obey the GNU General Public License in all respects for all of the
28  * code used other than OpenSSL. If you modify file(s) with this exception, you
29  * may extend this exception to your version of the file(s), but you are not
30  * obligated to do so. If you do not wish to do so, delete this exception
31  * statement from your version. If you delete this exception statement from all
32  * source files in the program, then also delete it here.
33  *
34  */
35 
36 #include "config.h"
37 
38 #include <string.h>
39 #include <stdlib.h>
40 #include <assert.h>
41 #include <glib.h>
42 #include <assert.h>
43 
44 #include "config/preferences.h"
45 #include "tools/autocomplete.h"
46 #include "xmpp/roster_list.h"
47 #include "xmpp/resource.h"
48 #include "xmpp/contact.h"
49 #include "xmpp/jid.h"
50 
51 typedef struct prof_roster_t
52 {
53     // contacts, indexed on barejid
54     GHashTable* contacts;
55 
56     // nicknames
57     Autocomplete name_ac;
58 
59     // barejids
60     Autocomplete barejid_ac;
61 
62     // fulljids
63     Autocomplete fulljid_ac;
64 
65     // nickname to barejid map
66     GHashTable* name_to_barejid;
67 
68     // groups
69     Autocomplete groups_ac;
70     GHashTable* group_count;
71 } ProfRoster;
72 
73 typedef struct pending_presence
74 {
75     char* barejid;
76     Resource* resource;
77     GDateTime* last_activity;
78 } ProfPendingPresence;
79 
80 static ProfRoster* roster = NULL;
81 static gboolean roster_received = FALSE;
82 static GSList* roster_pending_presence = NULL;
83 
84 static gboolean _key_equals(void* key1, void* key2);
85 static gboolean _datetimes_equal(GDateTime* dt1, GDateTime* dt2);
86 static void _replace_name(const char* const current_name, const char* const new_name, const char* const barejid);
87 static void _add_name_and_barejid(const char* const name, const char* const barejid);
88 
89 void
roster_create(void)90 roster_create(void)
91 {
92     assert(roster == NULL);
93 
94     roster = malloc(sizeof(ProfRoster));
95     roster->contacts = g_hash_table_new_full(g_str_hash, (GEqualFunc)_key_equals, g_free, (GDestroyNotify)p_contact_free);
96     roster->name_ac = autocomplete_new();
97     roster->barejid_ac = autocomplete_new();
98     roster->fulljid_ac = autocomplete_new();
99     roster->name_to_barejid = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free);
100     roster->groups_ac = autocomplete_new();
101     roster->group_count = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL);
102 
103     roster_received = FALSE;
104     roster_pending_presence = NULL;
105 }
106 
107 void
roster_destroy(void)108 roster_destroy(void)
109 {
110     assert(roster != NULL);
111 
112     g_hash_table_destroy(roster->contacts);
113     autocomplete_free(roster->name_ac);
114     autocomplete_free(roster->barejid_ac);
115     autocomplete_free(roster->fulljid_ac);
116     g_hash_table_destroy(roster->name_to_barejid);
117     autocomplete_free(roster->groups_ac);
118     g_hash_table_destroy(roster->group_count);
119 
120     free(roster);
121     roster = NULL;
122 }
123 
124 gboolean
roster_update_presence(const char * const barejid,Resource * resource,GDateTime * last_activity)125 roster_update_presence(const char* const barejid, Resource* resource, GDateTime* last_activity)
126 {
127     assert(roster != NULL);
128 
129     assert(barejid != NULL);
130     assert(resource != NULL);
131 
132     if (!roster_received) {
133         ProfPendingPresence* presence = malloc(sizeof(ProfPendingPresence));
134         presence->barejid = strdup(barejid);
135         presence->resource = resource;
136         presence->last_activity = last_activity;
137         if (last_activity) {
138             g_date_time_ref(last_activity);
139         }
140         roster_pending_presence = g_slist_append(roster_pending_presence, presence);
141         return FALSE;
142     }
143 
144     PContact contact = roster_get_contact(barejid);
145     if (contact == NULL) {
146         /* Don't lose resource when there is no owner. */
147         resource_destroy(resource);
148         return FALSE;
149     }
150     if (!_datetimes_equal(p_contact_last_activity(contact), last_activity)) {
151         p_contact_set_last_activity(contact, last_activity);
152     }
153     p_contact_set_presence(contact, resource);
154     Jid* jid = jid_create_from_bare_and_resource(barejid, resource->name);
155     autocomplete_add(roster->fulljid_ac, jid->fulljid);
156     jid_destroy(jid);
157 
158     return TRUE;
159 }
160 
161 PContact
roster_get_contact(const char * const barejid)162 roster_get_contact(const char* const barejid)
163 {
164     assert(roster != NULL);
165 
166     gchar* barejidlower = g_utf8_strdown(barejid, -1);
167     PContact contact = g_hash_table_lookup(roster->contacts, barejidlower);
168     g_free(barejidlower);
169 
170     return contact;
171 }
172 
173 char*
roster_get_display_name(const char * const barejid)174 roster_get_display_name(const char* const barejid)
175 {
176     assert(roster != NULL);
177 
178     GString* result = g_string_new("");
179 
180     PContact contact = roster_get_contact(barejid);
181     if (contact) {
182         if (p_contact_name(contact)) {
183             g_string_append(result, p_contact_name(contact));
184         } else {
185             g_string_append(result, barejid);
186         }
187     } else {
188         g_string_append(result, barejid);
189     }
190 
191     char* result_str = result->str;
192     g_string_free(result, FALSE);
193 
194     return result_str;
195 }
196 
197 char*
roster_get_msg_display_name(const char * const barejid,const char * const resource)198 roster_get_msg_display_name(const char* const barejid, const char* const resource)
199 {
200     assert(roster != NULL);
201 
202     GString* result = g_string_new("");
203 
204     PContact contact = roster_get_contact(barejid);
205     if (contact) {
206         if (p_contact_name(contact)) {
207             g_string_append(result, p_contact_name(contact));
208         } else {
209             g_string_append(result, barejid);
210         }
211     } else {
212         g_string_append(result, barejid);
213     }
214 
215     if (resource && prefs_get_boolean(PREF_RESOURCE_MESSAGE)) {
216         g_string_append(result, "/");
217         g_string_append(result, resource);
218     }
219 
220     char* result_str = result->str;
221     g_string_free(result, FALSE);
222 
223     return result_str;
224 }
225 
226 gboolean
roster_contact_offline(const char * const barejid,const char * const resource,const char * const status)227 roster_contact_offline(const char* const barejid, const char* const resource, const char* const status)
228 {
229     assert(roster != NULL);
230 
231     PContact contact = roster_get_contact(barejid);
232 
233     if (contact == NULL) {
234         return FALSE;
235     }
236     if (resource == NULL) {
237         return TRUE;
238     } else {
239         gboolean result = p_contact_remove_resource(contact, resource);
240         if (result == TRUE) {
241             Jid* jid = jid_create_from_bare_and_resource(barejid, resource);
242             autocomplete_remove(roster->fulljid_ac, jid->fulljid);
243             jid_destroy(jid);
244         }
245 
246         return result;
247     }
248 }
249 
250 void
roster_reset_search_attempts(void)251 roster_reset_search_attempts(void)
252 {
253     assert(roster != NULL);
254 
255     autocomplete_reset(roster->name_ac);
256     autocomplete_reset(roster->barejid_ac);
257     autocomplete_reset(roster->fulljid_ac);
258     autocomplete_reset(roster->groups_ac);
259 }
260 
261 void
roster_change_name(PContact contact,const char * const new_name)262 roster_change_name(PContact contact, const char* const new_name)
263 {
264     assert(roster != NULL);
265     assert(contact != NULL);
266 
267     char* current_name = NULL;
268     const char* barejid = p_contact_barejid(contact);
269 
270     if (p_contact_name(contact)) {
271         current_name = strdup(p_contact_name(contact));
272     }
273 
274     p_contact_set_name(contact, new_name);
275     _replace_name(current_name, new_name, barejid);
276     free(current_name);
277 }
278 
279 void
roster_remove(const char * const name,const char * const barejid)280 roster_remove(const char* const name, const char* const barejid)
281 {
282     assert(roster != NULL);
283 
284     autocomplete_remove(roster->barejid_ac, barejid);
285     autocomplete_remove(roster->name_ac, name);
286     g_hash_table_remove(roster->name_to_barejid, name);
287 
288     // remove each fulljid
289     PContact contact = roster_get_contact(barejid);
290     if (contact) {
291         GList* resources = p_contact_get_available_resources(contact);
292         while (resources) {
293             GString* fulljid = g_string_new(barejid);
294             g_string_append(fulljid, "/");
295             g_string_append(fulljid, resources->data);
296             autocomplete_remove(roster->fulljid_ac, fulljid->str);
297             g_string_free(fulljid, TRUE);
298             resources = g_list_next(resources);
299         }
300         g_list_free(resources);
301 
302         GSList* groups = p_contact_groups(contact);
303         GSList* curr = groups;
304         while (curr) {
305             gchar* group = curr->data;
306             if (g_hash_table_contains(roster->group_count, group)) {
307                 int count = GPOINTER_TO_INT(g_hash_table_lookup(roster->group_count, group));
308                 count--;
309                 if (count < 1) {
310                     g_hash_table_remove(roster->group_count, group);
311                     autocomplete_remove(roster->groups_ac, group);
312                 } else {
313                     g_hash_table_insert(roster->group_count, strdup(group), GINT_TO_POINTER(count));
314                 }
315             }
316             curr = g_slist_next(curr);
317         }
318     }
319 
320     // remove the contact
321     g_hash_table_remove(roster->contacts, barejid);
322 }
323 
324 void
roster_update(const char * const barejid,const char * const name,GSList * groups,const char * const subscription,gboolean pending_out)325 roster_update(const char* const barejid, const char* const name, GSList* groups, const char* const subscription,
326               gboolean pending_out)
327 {
328     assert(roster != NULL);
329 
330     PContact contact = roster_get_contact(barejid);
331     assert(contact != NULL);
332 
333     p_contact_set_subscription(contact, subscription);
334     p_contact_set_pending_out(contact, pending_out);
335 
336     roster_change_name(contact, name);
337 
338     GSList* curr_new_group = groups;
339     while (curr_new_group) {
340         char* new_group = curr_new_group->data;
341 
342         // contact added to group
343         if (!p_contact_in_group(contact, new_group)) {
344 
345             // group doesn't yet exist
346             if (!g_hash_table_contains(roster->group_count, new_group)) {
347                 g_hash_table_insert(roster->group_count, strdup(new_group), GINT_TO_POINTER(1));
348                 autocomplete_add(roster->groups_ac, curr_new_group->data);
349 
350                 // increment count
351             } else {
352                 int count = GPOINTER_TO_INT(g_hash_table_lookup(roster->group_count, new_group));
353                 g_hash_table_insert(roster->group_count, strdup(new_group), GINT_TO_POINTER(count + 1));
354             }
355         }
356         curr_new_group = g_slist_next(curr_new_group);
357     }
358 
359     GSList* old_groups = p_contact_groups(contact);
360     GSList* curr_old_group = old_groups;
361     while (curr_old_group) {
362         char* old_group = curr_old_group->data;
363         // removed from group
364         if (!g_slist_find_custom(groups, old_group, (GCompareFunc)g_strcmp0)) {
365             if (g_hash_table_contains(roster->group_count, old_group)) {
366                 int count = GPOINTER_TO_INT(g_hash_table_lookup(roster->group_count, old_group));
367                 count--;
368                 if (count < 1) {
369                     g_hash_table_remove(roster->group_count, old_group);
370                     autocomplete_remove(roster->groups_ac, old_group);
371                 } else {
372                     g_hash_table_insert(roster->group_count, strdup(old_group), GINT_TO_POINTER(count));
373                 }
374             }
375         }
376 
377         curr_old_group = g_slist_next(curr_old_group);
378     }
379 
380     p_contact_set_groups(contact, groups);
381 }
382 
383 gboolean
roster_add(const char * const barejid,const char * const name,GSList * groups,const char * const subscription,gboolean pending_out)384 roster_add(const char* const barejid, const char* const name, GSList* groups, const char* const subscription,
385            gboolean pending_out)
386 {
387     assert(roster != NULL);
388 
389     PContact contact = roster_get_contact(barejid);
390     if (contact) {
391         return FALSE;
392     }
393 
394     contact = p_contact_new(barejid, name, groups, subscription, NULL, pending_out);
395 
396     // add groups
397     GSList* curr_new_group = groups;
398     while (curr_new_group) {
399         char* new_group = curr_new_group->data;
400         if (g_hash_table_contains(roster->group_count, new_group)) {
401             int count = GPOINTER_TO_INT(g_hash_table_lookup(roster->group_count, new_group));
402             g_hash_table_insert(roster->group_count, strdup(new_group), GINT_TO_POINTER(count + 1));
403         } else {
404             g_hash_table_insert(roster->group_count, strdup(new_group), GINT_TO_POINTER(1));
405             autocomplete_add(roster->groups_ac, new_group);
406         }
407 
408         curr_new_group = g_slist_next(curr_new_group);
409     }
410 
411     g_hash_table_insert(roster->contacts, strdup(barejid), contact);
412     autocomplete_add(roster->barejid_ac, barejid);
413     _add_name_and_barejid(name, barejid);
414 
415     return TRUE;
416 }
417 
418 char*
roster_barejid_from_name(const char * const name)419 roster_barejid_from_name(const char* const name)
420 {
421     assert(roster != NULL);
422 
423     if (name) {
424         return g_hash_table_lookup(roster->name_to_barejid, name);
425     } else {
426         return NULL;
427     }
428 }
429 
430 GSList*
roster_get_contacts_by_presence(const char * const presence)431 roster_get_contacts_by_presence(const char* const presence)
432 {
433     assert(roster != NULL);
434 
435     GSList* result = NULL;
436     GHashTableIter iter;
437     gpointer key;
438     gpointer value;
439 
440     g_hash_table_iter_init(&iter, roster->contacts);
441     while (g_hash_table_iter_next(&iter, &key, &value)) {
442         PContact contact = (PContact)value;
443         if (g_strcmp0(p_contact_presence(contact), presence) == 0) {
444             result = g_slist_insert_sorted(result, value, (GCompareFunc)roster_compare_name);
445         }
446     }
447 
448     // return all contact structs
449     return result;
450 }
451 
452 GSList*
roster_get_contacts(roster_ord_t order)453 roster_get_contacts(roster_ord_t order)
454 {
455     assert(roster != NULL);
456 
457     GSList* result = NULL;
458     GHashTableIter iter;
459     gpointer key;
460     gpointer value;
461 
462     GCompareFunc cmp_func;
463     if (order == ROSTER_ORD_PRESENCE) {
464         cmp_func = (GCompareFunc)roster_compare_presence;
465     } else {
466         cmp_func = (GCompareFunc)roster_compare_name;
467     }
468 
469     g_hash_table_iter_init(&iter, roster->contacts);
470     while (g_hash_table_iter_next(&iter, &key, &value)) {
471         result = g_slist_insert_sorted(result, value, cmp_func);
472     }
473 
474     // return all contact structs
475     return result;
476 }
477 
478 GSList*
roster_get_contacts_online(void)479 roster_get_contacts_online(void)
480 {
481     assert(roster != NULL);
482 
483     GSList* result = NULL;
484     GHashTableIter iter;
485     gpointer key;
486     gpointer value;
487 
488     g_hash_table_iter_init(&iter, roster->contacts);
489     while (g_hash_table_iter_next(&iter, &key, &value)) {
490         if (strcmp(p_contact_presence(value), "offline"))
491             result = g_slist_insert_sorted(result, value, (GCompareFunc)roster_compare_name);
492     }
493 
494     // return all contact structs
495     return result;
496 }
497 
498 gboolean
roster_has_pending_subscriptions(void)499 roster_has_pending_subscriptions(void)
500 {
501     assert(roster != NULL);
502 
503     GHashTableIter iter;
504     gpointer key;
505     gpointer value;
506 
507     g_hash_table_iter_init(&iter, roster->contacts);
508     while (g_hash_table_iter_next(&iter, &key, &value)) {
509         PContact contact = (PContact)value;
510         if (p_contact_pending_out(contact)) {
511             return TRUE;
512         }
513     }
514 
515     return FALSE;
516 }
517 
518 char*
roster_contact_autocomplete(const char * const search_str,gboolean previous,void * context)519 roster_contact_autocomplete(const char* const search_str, gboolean previous, void* context)
520 {
521     assert(roster != NULL);
522 
523     return autocomplete_complete(roster->name_ac, search_str, TRUE, previous);
524 }
525 
526 char*
roster_fulljid_autocomplete(const char * const search_str,gboolean previous,void * context)527 roster_fulljid_autocomplete(const char* const search_str, gboolean previous, void* context)
528 {
529     assert(roster != NULL);
530 
531     return autocomplete_complete(roster->fulljid_ac, search_str, TRUE, previous);
532 }
533 
534 GSList*
roster_get_group(const char * const group,roster_ord_t order)535 roster_get_group(const char* const group, roster_ord_t order)
536 {
537     assert(roster != NULL);
538 
539     GSList* result = NULL;
540     GHashTableIter iter;
541     gpointer key;
542     gpointer value;
543 
544     GCompareFunc cmp_func;
545     if (order == ROSTER_ORD_PRESENCE) {
546         cmp_func = (GCompareFunc)roster_compare_presence;
547     } else {
548         cmp_func = (GCompareFunc)roster_compare_name;
549     }
550 
551     g_hash_table_iter_init(&iter, roster->contacts);
552     while (g_hash_table_iter_next(&iter, &key, &value)) {
553         GSList* groups = p_contact_groups(value);
554         if (group == NULL) {
555             if (groups == NULL) {
556                 result = g_slist_insert_sorted(result, value, cmp_func);
557             }
558         } else {
559             while (groups) {
560                 if (strcmp(groups->data, group) == 0) {
561                     result = g_slist_insert_sorted(result, value, cmp_func);
562                     break;
563                 }
564                 groups = g_slist_next(groups);
565             }
566         }
567     }
568 
569     // return all contact structs
570     return result;
571 }
572 
573 GList*
roster_get_groups(void)574 roster_get_groups(void)
575 {
576     assert(roster != NULL);
577 
578     return autocomplete_create_list(roster->groups_ac);
579 }
580 
581 char*
roster_group_autocomplete(const char * const search_str,gboolean previous,void * context)582 roster_group_autocomplete(const char* const search_str, gboolean previous, void* context)
583 {
584     assert(roster != NULL);
585 
586     return autocomplete_complete(roster->groups_ac, search_str, TRUE, previous);
587 }
588 
589 char*
roster_barejid_autocomplete(const char * const search_str,gboolean previous,void * context)590 roster_barejid_autocomplete(const char* const search_str, gboolean previous, void* context)
591 {
592     assert(roster != NULL);
593 
594     return autocomplete_complete(roster->barejid_ac, search_str, TRUE, previous);
595 }
596 
597 static gboolean
_key_equals(void * key1,void * key2)598 _key_equals(void* key1, void* key2)
599 {
600     gchar* str1 = (gchar*)key1;
601     gchar* str2 = (gchar*)key2;
602 
603     return (g_strcmp0(str1, str2) == 0);
604 }
605 
606 static gboolean
_datetimes_equal(GDateTime * dt1,GDateTime * dt2)607 _datetimes_equal(GDateTime* dt1, GDateTime* dt2)
608 {
609     if ((dt1 == NULL) && (dt2 == NULL)) {
610         return TRUE;
611     } else if ((dt1 == NULL) && (dt2 != NULL)) {
612         return FALSE;
613     } else if ((dt1 != NULL) && (dt2 == NULL)) {
614         return FALSE;
615     } else {
616         return g_date_time_equal(dt1, dt2);
617     }
618 }
619 
620 static void
_replace_name(const char * const current_name,const char * const new_name,const char * const barejid)621 _replace_name(const char* const current_name, const char* const new_name, const char* const barejid)
622 {
623     assert(roster != NULL);
624 
625     // current handle exists already
626     if (current_name) {
627         autocomplete_remove(roster->name_ac, current_name);
628         g_hash_table_remove(roster->name_to_barejid, current_name);
629         _add_name_and_barejid(new_name, barejid);
630         // no current handle
631     } else if (new_name) {
632         autocomplete_remove(roster->name_ac, barejid);
633         g_hash_table_remove(roster->name_to_barejid, barejid);
634         _add_name_and_barejid(new_name, barejid);
635     }
636 }
637 
638 static void
_add_name_and_barejid(const char * const name,const char * const barejid)639 _add_name_and_barejid(const char* const name, const char* const barejid)
640 {
641     assert(roster != NULL);
642 
643     if (name) {
644         autocomplete_add(roster->name_ac, name);
645         g_hash_table_insert(roster->name_to_barejid, strdup(name), strdup(barejid));
646     } else {
647         autocomplete_add(roster->name_ac, barejid);
648         g_hash_table_insert(roster->name_to_barejid, strdup(barejid), strdup(barejid));
649     }
650 }
651 
652 gint
roster_compare_name(PContact a,PContact b)653 roster_compare_name(PContact a, PContact b)
654 {
655     const char* utf8_str_a = NULL;
656     const char* utf8_str_b = NULL;
657 
658     if (p_contact_name_collate_key(a)) {
659         utf8_str_a = p_contact_name_collate_key(a);
660     } else {
661         utf8_str_a = p_contact_barejid_collate_key(a);
662     }
663     if (p_contact_name_collate_key(b)) {
664         utf8_str_b = p_contact_name_collate_key(b);
665     } else {
666         utf8_str_b = p_contact_barejid_collate_key(b);
667     }
668 
669     gint result = g_strcmp0(utf8_str_a, utf8_str_b);
670 
671     return result;
672 }
673 
674 static gint
_get_presence_weight(const char * presence)675 _get_presence_weight(const char* presence)
676 {
677     if (g_strcmp0(presence, "chat") == 0) {
678         return 0;
679     } else if (g_strcmp0(presence, "online") == 0) {
680         return 1;
681     } else if (g_strcmp0(presence, "away") == 0) {
682         return 2;
683     } else if (g_strcmp0(presence, "xa") == 0) {
684         return 3;
685     } else if (g_strcmp0(presence, "dnd") == 0) {
686         return 4;
687     } else { // offline
688         return 5;
689     }
690 }
691 
692 gint
roster_compare_presence(PContact a,PContact b)693 roster_compare_presence(PContact a, PContact b)
694 {
695     const char* presence_a = p_contact_presence(a);
696     const char* presence_b = p_contact_presence(b);
697 
698     // if presence different, order by presence
699     if (g_strcmp0(presence_a, presence_b) != 0) {
700         int weight_a = _get_presence_weight(presence_a);
701         int weight_b = _get_presence_weight(presence_b);
702         if (weight_a < weight_b) {
703             return -1;
704         } else {
705             return 1;
706         }
707 
708         // otherwise order by name
709     } else {
710         return roster_compare_name(a, b);
711     }
712 }
713 
714 static void
_pendingPresence_free(ProfPendingPresence * presence)715 _pendingPresence_free(ProfPendingPresence* presence)
716 {
717     if (!presence)
718         return;
719     free(presence->barejid);
720     free(presence);
721 }
722 
723 void
roster_process_pending_presence(void)724 roster_process_pending_presence(void)
725 {
726     roster_received = TRUE;
727 
728     GSList* iter;
729     for (iter = roster_pending_presence; iter != NULL; iter = iter->next) {
730         ProfPendingPresence* presence = iter->data;
731         roster_update_presence(presence->barejid, presence->resource, presence->last_activity);
732         /* seems like resource isn't free on the calling side */
733         if (presence->last_activity) {
734             g_date_time_unref(presence->last_activity);
735         }
736     }
737 
738     g_slist_free_full(roster_pending_presence, (GDestroyNotify)_pendingPresence_free);
739     roster_pending_presence = NULL;
740 }
741 
742 gboolean
roster_exists(void)743 roster_exists(void)
744 {
745     if (roster != NULL) {
746         return TRUE;
747     }
748     return FALSE;
749 }
750