]> git.0d.be Git - empathy.git/blob - libempathy/empathy-individual-manager.c
individual-manager: use tp-glib blocking API
[empathy.git] / libempathy / empathy-individual-manager.c
1 /* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
2 /*
3  * Copyright (C) 2007-2010 Collabora Ltd.
4  *
5  * This library is free software; you can redistribute it and/or
6  * modify it under the terms of the GNU Lesser General Public
7  * License as published by the Free Software Foundation; either
8  * version 2.1 of the License, or (at your option) any later version.
9  *
10  * This library is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13  * Lesser General Public License for more details.
14  *
15  * You should have received a copy of the GNU Lesser General Public
16  * License along with this library; if not, write to the Free Software
17  * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
18  *
19  * Authors: Xavier Claessens <xclaesse@gmail.com>
20  *          Travis Reitter <travis.reitter@collabora.co.uk>
21  */
22
23 #include <config.h>
24
25 #include <string.h>
26
27 #include <telepathy-glib/account-manager.h>
28 #include <telepathy-glib/enums.h>
29 #include <telepathy-glib/proxy-subclass.h>
30 #include <telepathy-glib/util.h>
31
32 #include <folks/folks.h>
33 #include <folks/folks-telepathy.h>
34
35 #include <extensions/extensions.h>
36
37 #include "empathy-individual-manager.h"
38 #include "empathy-utils.h"
39 #include "empathy-contact-manager.h"
40
41 #define DEBUG_FLAG EMPATHY_DEBUG_CONTACT
42 #include "empathy-debug.h"
43
44 #define GET_PRIV(obj) EMPATHY_GET_PRIV (obj, EmpathyIndividualManager)
45
46 /* This class only stores and refs Individuals who contain an EmpathyContact.
47  *
48  * This class merely forwards along signals from the aggregator and individuals
49  * and wraps aggregator functions for other client code. */
50 typedef struct
51 {
52   FolksIndividualAggregator *aggregator;
53   GHashTable *individuals; /* Individual.id -> Individual */
54 } EmpathyIndividualManagerPriv;
55
56 enum
57 {
58   FAVOURITES_CHANGED,
59   GROUPS_CHANGED,
60   MEMBERS_CHANGED,
61   LAST_SIGNAL
62 };
63
64 static guint signals[LAST_SIGNAL] = { 0 };
65
66 G_DEFINE_TYPE (EmpathyIndividualManager, empathy_individual_manager,
67     G_TYPE_OBJECT);
68
69 static EmpathyIndividualManager *manager_singleton = NULL;
70
71 static void
72 individual_group_changed_cb (FolksIndividual *individual,
73     gchar *group,
74     gboolean is_member,
75     EmpathyIndividualManager *self)
76 {
77   g_signal_emit (self, signals[GROUPS_CHANGED], 0, individual, group,
78       is_member);
79 }
80
81 static void
82 individual_notify_is_favourite_cb (FolksIndividual *individual,
83     GParamSpec *pspec,
84     EmpathyIndividualManager *self)
85 {
86   gboolean is_favourite = folks_favourite_details_get_is_favourite (
87       FOLKS_FAVOURITE_DETAILS (individual));
88   g_signal_emit (self, signals[FAVOURITES_CHANGED], 0, individual,
89       is_favourite);
90 }
91
92 static void
93 add_individual (EmpathyIndividualManager *self, FolksIndividual *individual)
94 {
95   EmpathyIndividualManagerPriv *priv = GET_PRIV (self);
96
97   g_hash_table_insert (priv->individuals,
98       g_strdup (folks_individual_get_id (individual)),
99       g_object_ref (individual));
100
101   g_signal_connect (individual, "group-changed",
102       G_CALLBACK (individual_group_changed_cb), self);
103   g_signal_connect (individual, "notify::is-favourite",
104       G_CALLBACK (individual_notify_is_favourite_cb), self);
105 }
106
107 static void
108 remove_individual (EmpathyIndividualManager *self, FolksIndividual *individual)
109 {
110   EmpathyIndividualManagerPriv *priv = GET_PRIV (self);
111
112   g_signal_handlers_disconnect_by_func (individual,
113       individual_group_changed_cb, self);
114   g_signal_handlers_disconnect_by_func (individual,
115       individual_notify_is_favourite_cb, self);
116
117   g_hash_table_remove (priv->individuals, folks_individual_get_id (individual));
118 }
119
120 /* This is emitted for *all* individuals in the individual aggregator (not
121  * just the ones we keep a reference to), to allow for the case where a new
122  * individual doesn't contain an EmpathyContact, but later has a persona added
123  * which does. */
124 static void
125 individual_notify_personas_cb (FolksIndividual *individual,
126     GParamSpec *pspec,
127     EmpathyIndividualManager *self)
128 {
129   EmpathyIndividualManagerPriv *priv = GET_PRIV (self);
130
131   const gchar *id = folks_individual_get_id (individual);
132   gboolean has_contact = empathy_folks_individual_contains_contact (individual);
133   gboolean had_contact = (g_hash_table_lookup (priv->individuals,
134       id) != NULL) ? TRUE : FALSE;
135
136   if (had_contact == TRUE && has_contact == FALSE)
137     {
138       GList *removed = NULL;
139
140       /* The Individual has lost its EmpathyContact */
141       removed = g_list_prepend (removed, individual);
142       g_signal_emit (self, signals[MEMBERS_CHANGED], 0, NULL, NULL, removed,
143           TP_CHANNEL_GROUP_CHANGE_REASON_NONE /* FIXME */);
144       g_list_free (removed);
145
146       remove_individual (self, individual);
147     }
148   else if (had_contact == FALSE && has_contact == TRUE)
149     {
150       GList *added = NULL;
151
152       /* The Individual has gained its first EmpathyContact */
153       add_individual (self, individual);
154
155       added = g_list_prepend (added, individual);
156       g_signal_emit (self, signals[MEMBERS_CHANGED], 0, NULL, added, NULL,
157           TP_CHANNEL_GROUP_CHANGE_REASON_NONE /* FIXME */);
158       g_list_free (added);
159     }
160 }
161
162 static void
163 aggregator_individuals_changed_cb (FolksIndividualAggregator *aggregator,
164     GeeMultiMap *changes,
165     EmpathyIndividualManager *self)
166 {
167   EmpathyIndividualManagerPriv *priv = GET_PRIV (self);
168   GeeIterator *iter;
169   GeeSet *removed;
170   GeeCollection *added;
171   GList *added_set = NULL, *added_filtered = NULL, *removed_list = NULL;
172
173   /* We're not interested in the relationships between the added and removed
174    * individuals, so just extract collections of them. Note that the added
175    * collection may contain duplicates, while the removed set won't. */
176   removed = gee_multi_map_get_keys (changes);
177   added = gee_multi_map_get_values (changes);
178
179   /* Handle the removals first, as one of the added Individuals might have the
180    * same ID as one of the removed Individuals (due to linking). */
181   iter = gee_iterable_iterator (GEE_ITERABLE (removed));
182   while (gee_iterator_next (iter))
183     {
184       FolksIndividual *ind = gee_iterator_get (iter);
185
186       if (ind == NULL)
187         continue;
188
189       g_signal_handlers_disconnect_by_func (ind,
190           individual_notify_personas_cb, self);
191
192       if (g_hash_table_lookup (priv->individuals,
193           folks_individual_get_id (ind)) != NULL)
194         {
195           remove_individual (self, ind);
196           removed_list = g_list_prepend (removed_list, ind);
197         }
198
199       g_clear_object (&ind);
200     }
201   g_clear_object (&iter);
202
203   /* Filter the individuals for ones which contain EmpathyContacts */
204   iter = gee_iterable_iterator (GEE_ITERABLE (added));
205   while (gee_iterator_next (iter))
206     {
207       FolksIndividual *ind = gee_iterator_get (iter);
208
209       /* Make sure we handle each added individual only once. */
210       if (ind == NULL || g_list_find (added_set, ind) != NULL)
211         continue;
212       added_set = g_list_prepend (added_set, ind);
213
214       g_signal_connect (ind, "notify::personas",
215           G_CALLBACK (individual_notify_personas_cb), self);
216
217       if (empathy_folks_individual_contains_contact (ind) == TRUE)
218         {
219           add_individual (self, ind);
220           added_filtered = g_list_prepend (added_filtered, ind);
221         }
222
223       g_clear_object (&ind);
224     }
225   g_clear_object (&iter);
226
227   g_list_free (added_set);
228
229   g_object_unref (added);
230   g_object_unref (removed);
231
232   /* Bail if we have no individuals left */
233   if (added_filtered == NULL && removed == NULL)
234     return;
235
236   added_filtered = g_list_reverse (added_filtered);
237
238   g_signal_emit (self, signals[MEMBERS_CHANGED], 0, NULL,
239       added_filtered, removed_list,
240       TP_CHANNEL_GROUP_CHANGE_REASON_NONE,
241       TRUE);
242
243   g_list_free (added_filtered);
244   g_list_free (removed_list);
245 }
246
247 static void
248 individual_manager_dispose (GObject *object)
249 {
250   EmpathyIndividualManagerPriv *priv = GET_PRIV (object);
251
252   g_hash_table_destroy (priv->individuals);
253
254   g_signal_handlers_disconnect_by_func (priv->aggregator,
255       aggregator_individuals_changed_cb, object);
256   tp_clear_object (&priv->aggregator);
257
258   G_OBJECT_CLASS (empathy_individual_manager_parent_class)->dispose (object);
259 }
260
261 static GObject *
262 individual_manager_constructor (GType type,
263     guint n_props,
264     GObjectConstructParam *props)
265 {
266   GObject *retval;
267
268   if (manager_singleton)
269     {
270       retval = g_object_ref (manager_singleton);
271     }
272   else
273     {
274       retval =
275           G_OBJECT_CLASS (empathy_individual_manager_parent_class)->
276           constructor (type, n_props, props);
277
278       manager_singleton = EMPATHY_INDIVIDUAL_MANAGER (retval);
279       g_object_add_weak_pointer (retval, (gpointer) & manager_singleton);
280     }
281
282   return retval;
283 }
284
285 /**
286  * empathy_individual_manager_initialized:
287  *
288  * Reports whether or not the singleton has already been created.
289  *
290  * There can be instances where you want to access the #EmpathyIndividualManager
291  * only if it has been set up for this process.
292  *
293  * Returns: %TRUE if the #EmpathyIndividualManager singleton has previously
294  * been initialized.
295  */
296 gboolean
297 empathy_individual_manager_initialized (void)
298 {
299   return (manager_singleton != NULL);
300 }
301
302 static void
303 empathy_individual_manager_class_init (EmpathyIndividualManagerClass *klass)
304 {
305   GObjectClass *object_class = G_OBJECT_CLASS (klass);
306
307   object_class->dispose = individual_manager_dispose;
308   object_class->constructor = individual_manager_constructor;
309
310   signals[GROUPS_CHANGED] =
311       g_signal_new ("groups-changed",
312           G_TYPE_FROM_CLASS (klass),
313           G_SIGNAL_RUN_LAST,
314           0,
315           NULL, NULL,
316           g_cclosure_marshal_generic,
317           G_TYPE_NONE, 3, FOLKS_TYPE_INDIVIDUAL, G_TYPE_STRING, G_TYPE_BOOLEAN);
318
319   signals[FAVOURITES_CHANGED] =
320       g_signal_new ("favourites-changed",
321           G_TYPE_FROM_CLASS (klass),
322           G_SIGNAL_RUN_LAST,
323           0,
324           NULL, NULL,
325           g_cclosure_marshal_generic,
326           G_TYPE_NONE, 2, FOLKS_TYPE_INDIVIDUAL, G_TYPE_BOOLEAN);
327
328   signals[MEMBERS_CHANGED] =
329       g_signal_new ("members-changed",
330           G_TYPE_FROM_CLASS (klass),
331           G_SIGNAL_RUN_LAST,
332           0,
333           NULL, NULL,
334           g_cclosure_marshal_generic,
335           G_TYPE_NONE,
336           4, G_TYPE_STRING, G_TYPE_POINTER, G_TYPE_POINTER, G_TYPE_UINT);
337
338   g_type_class_add_private (object_class,
339       sizeof (EmpathyIndividualManagerPriv));
340 }
341
342 static void
343 empathy_individual_manager_init (EmpathyIndividualManager *self)
344 {
345   EmpathyIndividualManagerPriv *priv = G_TYPE_INSTANCE_GET_PRIVATE (self,
346       EMPATHY_TYPE_INDIVIDUAL_MANAGER, EmpathyIndividualManagerPriv);
347
348   self->priv = priv;
349   priv->individuals = g_hash_table_new_full (g_str_hash, g_str_equal,
350       g_free, g_object_unref);
351
352   priv->aggregator = folks_individual_aggregator_new ();
353   g_signal_connect (priv->aggregator, "individuals-changed-detailed",
354       G_CALLBACK (aggregator_individuals_changed_cb), self);
355   folks_individual_aggregator_prepare (priv->aggregator, NULL, NULL);
356 }
357
358 EmpathyIndividualManager *
359 empathy_individual_manager_dup_singleton (void)
360 {
361   return g_object_new (EMPATHY_TYPE_INDIVIDUAL_MANAGER, NULL);
362 }
363
364 GList *
365 empathy_individual_manager_get_members (EmpathyIndividualManager *self)
366 {
367   EmpathyIndividualManagerPriv *priv = GET_PRIV (self);
368
369   g_return_val_if_fail (EMPATHY_IS_INDIVIDUAL_MANAGER (self), NULL);
370
371   return g_hash_table_get_values (priv->individuals);
372 }
373
374 FolksIndividual *
375 empathy_individual_manager_lookup_member (EmpathyIndividualManager *self,
376     const gchar *id)
377 {
378   EmpathyIndividualManagerPriv *priv = GET_PRIV (self);
379
380   g_return_val_if_fail (EMPATHY_IS_INDIVIDUAL_MANAGER (self), NULL);
381
382   return g_hash_table_lookup (priv->individuals, id);
383 }
384
385 static void
386 aggregator_add_persona_from_details_cb (GObject *source,
387     GAsyncResult *result,
388     gpointer user_data)
389 {
390   FolksIndividualAggregator *aggregator = FOLKS_INDIVIDUAL_AGGREGATOR (source);
391   EmpathyContact *contact = EMPATHY_CONTACT (user_data);
392   FolksPersona *persona;
393   GError *error = NULL;
394
395   persona = folks_individual_aggregator_add_persona_from_details_finish (
396       aggregator, result, &error);
397   if (error != NULL)
398     {
399       g_warning ("failed to add individual from contact: %s", error->message);
400       g_clear_error (&error);
401     }
402
403   /* The persona can be NULL even if there wasn't an error, if the persona was
404    * already in the contact list */
405   if (persona != NULL)
406     {
407       /* Set the contact's persona */
408       empathy_contact_set_persona (contact, persona);
409       g_object_unref (persona);
410     }
411
412   g_object_unref (contact);
413 }
414
415 void
416 empathy_individual_manager_add_from_contact (EmpathyIndividualManager *self,
417     EmpathyContact *contact)
418 {
419   EmpathyIndividualManagerPriv *priv;
420   FolksBackendStore *backend_store;
421   FolksBackend *backend;
422   FolksPersonaStore *persona_store;
423   GHashTable* details;
424   GeeMap *persona_stores;
425   TpAccount *account;
426   const gchar *store_id;
427
428   g_return_if_fail (EMPATHY_IS_INDIVIDUAL_MANAGER (self));
429   g_return_if_fail (EMPATHY_IS_CONTACT (contact));
430
431   priv = GET_PRIV (self);
432
433   /* We need to ref the contact since otherwise its linked TpHandle will be
434    * destroyed. */
435   g_object_ref (contact);
436
437   DEBUG ("adding individual from contact %s (%s)",
438       empathy_contact_get_id (contact), empathy_contact_get_alias (contact));
439
440   account = empathy_contact_get_account (contact);
441   store_id = tp_proxy_get_object_path (TP_PROXY (account));
442
443   /* Get the persona store to use */
444   backend_store = folks_backend_store_dup ();
445   backend =
446       folks_backend_store_dup_backend_by_name (backend_store, "telepathy");
447
448   if (backend == NULL)
449     {
450       g_warning ("Failed to add individual from contact: couldn't get "
451           "'telepathy' backend");
452       goto finish;
453     }
454
455   persona_stores = folks_backend_get_persona_stores (backend);
456   persona_store = gee_map_get (persona_stores, store_id);
457
458   if (persona_store == NULL)
459     {
460       g_warning ("Failed to add individual from contact: couldn't get persona "
461           "store '%s'", store_id);
462       goto finish;
463     }
464
465   details = tp_asv_new (
466       "contact", G_TYPE_STRING, empathy_contact_get_id (contact),
467       NULL);
468
469   folks_individual_aggregator_add_persona_from_details (
470       priv->aggregator, NULL, persona_store, details,
471       aggregator_add_persona_from_details_cb, contact);
472
473   g_hash_table_destroy (details);
474   g_object_unref (persona_store);
475
476 finish:
477   tp_clear_object (&backend);
478   tp_clear_object (&backend_store);
479 }
480
481 static void
482 aggregator_remove_individual_cb (GObject *source,
483     GAsyncResult *result,
484     gpointer user_data)
485 {
486   FolksIndividualAggregator *aggregator = FOLKS_INDIVIDUAL_AGGREGATOR (source);
487   GError *error = NULL;
488
489   folks_individual_aggregator_remove_individual_finish (
490       aggregator, result, &error);
491   if (error != NULL)
492     {
493       g_warning ("failed to remove individual: %s", error->message);
494       g_clear_error (&error);
495     }
496 }
497
498 /**
499  * Removes the inner contact from the server (and thus the Individual). Not
500  * meant for de-shelling inner personas from an Individual.
501  */
502 void
503 empathy_individual_manager_remove (EmpathyIndividualManager *self,
504     FolksIndividual *individual,
505     const gchar *message)
506 {
507   EmpathyIndividualManagerPriv *priv;
508
509   g_return_if_fail (EMPATHY_IS_INDIVIDUAL_MANAGER (self));
510   g_return_if_fail (FOLKS_IS_INDIVIDUAL (individual));
511
512   priv = GET_PRIV (self);
513
514   DEBUG ("removing individual %s (%s)",
515       folks_individual_get_id (individual),
516       folks_alias_details_get_alias (FOLKS_ALIAS_DETAILS (individual)));
517
518   folks_individual_aggregator_remove_individual (priv->aggregator, individual,
519       aggregator_remove_individual_cb, self);
520 }
521
522 /* FIXME: The parameter @self is not required and the method can be placed in
523  * utilities. I left it as it is to stay coherent with empathy-2.34 */
524 /**
525  * empathy_individual_manager_supports_blocking
526  * @self: the #EmpathyIndividualManager
527  * @individual: an individual to check
528  *
529  * Indicates whether any personas of an @individual can be blocked.
530  *
531  * Returns: %TRUE if any persona supports contact blocking
532  */
533 gboolean
534 empathy_individual_manager_supports_blocking (EmpathyIndividualManager *self,
535     FolksIndividual *individual)
536 {
537   GeeSet *personas;
538   GeeIterator *iter;
539   gboolean retval = FALSE;
540
541   g_return_val_if_fail (EMPATHY_IS_INDIVIDUAL_MANAGER (self), FALSE);
542
543   personas = folks_individual_get_personas (individual);
544   iter = gee_iterable_iterator (GEE_ITERABLE (personas));
545   while (!retval && gee_iterator_next (iter))
546     {
547       TpfPersona *persona = gee_iterator_get (iter);
548       TpConnection *conn;
549
550       if (TPF_IS_PERSONA (persona))
551         {
552           TpContact *tp_contact;
553
554           tp_contact = tpf_persona_get_contact (persona);
555           if (tp_contact != NULL)
556             {
557               conn = tp_contact_get_connection (tp_contact);
558
559               if (tp_proxy_has_interface_by_id (conn,
560                     TP_IFACE_QUARK_CONNECTION_INTERFACE_CONTACT_BLOCKING))
561                 retval = TRUE;
562             }
563         }
564       g_clear_object (&persona);
565     }
566   g_clear_object (&iter);
567
568   return retval;
569 }
570
571 void
572 empathy_individual_manager_set_blocked (EmpathyIndividualManager *self,
573     FolksIndividual *individual,
574     gboolean blocked,
575     gboolean abusive)
576 {
577   GeeSet *personas;
578   GeeIterator *iter;
579
580   g_return_if_fail (EMPATHY_IS_INDIVIDUAL_MANAGER (self));
581
582   personas = folks_individual_get_personas (individual);
583   iter = gee_iterable_iterator (GEE_ITERABLE (personas));
584   while (gee_iterator_next (iter))
585     {
586       TpfPersona *persona = gee_iterator_get (iter);
587
588       if (TPF_IS_PERSONA (persona))
589         {
590           TpContact *tp_contact;
591           TpConnection *conn;
592
593           tp_contact = tpf_persona_get_contact (persona);
594           if (tp_contact == NULL)
595             continue;
596
597           conn = tp_contact_get_connection (tp_contact);
598
599           if (!tp_proxy_has_interface_by_id (conn,
600                 TP_IFACE_QUARK_CONNECTION_INTERFACE_CONTACT_BLOCKING))
601             continue;
602
603           if (blocked)
604             tp_contact_block_async (tp_contact, abusive, NULL, NULL);
605           else
606             tp_contact_unblock_async (tp_contact, NULL, NULL);
607         }
608       g_clear_object (&persona);
609     }
610   g_clear_object (&iter);
611 }
612
613 static void
614 groups_change_group_cb (GObject *source,
615     GAsyncResult *result,
616     gpointer user_data)
617 {
618   FolksGroupDetails *group_details = FOLKS_GROUP_DETAILS (source);
619   GError *error = NULL;
620
621   folks_group_details_change_group_finish (group_details, result, &error);
622   if (error != NULL)
623     {
624       g_warning ("failed to change group: %s", error->message);
625       g_clear_error (&error);
626     }
627 }
628
629 static void
630 remove_group_cb (const gchar *id,
631     FolksIndividual *individual,
632     const gchar *group)
633 {
634   folks_group_details_change_group (FOLKS_GROUP_DETAILS (individual), group,
635       FALSE, groups_change_group_cb, NULL);
636 }
637
638 void
639 empathy_individual_manager_remove_group (EmpathyIndividualManager *manager,
640     const gchar *group)
641 {
642   EmpathyIndividualManagerPriv *priv;
643
644   g_return_if_fail (EMPATHY_IS_INDIVIDUAL_MANAGER (manager));
645   g_return_if_fail (group != NULL);
646
647   priv = GET_PRIV (manager);
648
649   DEBUG ("removing group %s", group);
650
651   /* Remove every individual from the group */
652   g_hash_table_foreach (priv->individuals, (GHFunc) remove_group_cb,
653       (gpointer) group);
654 }
655
656 static void
657 link_personas_cb (FolksIndividualAggregator *aggregator,
658     GAsyncResult *async_result,
659     gpointer user_data)
660 {
661   GError *error = NULL;
662
663   folks_individual_aggregator_link_personas_finish (aggregator, async_result,
664       &error);
665
666   if (error != NULL)
667     {
668       g_warning ("Failed to link personas: %s", error->message);
669       g_clear_error (&error);
670     }
671 }
672
673 void
674 empathy_individual_manager_link_personas (EmpathyIndividualManager *self,
675     GeeSet *personas)
676 {
677   EmpathyIndividualManagerPriv *priv;
678
679   g_return_if_fail (EMPATHY_IS_INDIVIDUAL_MANAGER (self));
680   g_return_if_fail (personas != NULL);
681
682   priv = GET_PRIV (self);
683
684   DEBUG ("Linking %u personas",
685       gee_collection_get_size (GEE_COLLECTION (personas)));
686
687   folks_individual_aggregator_link_personas (priv->aggregator, personas,
688       (GAsyncReadyCallback) link_personas_cb, NULL);
689 }
690
691 static void
692 unlink_individual_cb (FolksIndividualAggregator *aggregator,
693     GAsyncResult *async_result,
694     gpointer user_data)
695 {
696   GError *error = NULL;
697
698   folks_individual_aggregator_unlink_individual_finish (aggregator,
699       async_result, &error);
700
701   if (error != NULL)
702     {
703       g_warning ("Failed to unlink individual: %s", error->message);
704       g_clear_error (&error);
705     }
706 }
707
708 void
709 empathy_individual_manager_unlink_individual (EmpathyIndividualManager *self,
710     FolksIndividual *individual)
711 {
712   EmpathyIndividualManagerPriv *priv;
713
714   g_return_if_fail (EMPATHY_IS_INDIVIDUAL_MANAGER (self));
715   g_return_if_fail (FOLKS_IS_INDIVIDUAL (individual));
716
717   priv = GET_PRIV (self);
718
719   DEBUG ("Unlinking individual '%s'", folks_individual_get_id (individual));
720
721   folks_individual_aggregator_unlink_individual (priv->aggregator, individual,
722       (GAsyncReadyCallback) unlink_individual_cb, NULL);
723 }