]> git.0d.be Git - empathy.git/blob - libempathy/empathy-tp-contact-list.c
d3964d6bc7bcca67976514cc5073b6f1ac29b713
[empathy.git] / libempathy / empathy-tp-contact-list.c
1 /* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
2 /*
3  * Copyright (C) 2007 Xavier Claessens <xclaesse@gmail.com>
4  * Copyright (C) 2007-2009 Collabora Ltd.
5  *
6  * This library is free software; you can redistribute it and/or
7  * modify it under the terms of the GNU Lesser General Public
8  * License as published by the Free Software Foundation; either
9  * version 2.1 of the License, or (at your option) any later version.
10  *
11  * This library is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14  * Lesser General Public License for more details.
15  *
16  * You should have received a copy of the GNU Lesser General Public
17  * License along with this library; if not, write to the Free Software
18  * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
19  *
20  * Authors: Xavier Claessens <xclaesse@gmail.com>
21  */
22
23 #include <config.h>
24
25 #include <string.h>
26 #include <glib/gi18n-lib.h>
27
28 #include <telepathy-glib/channel.h>
29 #include <telepathy-glib/connection.h>
30 #include <telepathy-glib/util.h>
31 #include <telepathy-glib/dbus.h>
32 #include <telepathy-glib/interfaces.h>
33
34 #include <extensions/extensions.h>
35
36 #include "empathy-tp-contact-list.h"
37 #include "empathy-tp-contact-factory.h"
38 #include "empathy-contact-list.h"
39 #include "empathy-utils.h"
40
41 #define DEBUG_FLAG EMPATHY_DEBUG_TP | EMPATHY_DEBUG_CONTACT
42 #include "empathy-debug.h"
43
44 #define GET_PRIV(obj) EMPATHY_GET_PRIV (obj, EmpathyTpContactList)
45 typedef struct {
46         TpConnection   *connection;
47
48         TpChannel      *publish;
49         TpChannel      *subscribe;
50         TpChannel      *stored;
51         /* contact handle (TpHandle) => reffed (EmpathyContact *)
52          *
53          * Union of:
54          *  - members of 'subscribe': we receive their presence
55          *  - RP of 'subscribe': we asked to receive their presence
56          *  - members of 'publish': we send them our presence
57          *  - members of 'stored': they are in our roster
58          */
59         GHashTable     *members;
60         /* contact handle (TpHandle) => reffed (EmpathyContact *)
61          *
62          * Contacts which are local-pending in the publish channel but are NOT in
63          * the members hash table: they asked to receive our presence and we don't
64          * receive theirs or asked to.
65          * That's basically the contacts which asked to add us to their contact
66          * list and we didn't answer it. */
67         GHashTable     *pendings;
68         /* group name: borrowed (const gchar *)  => reffed (TpChannel *) */
69         GHashTable     *groups;
70         /* group name: owned (gchar *) => owned GArray of TpHandle */
71         GHashTable     *add_to_group;
72 } EmpathyTpContactListPriv;
73
74 typedef enum {
75         TP_CONTACT_LIST_TYPE_PUBLISH,
76         TP_CONTACT_LIST_TYPE_SUBSCRIBE,
77         TP_CONTACT_LIST_TYPE_UNKNOWN
78 } TpContactListType;
79
80 static void tp_contact_list_iface_init         (EmpathyContactListIface   *iface);
81
82 enum {
83         PROP_0,
84         PROP_CONNECTION,
85 };
86
87 G_DEFINE_TYPE_WITH_CODE (EmpathyTpContactList, empathy_tp_contact_list, G_TYPE_OBJECT,
88                          G_IMPLEMENT_INTERFACE (EMPATHY_TYPE_CONTACT_LIST,
89                                                 tp_contact_list_iface_init));
90
91 static void
92 tp_contact_list_forget_group (EmpathyTpContactList *list,
93                               TpChannel *channel)
94 {
95         EmpathyTpContactListPriv *priv = GET_PRIV (list);
96         const TpIntSet *members;
97         TpIntSetIter iter;
98         const gchar *group_name;
99
100         group_name = tp_channel_get_identifier (channel);
101
102         /* Signal that all members are not in that group anymore */
103         members = tp_channel_group_get_members (channel);
104         tp_intset_iter_init (&iter, members);
105         while (tp_intset_iter_next (&iter)) {
106                 EmpathyContact *contact;
107
108                 contact = g_hash_table_lookup (priv->members,
109                                                GUINT_TO_POINTER (iter.element));
110                 if (contact == NULL) {
111                         continue;
112                 }
113
114                 DEBUG ("Contact %s (%d) removed from group %s",
115                         empathy_contact_get_id (contact), iter.element,
116                         group_name);
117                 g_signal_emit_by_name (list, "groups-changed", contact,
118                                        group_name,
119                                        FALSE);
120         }
121 }
122
123 static void
124 tp_contact_list_group_invalidated_cb (TpChannel *channel,
125                                       guint      domain,
126                                       gint       code,
127                                       gchar     *message,
128                                       EmpathyTpContactList *list)
129 {
130         EmpathyTpContactListPriv *priv = GET_PRIV (list);
131         const gchar *group_name;
132
133         group_name = tp_channel_get_identifier (channel);
134         DEBUG ("Group %s invalidated. Message: %s", group_name, message);
135
136         tp_contact_list_forget_group (list, channel);
137
138         g_hash_table_remove (priv->groups, group_name);
139 }
140
141 static void
142 contacts_added_to_group (EmpathyTpContactList *list,
143                          TpChannel *channel,
144                          GArray *added)
145 {
146         EmpathyTpContactListPriv *priv = GET_PRIV (list);
147         const gchar *group_name;
148         guint i;
149
150         group_name = tp_channel_get_identifier (channel);
151
152         for (i = 0; i < added->len; i++) {
153                 EmpathyContact *contact;
154                 TpHandle handle;
155
156                 handle = g_array_index (added, TpHandle, i);
157                 contact = g_hash_table_lookup (priv->members,
158                                                GUINT_TO_POINTER (handle));
159                 if (contact == NULL) {
160                         continue;
161                 }
162
163                 DEBUG ("Contact %s (%d) added to group %s",
164                         empathy_contact_get_id (contact), handle, group_name);
165                 g_signal_emit_by_name (list, "groups-changed", contact,
166                                        group_name,
167                                        TRUE);
168         }
169 }
170
171 static void
172 tp_contact_list_group_members_changed_cb (TpChannel     *channel,
173                                           gchar         *message,
174                                           GArray        *added,
175                                           GArray        *removed,
176                                           GArray        *local_pending,
177                                           GArray        *remote_pending,
178                                           guint          actor,
179                                           guint          reason,
180                                           EmpathyTpContactList *list)
181 {
182         EmpathyTpContactListPriv  *priv = GET_PRIV (list);
183         const gchar *group_name;
184         guint i;
185
186         contacts_added_to_group (list, channel, added);
187
188         group_name = tp_channel_get_identifier (channel);
189
190         for (i = 0; i < removed->len; i++) {
191                 EmpathyContact *contact;
192                 TpHandle handle;
193
194                 handle = g_array_index (removed, TpHandle, i);
195                 contact = g_hash_table_lookup (priv->members,
196                                                GUINT_TO_POINTER (handle));
197                 if (contact == NULL) {
198                         continue;
199                 }
200
201                 DEBUG ("Contact %s (%d) removed from group %s",
202                         empathy_contact_get_id (contact), handle, group_name);
203
204                 g_signal_emit_by_name (list, "groups-changed", contact,
205                                        group_name,
206                                        FALSE);
207         }
208 }
209
210 static void
211 tp_contact_list_group_ready_cb (TpChannel *channel,
212                                 const GError *error,
213                                 gpointer list)
214 {
215         EmpathyTpContactListPriv *priv = GET_PRIV (list);
216         TpChannel *old_group;
217         const gchar *group_name;
218         const TpIntSet *members;
219         GArray *arr;
220
221         if (error) {
222                 DEBUG ("Error: %s", error->message);
223                 g_object_unref (channel);
224                 return;
225         }
226
227         group_name = tp_channel_get_identifier (channel);
228
229         /* If there's already a group with this name in the table, we can't
230          * just let it be replaced. Replacing it causes it to be unreffed,
231          * which causes it to be invalidated (see
232          * <https://bugs.freedesktop.org/show_bug.cgi?id=22119>), which causes
233          * it to be removed from the hash table again, which causes it to be
234          * unreffed again.
235          */
236         old_group = g_hash_table_lookup (priv->groups, group_name);
237
238         if (old_group != NULL) {
239                 DEBUG ("Discarding old group %s (%p)", group_name, old_group);
240                 g_hash_table_steal (priv->groups, group_name);
241                 tp_contact_list_forget_group (list, old_group);
242                 g_object_unref (old_group);
243         }
244
245         /* Pass the reference on the TpChannel to priv->groups */
246         g_hash_table_insert (priv->groups, (gpointer) group_name, channel);
247         DEBUG ("Group %s added", group_name);
248
249         g_signal_connect (channel, "group-members-changed",
250                           G_CALLBACK (tp_contact_list_group_members_changed_cb),
251                           list);
252
253         g_signal_connect (channel, "invalidated",
254                           G_CALLBACK (tp_contact_list_group_invalidated_cb),
255                           list);
256
257         if (priv->add_to_group) {
258                 GArray *handles;
259
260                 handles = g_hash_table_lookup (priv->add_to_group, group_name);
261                 if (handles) {
262                         DEBUG ("Adding initial members to group %s", group_name);
263                         tp_cli_channel_interface_group_call_add_members (channel,
264                                 -1, handles, NULL, NULL, NULL, NULL, NULL);
265                         g_hash_table_remove (priv->add_to_group, group_name);
266                 }
267         }
268
269         /* Get initial members of the group */
270         members = tp_channel_group_get_members (channel);
271         g_assert (members != NULL);
272         arr = tp_intset_to_array (members);
273         contacts_added_to_group (list, channel, arr);
274         g_array_unref (arr);
275 }
276
277 static void
278 tp_contact_list_group_add_channel (EmpathyTpContactList *list,
279                                    const gchar          *object_path,
280                                    GHashTable           *properties)
281 {
282         EmpathyTpContactListPriv *priv = GET_PRIV (list);
283         TpChannel                *channel;
284         GError *error = NULL;
285
286         channel = tp_channel_new_from_properties (priv->connection,
287                                   object_path, properties, &error);
288         if (channel == NULL) {
289                 DEBUG ("Failed to create group channel: %s", error->message);
290                 g_error_free (error);
291                 return;
292         }
293
294         /* Give the ref to the callback */
295         tp_channel_call_when_ready (channel,
296                                     tp_contact_list_group_ready_cb,
297                                     list);
298 }
299
300 static void
301 tp_contact_list_group_request_channel_cb (TpConnection *connection,
302                                           const gchar  *object_path,
303                                           const GError *error,
304                                           gpointer      user_data,
305                                           GObject      *list)
306 {
307         /* The new channel will be handled in NewChannel cb. Here we only
308          * handle the error if RequestChannel failed */
309         if (error) {
310                 DEBUG ("Error: %s", error->message);
311                 return;
312         }
313 }
314
315 static void
316 tp_contact_list_group_request_handles_cb (TpConnection *connection,
317                                           const GArray *handles,
318                                           const GError *error,
319                                           gpointer      user_data,
320                                           GObject      *list)
321 {
322         TpHandle channel_handle;
323
324         if (error) {
325                 DEBUG ("Error: %s", error->message);
326                 return;
327         }
328
329         channel_handle = g_array_index (handles, TpHandle, 0);
330         tp_cli_connection_call_request_channel (connection, -1,
331                                                 TP_IFACE_CHANNEL_TYPE_CONTACT_LIST,
332                                                 TP_HANDLE_TYPE_GROUP,
333                                                 channel_handle,
334                                                 TRUE,
335                                                 tp_contact_list_group_request_channel_cb,
336                                                 NULL, NULL,
337                                                 list);
338 }
339
340 /* This function takes ownership of handles array */
341 static void
342 tp_contact_list_group_add (EmpathyTpContactList *list,
343                            const gchar          *group_name,
344                            GArray               *handles)
345 {
346         EmpathyTpContactListPriv *priv = GET_PRIV (list);
347         TpChannel                *channel;
348         const gchar              *names[] = {group_name, NULL};
349
350         /* Search the channel for that group name */
351         channel = g_hash_table_lookup (priv->groups, group_name);
352         if (channel) {
353                 tp_cli_channel_interface_group_call_add_members (channel, -1,
354                         handles, NULL, NULL, NULL, NULL, NULL);
355                 g_array_unref (handles);
356                 return;
357         }
358
359         /* That group does not exist yet, we have to:
360          * 1) Request an handle for the group name
361          * 2) Request a channel
362          * 3) When NewChannel is emitted, add handles in members
363          */
364         g_hash_table_insert (priv->add_to_group,
365                              g_strdup (group_name),
366                              handles);
367         tp_cli_connection_call_request_handles (priv->connection, -1,
368                                                 TP_HANDLE_TYPE_GROUP, names,
369                                                 tp_contact_list_group_request_handles_cb,
370                                                 NULL, NULL,
371                                                 G_OBJECT (list));
372 }
373
374 static void
375 got_added_members_cb (TpConnection            *connection,
376                       guint                    n_contacts,
377                       EmpathyContact * const * contacts,
378                       guint                    n_failed,
379                       const TpHandle          *failed,
380                       const GError            *error,
381                       gpointer                 user_data,
382                       GObject                 *list)
383 {
384         EmpathyTpContactListPriv *priv = GET_PRIV (list);
385         guint i;
386
387         if (error) {
388                 DEBUG ("Error: %s", error->message);
389                 return;
390         }
391
392         for (i = 0; i < n_contacts; i++) {
393                 EmpathyContact *contact = contacts[i];
394                 TpHandle handle = empathy_contact_get_handle (contact);
395
396                 if (g_hash_table_lookup (priv->members, GUINT_TO_POINTER (handle)))
397                         continue;
398
399                 /* Add to the list and emit signal */
400                 g_hash_table_insert (priv->members, GUINT_TO_POINTER (handle),
401                                      g_object_ref (contact));
402                 g_signal_emit_by_name (list, "members-changed", contact,
403                                        0, 0, NULL, TRUE);
404         }
405 }
406
407 static void
408 add_to_members (EmpathyTpContactList *list,
409                 GArray *handles)
410 {
411         EmpathyTpContactListPriv *priv = GET_PRIV (list);
412         GArray *request;
413         guint i;
414
415         if (handles->len == 0)
416                 return;
417
418         request = g_array_new (FALSE, FALSE, sizeof (TpHandle));
419
420         for (i = 0; i < handles->len; i++) {
421                 TpHandle handle = g_array_index (handles, TpHandle, i);
422
423                 if (g_hash_table_lookup (priv->members, GUINT_TO_POINTER (handle)))
424                         continue;
425
426                 g_array_append_val (request, handle);
427         }
428
429         if (request->len > 0) {
430                         empathy_tp_contact_factory_get_from_handles (priv->connection,
431                                 request->len, (TpHandle *) request->data,
432                                 got_added_members_cb, NULL, NULL, G_OBJECT (list));
433         }
434
435         g_array_unref (request);
436 }
437
438 static void
439 tp_contact_list_got_local_pending_cb (TpConnection            *connection,
440                                       guint                    n_contacts,
441                                       EmpathyContact * const * contacts,
442                                       guint                    n_failed,
443                                       const TpHandle          *failed,
444                                       const GError            *error,
445                                       gpointer                 user_data,
446                                       GObject                 *list)
447 {
448         EmpathyTpContactListPriv *priv = GET_PRIV (list);
449         guint i;
450
451         if (error) {
452                 DEBUG ("Error: %s", error->message);
453                 return;
454         }
455
456         for (i = 0; i < n_contacts; i++) {
457                 EmpathyContact *contact = contacts[i];
458                 TpHandle handle;
459                 const gchar *message;
460                 TpChannelGroupChangeReason reason;
461                 const TpIntSet *members, *remote_pending;
462
463                 handle = empathy_contact_get_handle (contact);
464                 members = tp_channel_group_get_members (priv->subscribe);
465                 remote_pending = tp_channel_group_get_remote_pending (priv->subscribe);
466
467                 if (tp_intset_is_member (members, handle) ||
468                     tp_intset_is_member (remote_pending, handle)) {
469                         GArray handles = {(gchar *) &handle, 1};
470
471                         /* This contact is already subscribed, or user requested
472                          * to subscribe, auto accept. */
473                         tp_cli_channel_interface_group_call_add_members (priv->publish,
474                                 -1, &handles, NULL, NULL, NULL, NULL, NULL);
475                 }
476                 else if (tp_channel_group_get_local_pending_info (priv->publish,
477                                                                   handle,
478                                                                   NULL,
479                                                                   &reason,
480                                                                   &message)) {
481                         /* Add contact to pendings */
482                         g_hash_table_insert (priv->pendings, GUINT_TO_POINTER (handle),
483                                              g_object_ref (contact));
484                         g_signal_emit_by_name (list, "pendings-changed", contact,
485                                                contact, reason, message, TRUE);
486                 }
487         }
488 }
489
490 static void
491 tp_contact_list_remove_handle (EmpathyTpContactList *list,
492                                GHashTable *table,
493                                TpHandle handle)
494 {
495         EmpathyTpContactListPriv *priv = GET_PRIV (list);
496         EmpathyContact *contact;
497         const gchar *sig;
498
499         if (table == priv->pendings)
500                 sig = "pendings-changed";
501         else if (table == priv->members)
502                 sig = "members-changed";
503         else
504                 return;
505
506         contact = g_hash_table_lookup (table, GUINT_TO_POINTER (handle));
507         if (contact) {
508                 g_object_ref (contact);
509                 g_hash_table_remove (table, GUINT_TO_POINTER (handle));
510                 g_signal_emit_by_name (list, sig, contact, 0, 0, NULL,
511                                        FALSE);
512                 g_object_unref (contact);
513         }
514 }
515
516 static void
517 remove_from_member_if_needed (EmpathyTpContactList *list,
518                               TpHandle handle)
519 {
520         /* remove contact from members if it's not in publish and subscribe */
521         EmpathyTpContactListPriv *priv = GET_PRIV (list);
522         const TpIntSet *members;
523
524         members = tp_channel_group_get_members (priv->subscribe);
525         if (tp_intset_is_member (members, handle))
526                 return;
527
528         members = tp_channel_group_get_remote_pending (priv->subscribe);
529         if (tp_intset_is_member (members, handle))
530                 return;
531
532         members = tp_channel_group_get_members (priv->publish);
533         if (tp_intset_is_member (members, handle))
534                 return;
535
536         tp_contact_list_remove_handle (list, priv->members, handle);
537 }
538
539 static void
540 tp_contact_list_publish_group_members_changed_cb (TpChannel     *channel,
541                                                   gchar         *message,
542                                                   GArray        *added,
543                                                   GArray        *removed,
544                                                   GArray        *local_pending,
545                                                   GArray        *remote_pending,
546                                                   TpHandle       actor,
547                                                   TpChannelGroupChangeReason reason,
548                                                   EmpathyTpContactList *list)
549 {
550         EmpathyTpContactListPriv *priv = GET_PRIV (list);
551         guint i;
552
553         /* We now send our presence to those contacts, remove them from pendings */
554         add_to_members (list, added);
555         for (i = 0; i < added->len; i++) {
556                 tp_contact_list_remove_handle (list, priv->pendings,
557                         g_array_index (added, TpHandle, i));
558         }
559
560         /* We refuse to send our presence to those contacts, remove from pendings */
561         for (i = 0; i < removed->len; i++) {
562                 TpHandle handle = g_array_index (removed, TpHandle, i);
563
564                 tp_contact_list_remove_handle (list, priv->pendings, handle);
565                 remove_from_member_if_needed (list, handle);
566         }
567
568         /* Those contacts want our presence, auto accept those that are already
569          * member, otherwise add in pendings. */
570         if (local_pending->len > 0) {
571                 empathy_tp_contact_factory_get_from_handles (priv->connection,
572                         local_pending->len, (TpHandle *) local_pending->data,
573                         tp_contact_list_got_local_pending_cb, NULL, NULL,
574                         G_OBJECT (list));
575         }
576 }
577
578 static void
579 tp_contact_list_subscribe_group_members_changed_cb (TpChannel     *channel,
580                                                     gchar         *message,
581                                                     GArray        *added,
582                                                     GArray        *removed,
583                                                     GArray        *local_pending,
584                                                     GArray        *remote_pending,
585                                                     guint          actor,
586                                                     guint          reason,
587                                                     EmpathyTpContactList *list)
588 {
589         EmpathyTpContactListPriv *priv = GET_PRIV (list);
590         guint i;
591         GArray *accept;
592
593         /* We now get the presence of those contacts, add them to members */
594         add_to_members (list, added);
595
596         /* Those contacts refuse to send us their presence, remove from members. */
597         for (i = 0; i < removed->len; i++) {
598                 remove_from_member_if_needed (list, g_array_index (removed, TpHandle, i));
599         }
600
601         /* We want those contacts in our contact list but we don't get their
602          * presence yet. Add to members anyway. */
603         add_to_members (list, remote_pending);
604
605         /* Implicitly accept pending request of contacts which are now members. */
606         if (priv->publish == NULL)
607                 return;
608
609         accept = g_array_new (FALSE, FALSE, sizeof (TpHandle));
610         for (i = 0; i < added->len; i++) {
611                 TpHandle handle = g_array_index (added, TpHandle, i);
612
613                 if (g_hash_table_lookup (priv->pendings, GUINT_TO_POINTER (handle)) != NULL)
614                         g_array_append_val (accept, handle);
615         }
616
617         for (i = 0; i < remote_pending->len; i++) {
618                 TpHandle handle = g_array_index (added, TpHandle, i);
619
620                 if (g_hash_table_lookup (priv->pendings, GUINT_TO_POINTER (handle)) != NULL)
621                         g_array_append_val (accept, handle);
622         }
623
624         tp_cli_channel_interface_group_call_add_members (priv->publish,
625                 -1, accept, NULL, NULL, NULL, NULL, NULL);
626
627         g_array_unref (accept);
628 }
629
630 static void
631 tp_contact_list_store_group_members_changed_cb (TpChannel     *channel,
632                                                 gchar         *message,
633                                                 GArray        *added,
634                                                 GArray        *removed,
635                                                 GArray        *local_pending,
636                                                 GArray        *remote_pending,
637                                                 guint          actor,
638                                                 guint          reason,
639                                                 EmpathyTpContactList *list)
640 {
641         guint i;
642
643         add_to_members (list, added);
644
645         for (i = 0; i < removed->len; i++) {
646                 remove_from_member_if_needed (list, g_array_index (removed, TpHandle, i));
647         }
648 }
649
650 static void
651 tp_contact_list_finalize (GObject *object)
652 {
653         EmpathyTpContactListPriv *priv;
654         EmpathyTpContactList     *list;
655         GHashTableIter            iter;
656         gpointer                  channel;
657
658         list = EMPATHY_TP_CONTACT_LIST (object);
659         priv = GET_PRIV (list);
660
661         DEBUG ("finalize: %p", object);
662
663         if (priv->subscribe) {
664                 g_object_unref (priv->subscribe);
665         }
666         if (priv->publish) {
667                 g_object_unref (priv->publish);
668         }
669         if (priv->stored) {
670                 g_object_unref (priv->stored);
671         }
672
673         if (priv->connection) {
674                 g_object_unref (priv->connection);
675         }
676
677         g_hash_table_iter_init (&iter, priv->groups);
678         while (g_hash_table_iter_next (&iter, NULL, &channel)) {
679                 g_signal_handlers_disconnect_by_func (channel,
680                         tp_contact_list_group_invalidated_cb, list);
681         }
682
683         g_hash_table_unref (priv->groups);
684         g_hash_table_unref (priv->members);
685         g_hash_table_unref (priv->pendings);
686         g_hash_table_unref (priv->add_to_group);
687
688         G_OBJECT_CLASS (empathy_tp_contact_list_parent_class)->finalize (object);
689 }
690
691 static void
692 got_list_channel (EmpathyTpContactList *list,
693                   TpChannel *channel)
694 {
695         EmpathyTpContactListPriv *priv = GET_PRIV (list);
696         const gchar *id;
697
698         /* We requested that channel by providing TargetID property, so it's
699          * guaranteed that tp_channel_get_identifier will return it. */
700         id = tp_channel_get_identifier (channel);
701
702         /* TpChannel emits initial set of members just before being ready */
703         if (!tp_strdiff (id, "stored")) {
704                 if (priv->stored != NULL)
705                         return;
706                 priv->stored = g_object_ref (channel);
707                 g_signal_connect (priv->stored, "group-members-changed",
708                                   G_CALLBACK (tp_contact_list_store_group_members_changed_cb),
709                                   list);
710         } else if (!tp_strdiff (id, "publish")) {
711                 if (priv->publish != NULL)
712                         return;
713                 priv->publish = g_object_ref (channel);
714                 g_signal_connect (priv->publish, "group-members-changed",
715                                   G_CALLBACK (tp_contact_list_publish_group_members_changed_cb),
716                                   list);
717         } else if (!tp_strdiff (id, "subscribe")) {
718                 if (priv->subscribe != NULL)
719                         return;
720                 priv->subscribe = g_object_ref (channel);
721                 g_signal_connect (priv->subscribe, "group-members-changed",
722                                   G_CALLBACK (tp_contact_list_subscribe_group_members_changed_cb),
723                                   list);
724         }
725 }
726
727 static void
728 list_ensure_channel_cb (TpConnection *conn,
729                         gboolean yours,
730                         const gchar *path,
731                         GHashTable *properties,
732                         const GError *error,
733                         gpointer user_data,
734                         GObject *weak_object)
735 {
736         EmpathyTpContactList *list = user_data;
737         TpChannel *channel;
738
739         if (error != NULL) {
740                 DEBUG ("failed: %s\n", error->message);
741                 return;
742         }
743
744         channel = tp_channel_new_from_properties (conn, path, properties, NULL);
745         got_list_channel (list, channel);
746         g_object_unref (channel);
747 }
748
749 static void
750 iterate_on_channels (EmpathyTpContactList *list,
751                      const GPtrArray *channels)
752 {
753         guint i;
754
755         for (i = 0; i < channels->len ; i++) {
756                 GValueArray *arr = g_ptr_array_index (channels, i);
757                 const gchar *path;
758                 GHashTable *properties;
759                 TpHandleType handle_type;
760
761                 path = g_value_get_boxed (g_value_array_get_nth (arr, 0));
762                 properties = g_value_get_boxed (g_value_array_get_nth (arr, 1));
763
764                 if (tp_strdiff (tp_asv_get_string (properties,
765                                 TP_IFACE_CHANNEL ".ChannelType"),
766                     TP_IFACE_CHANNEL_TYPE_CONTACT_LIST))
767                         continue;
768
769                 if (tp_asv_get_string (properties, TP_IFACE_CHANNEL ".TargetID") == NULL)
770                         continue;
771
772                 handle_type = tp_asv_get_uint32 (properties,
773                         TP_IFACE_CHANNEL ".TargetHandleType", NULL);
774
775                 if (handle_type != TP_HANDLE_TYPE_GROUP)
776                         continue;
777
778                 tp_contact_list_group_add_channel (list, path, properties);
779         }
780 }
781
782 static void
783 new_channels_cb (TpConnection *conn,
784                  const GPtrArray *channels,
785                  gpointer user_data,
786                  GObject *weak_object)
787 {
788         EmpathyTpContactList *list = EMPATHY_TP_CONTACT_LIST (weak_object);
789
790         iterate_on_channels (list, channels);
791 }
792
793 static void
794 got_channels_cb (TpProxy *conn,
795                  const GValue *out,
796                  const GError *error,
797                  gpointer user_data,
798                  GObject *weak_object)
799 {
800         EmpathyTpContactList *list = EMPATHY_TP_CONTACT_LIST (weak_object);
801         const GPtrArray *channels;
802
803         if (error != NULL) {
804                 DEBUG ("Get Channels property failed: %s", error->message);
805                 return;
806         }
807
808         channels = g_value_get_boxed (out);
809         iterate_on_channels (list, channels);
810 }
811
812 static void
813 conn_ready_cb (TpConnection *connection,
814                const GError *error,
815                gpointer data)
816 {
817         EmpathyTpContactList *list = data;
818         EmpathyTpContactListPriv *priv = GET_PRIV (list);
819         GHashTable *request;
820
821         if (error != NULL) {
822                 DEBUG ("failed: %s", error->message);
823                 goto out;
824         }
825
826         /* Look for existing group channels */
827         tp_cli_dbus_properties_call_get (connection, -1,
828                 TP_IFACE_CONNECTION_INTERFACE_REQUESTS, "Channels", got_channels_cb,
829                 NULL, NULL, G_OBJECT (list));
830
831         request = tp_asv_new (
832                 TP_PROP_CHANNEL_CHANNEL_TYPE, G_TYPE_STRING, TP_IFACE_CHANNEL_TYPE_CONTACT_LIST,
833                 TP_PROP_CHANNEL_TARGET_HANDLE_TYPE, G_TYPE_UINT, TP_HANDLE_TYPE_LIST,
834                 NULL);
835
836         /* Watch the NewChannels signal so if ensuring list channels fails (for
837          * example because the server is slow and the D-Bus call timeouts before CM
838          * fetches the roster), we have a chance to get them later. */
839         tp_cli_connection_interface_requests_connect_to_new_channels (
840                 priv->connection, new_channels_cb, NULL, NULL, G_OBJECT (list), NULL);
841
842         /* Request the 'stored' list. */
843         tp_asv_set_static_string (request, TP_PROP_CHANNEL_TARGET_ID, "stored");
844         tp_cli_connection_interface_requests_call_ensure_channel (priv->connection,
845                 G_MAXINT, request, list_ensure_channel_cb, list, NULL, G_OBJECT (list));
846
847         /* Request the 'publish' list. */
848         tp_asv_set_static_string (request, TP_PROP_CHANNEL_TARGET_ID, "publish");
849         tp_cli_connection_interface_requests_call_ensure_channel (priv->connection,
850                 G_MAXINT, request, list_ensure_channel_cb, list, NULL, G_OBJECT (list));
851
852         /* Request the 'subscribe' list. */
853         tp_asv_set_static_string (request, TP_PROP_CHANNEL_TARGET_ID, "subscribe");
854         tp_cli_connection_interface_requests_call_ensure_channel (priv->connection,
855                 G_MAXINT, request, list_ensure_channel_cb, list, NULL, G_OBJECT (list));
856
857         g_hash_table_unref (request);
858
859 out:
860         g_object_unref (list);
861 }
862
863 static void
864 tp_contact_list_constructed (GObject *list)
865 {
866         EmpathyTpContactListPriv *priv = GET_PRIV (list);
867
868         tp_connection_call_when_ready (priv->connection, conn_ready_cb,
869                 g_object_ref (list));
870 }
871
872 static void
873 tp_contact_list_get_property (GObject    *object,
874                               guint       param_id,
875                               GValue     *value,
876                               GParamSpec *pspec)
877 {
878         EmpathyTpContactListPriv *priv = GET_PRIV (object);
879
880         switch (param_id) {
881         case PROP_CONNECTION:
882                 g_value_set_object (value, priv->connection);
883                 break;
884         default:
885                 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, pspec);
886                 break;
887         };
888 }
889
890 static void
891 tp_contact_list_set_property (GObject      *object,
892                               guint         param_id,
893                               const GValue *value,
894                               GParamSpec   *pspec)
895 {
896         EmpathyTpContactListPriv *priv = GET_PRIV (object);
897
898         switch (param_id) {
899         case PROP_CONNECTION:
900                 priv->connection = g_value_dup_object (value);
901                 break;
902         default:
903                 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, pspec);
904                 break;
905         };
906 }
907
908 static void
909 empathy_tp_contact_list_class_init (EmpathyTpContactListClass *klass)
910 {
911         GObjectClass *object_class = G_OBJECT_CLASS (klass);
912
913         object_class->finalize = tp_contact_list_finalize;
914         object_class->constructed = tp_contact_list_constructed;
915         object_class->get_property = tp_contact_list_get_property;
916         object_class->set_property = tp_contact_list_set_property;
917
918         g_object_class_install_property (object_class,
919                                          PROP_CONNECTION,
920                                          g_param_spec_object ("connection",
921                                                               "The Connection",
922                                                               "The connection associated with the contact list",
923                                                               TP_TYPE_CONNECTION,
924                                                               G_PARAM_READWRITE |
925                                                               G_PARAM_CONSTRUCT_ONLY));
926
927         g_type_class_add_private (object_class, sizeof (EmpathyTpContactListPriv));
928 }
929
930 static void
931 tp_contact_list_array_free (gpointer handles)
932 {
933         g_array_unref (handles);
934 }
935
936 static void
937 empathy_tp_contact_list_init (EmpathyTpContactList *list)
938 {
939         EmpathyTpContactListPriv *priv = G_TYPE_INSTANCE_GET_PRIVATE (list,
940                 EMPATHY_TYPE_TP_CONTACT_LIST, EmpathyTpContactListPriv);
941
942         list->priv = priv;
943
944         /* Map group's name to group's TpChannel. The group name string is owned
945          * by the TpChannel object */
946         priv->groups = g_hash_table_new_full (g_str_hash, g_str_equal,
947                                               NULL,
948                                               (GDestroyNotify) g_object_unref);
949
950         /* Map contact's handle to EmpathyContact object */
951         priv->members = g_hash_table_new_full (g_direct_hash, g_direct_equal,
952                                                NULL,
953                                                (GDestroyNotify) g_object_unref);
954
955         /* Map contact's handle to EmpathyContact object */
956         priv->pendings = g_hash_table_new_full (g_direct_hash, g_direct_equal,
957                                                 NULL,
958                                                 (GDestroyNotify) g_object_unref);
959
960         /* Map group's name to GArray of handle */
961         priv->add_to_group = g_hash_table_new_full (g_str_hash, g_str_equal,
962                                                     g_free,
963                                                     tp_contact_list_array_free);
964 }
965
966 EmpathyTpContactList *
967 empathy_tp_contact_list_new (TpConnection *connection)
968 {
969         return g_object_new (EMPATHY_TYPE_TP_CONTACT_LIST,
970                              "connection", connection,
971                              NULL);
972 }
973
974 TpConnection *
975 empathy_tp_contact_list_get_connection (EmpathyTpContactList *list)
976 {
977         EmpathyTpContactListPriv *priv;
978
979         g_return_val_if_fail (EMPATHY_IS_TP_CONTACT_LIST (list), NULL);
980
981         priv = GET_PRIV (list);
982
983         return priv->connection;
984 }
985
986 static void
987 tp_contact_list_add (EmpathyContactList *list,
988                      EmpathyContact     *contact,
989                      const gchar        *message)
990 {
991         EmpathyTpContactListPriv *priv = GET_PRIV (list);
992         TpHandle handle;
993         GArray handles = {(gchar *) &handle, 1};
994
995         handle = empathy_contact_get_handle (contact);
996
997         if (priv->subscribe) {
998                 tp_cli_channel_interface_group_call_add_members (priv->subscribe,
999                         -1, &handles, message, NULL, NULL, NULL, NULL);
1000         }
1001
1002         if (priv->publish) {
1003                 TpChannelGroupFlags flags = tp_channel_group_get_flags (priv->subscribe);
1004                 if (flags & TP_CHANNEL_GROUP_FLAG_CAN_ADD ||
1005                     g_hash_table_lookup (priv->pendings, GUINT_TO_POINTER (handle))) {
1006                         tp_cli_channel_interface_group_call_add_members (priv->publish,
1007                                 -1, &handles, message, NULL, NULL, NULL, NULL);
1008                 }
1009         }
1010
1011         /* We want to unblock the contact */
1012         if (tp_proxy_has_interface_by_id (priv->connection,
1013                 TP_IFACE_QUARK_CONNECTION_INTERFACE_CONTACT_BLOCKING)) {
1014                 TpContact *tp_contact = empathy_contact_get_tp_contact (contact);
1015
1016                 if (tp_contact != NULL)
1017                         tp_contact_unblock_async (tp_contact, NULL, NULL);
1018         }
1019 }
1020
1021 static void
1022 tp_contact_list_remove (EmpathyContactList *list,
1023                         EmpathyContact     *contact,
1024                         const gchar        *message)
1025 {
1026         EmpathyTpContactListPriv *priv = GET_PRIV (list);
1027         TpHandle handle;
1028         GArray handles = {(gchar *) &handle, 1};
1029
1030         handle = empathy_contact_get_handle (contact);
1031
1032         /* FIXME: this is racy if tp_contact_list_remove is called before the
1033          * 'stored' list has been retrieved. */
1034         if (priv->stored != NULL) {
1035                 tp_cli_channel_interface_group_call_remove_members (priv->stored,
1036                         -1, &handles, message, NULL, NULL, NULL, NULL);
1037         }
1038
1039         if (priv->subscribe) {
1040                 tp_cli_channel_interface_group_call_remove_members (priv->subscribe,
1041                         -1, &handles, message, NULL, NULL, NULL, NULL);
1042         }
1043         if (priv->publish) {
1044                 tp_cli_channel_interface_group_call_remove_members (priv->publish,
1045                         -1, &handles, message, NULL, NULL, NULL, NULL);
1046         }
1047 }
1048
1049 static GList *
1050 tp_contact_list_get_members (EmpathyContactList *list)
1051 {
1052         EmpathyTpContactListPriv *priv = GET_PRIV (list);
1053         GList *ret;
1054
1055         ret = g_hash_table_get_values (priv->members);
1056         g_list_foreach (ret, (GFunc) g_object_ref, NULL);
1057         return ret;
1058 }
1059
1060 static GList *
1061 tp_contact_list_get_pendings (EmpathyContactList *list)
1062 {
1063         EmpathyTpContactListPriv *priv = GET_PRIV (list);
1064         GList *ret;
1065
1066         ret = g_hash_table_get_values (priv->pendings);
1067         g_list_foreach (ret, (GFunc) g_object_ref, NULL);
1068         return ret;
1069 }
1070
1071 static GList *
1072 tp_contact_list_get_all_groups (EmpathyContactList *list)
1073 {
1074         EmpathyTpContactListPriv *priv = GET_PRIV (list);
1075         GList                    *ret, *l;
1076
1077         ret = g_hash_table_get_keys (priv->groups);
1078         for (l = ret; l; l = l->next) {
1079                 l->data = g_strdup (l->data);
1080         }
1081
1082         return ret;
1083 }
1084
1085 static GList *
1086 tp_contact_list_get_groups (EmpathyContactList *list,
1087                             EmpathyContact     *contact)
1088 {
1089         EmpathyTpContactListPriv  *priv = GET_PRIV (list);
1090         GList                     *ret = NULL;
1091         GHashTableIter             iter;
1092         gpointer                   group_name;
1093         gpointer                   channel;
1094         TpHandle                   handle;
1095
1096         handle = empathy_contact_get_handle (contact);
1097         g_hash_table_iter_init (&iter, priv->groups);
1098         while (g_hash_table_iter_next (&iter, &group_name, &channel)) {
1099                 const TpIntSet *members;
1100
1101                 members = tp_channel_group_get_members (channel);
1102                 if (tp_intset_is_member (members, handle)) {
1103                         ret = g_list_prepend (ret, g_strdup (group_name));
1104                 }
1105         }
1106
1107         return ret;
1108 }
1109
1110 static void
1111 tp_contact_list_add_to_group (EmpathyContactList *list,
1112                               EmpathyContact     *contact,
1113                               const gchar        *group_name)
1114 {
1115         TpHandle handle;
1116         GArray *handles;
1117
1118         handle = empathy_contact_get_handle (contact);
1119         handles = g_array_sized_new (FALSE, FALSE, sizeof (TpHandle), 1);
1120         g_array_append_val (handles, handle);
1121         tp_contact_list_group_add (EMPATHY_TP_CONTACT_LIST (list),
1122                                    group_name, handles);
1123 }
1124
1125 static void
1126 tp_contact_list_remove_from_group (EmpathyContactList *list,
1127                                    EmpathyContact     *contact,
1128                                    const gchar        *group_name)
1129 {
1130         EmpathyTpContactListPriv *priv = GET_PRIV (list);
1131         TpChannel                *channel;
1132         TpHandle                  handle;
1133         GArray                    handles = {(gchar *) &handle, 1};
1134
1135         channel = g_hash_table_lookup (priv->groups, group_name);
1136         if (channel == NULL) {
1137                 return;
1138         }
1139
1140         handle = empathy_contact_get_handle (contact);
1141         DEBUG ("remove contact %s (%d) from group %s",
1142                 empathy_contact_get_id (contact), handle, group_name);
1143
1144         tp_cli_channel_interface_group_call_remove_members (channel, -1,
1145                 &handles, NULL, NULL, NULL, NULL, NULL);
1146 }
1147
1148 static void
1149 tp_contact_list_rename_group (EmpathyContactList *list,
1150                               const gchar        *old_group_name,
1151                               const gchar        *new_group_name)
1152 {
1153         EmpathyTpContactListPriv *priv = GET_PRIV (list);
1154         TpChannel                *channel;
1155         const TpIntSet           *members;
1156         GArray                   *handles;
1157
1158         channel = g_hash_table_lookup (priv->groups, old_group_name);
1159         if (channel == NULL) {
1160                 return;
1161         }
1162
1163         DEBUG ("rename group %s to %s", old_group_name, new_group_name);
1164
1165         /* Remove all members and close the old channel */
1166         members = tp_channel_group_get_members (channel);
1167         handles = tp_intset_to_array (members);
1168         tp_cli_channel_interface_group_call_remove_members (channel, -1,
1169                 handles, NULL, NULL, NULL, NULL, NULL);
1170         tp_cli_channel_call_close (channel, -1, NULL, NULL, NULL, NULL);
1171
1172         tp_contact_list_group_add (EMPATHY_TP_CONTACT_LIST (list),
1173                                    new_group_name, handles);
1174 }
1175
1176 static void
1177 tp_contact_list_remove_group (EmpathyContactList *list,
1178                               const gchar *group_name)
1179 {
1180         EmpathyTpContactListPriv *priv = GET_PRIV (list);
1181         TpChannel                *channel;
1182         const TpIntSet           *members;
1183         GArray                   *handles;
1184
1185         channel = g_hash_table_lookup (priv->groups, group_name);
1186         if (channel == NULL) {
1187                 return;
1188         }
1189
1190         DEBUG ("remove group %s", group_name);
1191
1192         /* Remove all members and close the channel */
1193         members = tp_channel_group_get_members (channel);
1194         handles = tp_intset_to_array (members);
1195         tp_cli_channel_interface_group_call_remove_members (channel, -1,
1196                 handles, NULL, NULL, NULL, NULL, NULL);
1197         tp_cli_channel_call_close (channel, -1, NULL, NULL, NULL, NULL);
1198         g_array_unref (handles);
1199 }
1200
1201 static void
1202 tp_contact_list_iface_init (EmpathyContactListIface *iface)
1203 {
1204         iface->add               = tp_contact_list_add;
1205         iface->remove            = tp_contact_list_remove;
1206         iface->get_members       = tp_contact_list_get_members;
1207         iface->get_pendings      = tp_contact_list_get_pendings;
1208         iface->get_all_groups    = tp_contact_list_get_all_groups;
1209         iface->get_groups        = tp_contact_list_get_groups;
1210         iface->add_to_group      = tp_contact_list_add_to_group;
1211         iface->remove_from_group = tp_contact_list_remove_from_group;
1212         iface->rename_group      = tp_contact_list_rename_group;
1213         iface->remove_group      = tp_contact_list_remove_group;
1214 }
1215
1216 void
1217 empathy_tp_contact_list_remove_all (EmpathyTpContactList *list)
1218 {
1219         EmpathyTpContactListPriv *priv = GET_PRIV (list);
1220         GHashTableIter            iter;
1221         gpointer                  contact;
1222
1223         g_return_if_fail (EMPATHY_IS_TP_CONTACT_LIST (list));
1224
1225         /* Remove all contacts */
1226         g_hash_table_iter_init (&iter, priv->members);
1227         while (g_hash_table_iter_next (&iter, NULL, &contact)) {
1228                 g_signal_emit_by_name (list, "members-changed", contact,
1229                                        NULL, 0, NULL,
1230                                        FALSE);
1231         }
1232         g_hash_table_remove_all (priv->members);
1233
1234         g_hash_table_iter_init (&iter, priv->pendings);
1235         while (g_hash_table_iter_next (&iter, NULL, &contact)) {
1236                 g_signal_emit_by_name (list, "pendings-changed", contact,
1237                                        NULL, 0, NULL,
1238                                        FALSE);
1239         }
1240         g_hash_table_remove_all (priv->pendings);
1241 }