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