Post-release version bump
[platform/upstream/folks.git] / folks / individual-aggregator.vala
1 /*
2  * Copyright (C) 2010 Collabora Ltd.
3  *
4  * This library is free software: you can redistribute it and/or modify
5  * it under the terms of the GNU Lesser General Public License as published by
6  * the Free Software Foundation, either version 2.1 of the License, or
7  * (at your option) any later version.
8  *
9  * This library is distributed in the hope that it will be useful,
10  * but WITHOUT ANY WARRANTY; without even the implied warranty of
11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12  * GNU Lesser General Public License for more details.
13  *
14  * You should have received a copy of the GNU Lesser General Public License
15  * along with this library.  If not, see <http://www.gnu.org/licenses/>.
16  *
17  * Authors:
18  *       Travis Reitter <travis.reitter@collabora.co.uk>
19  */
20
21 using Gee;
22 using GLib;
23
24 /**
25  * Errors from {@link IndividualAggregator}s.
26  */
27 public errordomain Folks.IndividualAggregatorError
28 {
29   /**
30    * Adding a {@link Persona} to a {@link PersonaStore} failed.
31    */
32   ADD_FAILED,
33
34   /**
35    * An operation which required the use of a writeable store failed because no
36    * writeable store was available.
37    *
38    * @since 0.1.13
39    */
40   [Deprecated (since = "0.6.2.1",
41       replacement = "IndividualAggregatorError.NO_PRIMARY_STORE")]
42   NO_WRITEABLE_STORE,
43
44   /**
45    * The {@link PersonaStore} was offline (ie, this is a temporary failure).
46    *
47    * @since 0.3.0
48    */
49   STORE_OFFLINE,
50
51   /**
52    * The {@link PersonaStore} did not support writing to a property which the
53    * user requested to write to, or which was necessary to write to for storing
54    * linking information.
55    *
56    * @since 0.6.2
57    */
58   PROPERTY_NOT_WRITEABLE,
59
60   /**
61    * An operation which required the use of a primary store failed because no
62    * primary store was available.
63    *
64    * @since 0.6.3
65    */
66   NO_PRIMARY_STORE,
67 }
68
69 /**
70  * Stores {@link Individual}s which have been created through
71  * aggregation of all the {@link Persona}s provided by the various
72  * {@link Backend}s.
73  *
74  * This is the main interface for client applications.
75  */
76 public class Folks.IndividualAggregator : Object
77 {
78   private BackendStore _backend_store;
79   private HashMap<string, PersonaStore> _stores;
80   private unowned PersonaStore? _primary_store = null;
81   private HashSet<Backend> _backends;
82   private HashTable<string, Individual> _link_map;
83   private bool _linking_enabled = true;
84   private bool _is_prepared = false;
85   private bool _prepare_pending = false;
86   private Debug _debug;
87   private string _configured_primary_store_type_id;
88   private string _configured_primary_store_id;
89   private static const string _FOLKS_CONFIG_KEY =
90     "/system/folks/backends/primary_store";
91
92   /* The number of persona stores and backends we're waiting to become
93    * quiescent. Once these both reach 0, we should be in a quiescent state.
94    * We have to count both of them so that we can handle the case where one
95    * backend becomes available, and its persona stores all become quiescent,
96    * long before any other backend becomes available. In this case, we want
97    * the aggregator to signal that it's reached a quiescent state only once
98    * all the other backends have also become available. */
99   private uint _non_quiescent_persona_store_count = 0;
100   /* Same for backends. */
101   private uint _non_quiescent_backend_count = 0;
102   private bool _is_quiescent = false;
103   /* We use this to know if the primary PersonaStore has been explicitly
104    * set by the user (either via GConf or an env variable). If that is the
105    * case, we don't want to override it with other PersonaStores that
106    * announce themselves as default (i.e.: default address book from e-d-s). */
107   private bool _user_configured_primary_store = false;
108
109   /**
110    * Whether {@link IndividualAggregator.prepare} has successfully completed for
111    * this aggregator.
112    *
113    * @since 0.3.0
114    */
115   public bool is_prepared
116     {
117       get { return this._is_prepared; }
118     }
119
120   /**
121    * Whether the aggregator has reached a quiescent state. This will happen at
122    * some point after {@link IndividualAggregator.prepare} has successfully
123    * completed for the aggregator. An aggregator is in a quiescent state when
124    * all the {@link PersonaStore}s listed by its backends have reached a
125    * quiescent state.
126    *
127    * It's guaranteed that this property's value will only ever change after
128    * {@link IndividualAggregator.is_prepared} has changed to `true`.
129    *
130    * @since 0.6.2
131    */
132   public bool is_quiescent
133     {
134       get { return this._is_quiescent; }
135     }
136
137   /**
138    * Our configured primary (writeable) store.
139    *
140    * Which one to use is decided (in order or precedence)
141    * by:
142    *
143    * - the FOLKS_PRIMARY_STORE env var (mostly for debugging)
144    * - the GConf key set in _FOLKS_CONFIG_KEY (system set store)
145    * - going with the `key-file` or `eds` store as the fall-back option
146    *
147    * @since 0.5.0
148    */
149   public PersonaStore? primary_store
150     {
151       get { return this._primary_store; }
152     }
153
154   private Map<string, Individual> _individuals;
155   private Map<string, Individual> _individuals_ro;
156
157   /**
158    * A map from {@link Individual.id}s to their {@link Individual}s.
159    *
160    * This is the canonical set of {@link Individual}s provided by this
161    * IndividualAggregator.
162    *
163    * {@link Individual}s may be added or removed using
164    * {@link IndividualAggregator.add_persona_from_details} and
165    * {@link IndividualAggregator.remove_individual}, respectively.
166    *
167    * @since 0.5.1
168    */
169   public Map<string, Individual> individuals
170     {
171       get { return this._individuals_ro; }
172       private set
173         {
174           this._individuals = value;
175           this._individuals_ro = this._individuals.read_only_view;
176         }
177     }
178
179   /**
180    * The {@link Individual} representing the user.
181    *
182    * If it exists, this holds the {@link Individual} who is the user: the
183    * {@link Individual} containing the {@link Persona}s who are the owners of
184    * the accounts for their respective backends.
185    *
186    * @since 0.3.0
187    */
188   public Individual? user { get; private set; }
189
190   /**
191    * Emitted when one or more {@link Individual}s are added to or removed from
192    * the aggregator.
193    *
194    * If more information about the relationships between {@link Individual}s
195    * which have been linked and unlinked is needed, consider connecting to
196    * {@link IndividualAggregator.individuals_changed_detailed} instead, which is
197    * emitted at the same time as this signal.
198    *
199    * This will not be emitted until after {@link IndividualAggregator.prepare}
200    * has been called.
201    *
202    * @param added a list of {@link Individual}s which have been added
203    * @param removed a list of {@link Individual}s which have been removed
204    * @param message a string message from the backend, if any
205    * @param actor the {@link Persona} who made the change, if known
206    * @param reason the reason for the change
207    *
208    * @since 0.5.1
209    */
210   [Deprecated (since = "0.6.2",
211       replacement = "IndividualAggregator.individuals_changed_detailed")]
212   public signal void individuals_changed (Set<Individual> added,
213       Set<Individual> removed,
214       string? message,
215       Persona? actor,
216       GroupDetails.ChangeReason reason);
217
218   /**
219    * Emitted when one or more {@link Individual}s are added to or removed from
220    * the aggregator.
221    *
222    * This is emitted at the same time as
223    * {@link IndividualAggregator.individuals_changed}, but includes more
224    * information about the relationships between {@link Individual}s which have
225    * been linked and unlinked.
226    *
227    * Individuals which have been linked will be listed in the multi-map as
228    * mappings from the old individuals to the single new individual which
229    * replaces them (i.e. each of the old individuals will map to the same new
230    * individual). This new individual is the one which will be specified as the
231    * `replacement_individual` in the {@link Individual.removed} signal for the
232    * old individuals.
233    *
234    * Individuals which have been unlinked will be listed in the multi-map as
235    * a mapping from the unlinked individual to a set of one or more individuals
236    * which replace it.
237    *
238    * Individuals which have been added will be listed in the multi-map as a
239    * mapping from `null` to the set of added individuals. If `null` doesn't
240    * map to anything, no individuals have been added to the aggregator.
241    *
242    * Individuals which have been removed will be listed in the multi-map as
243    * mappings from the removed individual to `null`.
244    *
245    * This will not be emitted until after {@link IndividualAggregator.prepare}
246    * has been called.
247    *
248    * @param changes a mapping of old {@link Individual}s to new
249    * {@link Individual}s for the individuals which have changed in the
250    * aggregator
251    *
252    * @since 0.6.2
253    */
254   public signal void individuals_changed_detailed (
255       MultiMap<Individual?, Individual?> changes);
256
257   /* FIXME: make this a singleton? */
258   /**
259    * Create a new IndividualAggregator.
260    *
261    * Clients should connect to the
262    * {@link IndividualAggregator.individuals_changed} signal (or the
263    * {@link IndividualAggregator.individuals_changed_detailed} signal), then
264    * call {@link IndividualAggregator.prepare} to load the backends and start
265    * aggregating individuals.
266    *
267    * An example of how to set up an IndividualAggregator:
268    * {{{
269    *   IndividualAggregator agg = new IndividualAggregator ();
270    *   agg.individuals_changed_detailed.connect (individuals_changed_cb);
271    *   agg.prepare ();
272    * }}}
273    */
274   public IndividualAggregator ()
275     {
276       Object ();
277     }
278
279   construct
280     {
281       this._stores = new HashMap<string, PersonaStore> ();
282       this._individuals = new HashMap<string, Individual> ();
283       this._individuals_ro = this._individuals.read_only_view;
284       this._link_map = new HashTable<string, Individual> (str_hash, str_equal);
285
286       this._backends = new HashSet<Backend> ();
287       this._debug = Debug.dup ();
288       this._debug.print_status.connect (this._debug_print_status);
289
290       /* Check out the configured primary store */
291       var store_config_ids = Environment.get_variable ("FOLKS_PRIMARY_STORE");
292       if (store_config_ids == null)
293         {
294           store_config_ids = Environment.get_variable ("FOLKS_WRITEABLE_STORE");
295           if (store_config_ids != null)
296             {
297               var deprecated_warn = "FOLKS_WRITEABLE_STORE is deprecated, ";
298               deprecated_warn += "use FOLKS_PRIMARY_STORE";
299               warning (deprecated_warn);
300             }
301         }
302
303       if (store_config_ids != null)
304         {
305           debug ("Setting primary store IDs from environment variable.");
306           this._configure_primary_store ((!) store_config_ids);
307         }
308       else
309         {
310           debug ("Setting primary store IDs to defaults.");
311           if (BuildConf.HAVE_EDS)
312             {
313               this._configured_primary_store_type_id = "eds";
314               this._configured_primary_store_id = "system";
315             }
316           else
317             {
318               this._configured_primary_store_type_id = "key-file";
319               this._configured_primary_store_id = "";
320             }
321
322           try
323             {
324               unowned GConf.Client client = GConf.Client.get_default ();
325               GConf.Value? val = client.get (this._FOLKS_CONFIG_KEY);
326               if (val != null)
327                 {
328                   string? val_str = ((!) val).get_string ();
329
330                   if (val_str != null)
331                     {
332                       debug ("Setting primary store IDs from GConf.");
333                       this._configure_primary_store ((!) val_str);
334                     }
335                 }
336             }
337           catch (GLib.Error e)
338             {
339               /* We ignore errors and go with the default store */
340             }
341         }
342
343       debug ("Primary store IDs are '%s' and '%s'.",
344           this._configured_primary_store_type_id,
345           this._configured_primary_store_id);
346
347       var disable_linking = Environment.get_variable ("FOLKS_DISABLE_LINKING");
348       if (disable_linking != null)
349         disable_linking = ((!) disable_linking).strip ().down ();
350       this._linking_enabled = (disable_linking == null ||
351           disable_linking == "no" || disable_linking == "0");
352
353       this._backend_store = BackendStore.dup ();
354
355       debug ("Constructing IndividualAggregator %p", this);
356     }
357
358   ~IndividualAggregator ()
359     {
360       debug ("Destroying IndividualAggregator %p", this);
361
362       this._backend_store.backend_available.disconnect (
363           this._backend_available_cb);
364
365       this._debug.print_status.disconnect (this._debug_print_status);
366     }
367
368   private void _configure_primary_store (string store_config_ids)
369     {
370       debug ("_configure_primary_store to '%s'", store_config_ids);
371       this._user_configured_primary_store = true;
372
373       if (store_config_ids.index_of (":") != -1)
374         {
375           var ids = store_config_ids.split (":", 2);
376           this._configured_primary_store_type_id = ids[0];
377           this._configured_primary_store_id = ids[1];
378         }
379       else
380         {
381           this._configured_primary_store_type_id = store_config_ids;
382           this._configured_primary_store_id = "";
383         }
384     }
385
386   private void _debug_print_status (Debug debug)
387     {
388       const string domain = Debug.STATUS_LOG_DOMAIN;
389       const LogLevelFlags level = LogLevelFlags.LEVEL_INFO;
390
391       debug.print_heading (domain, level, "IndividualAggregator (%p)", this);
392       debug.print_key_value_pairs (domain, level,
393           "Ref. count", this.ref_count.to_string (),
394           "Primary store", "%p".printf (this._primary_store),
395           "Configured store type id", this._configured_primary_store_type_id,
396           "Configured store id", this._configured_primary_store_id,
397           "Linking enabled?", this._linking_enabled ? "yes" : "no",
398           "Prepared?", this._is_prepared ? "yes" : "no",
399           "Quiescent?", this._is_quiescent
400               ? "yes"
401               : "no (%u backends, %u persona stores left)".printf (
402                   this._non_quiescent_backend_count,
403                   this._non_quiescent_persona_store_count)
404       );
405
406       debug.print_line (domain, level,
407           "%u Individuals:", this.individuals.size);
408       debug.indent ();
409
410       foreach (var individual in this.individuals.values)
411         {
412           string? trust_level = null;
413
414           switch (individual.trust_level)
415             {
416               case TrustLevel.NONE:
417                 trust_level = "none";
418                 break;
419               case TrustLevel.PERSONAS:
420                 trust_level = "personas";
421                 break;
422               default:
423                 assert_not_reached ();
424             }
425
426           debug.print_heading (domain, level, "Individual (%p)", individual);
427           debug.print_key_value_pairs (domain, level,
428               "Ref. count", individual.ref_count.to_string (),
429               "ID", individual.id,
430               "User?", individual.is_user ? "yes" : "no",
431               "Trust level", trust_level
432           );
433           debug.print_line (domain, level, "%u Personas:",
434               individual.personas.size);
435
436           debug.indent ();
437
438           foreach (var persona in individual.personas)
439             {
440               debug.print_heading (domain, level, "Persona (%p)", persona);
441               debug.print_key_value_pairs (domain, level,
442                   "Ref. count", persona.ref_count.to_string (),
443                   "UID", persona.uid,
444                   "IID", persona.iid,
445                   "Display ID", persona.display_id,
446                   "User?", persona.is_user ? "yes" : "no"
447               );
448             }
449
450           debug.unindent ();
451         }
452
453       debug.unindent ();
454
455       debug.print_line (domain, level, "%u entries in the link map:",
456           this._link_map.size ());
457       debug.indent ();
458
459       var iter = HashTableIter<string, Individual> (this._link_map);
460       string link_key;
461       Individual individual;
462       while (iter.next (out link_key, out individual) == true)
463         {
464           debug.print_line (domain, level,
465               "%s → %p", link_key, individual);
466         }
467
468       debug.unindent ();
469
470       debug.print_line (domain, level, "");
471     }
472
473   /**
474    * Prepare the IndividualAggregator for use.
475    *
476    * This loads all the available backends and prepares them for use by the
477    * IndividualAggregator. This should be called //after// connecting to the
478    * {@link IndividualAggregator.individuals_changed} signal (or
479    * {@link IndividualAggregator.individuals_changed_detailed} signal), or a
480    * race condition could occur, with the signal being emitted before your code
481    * has connected to them, and {@link Individual}s getting "lost" as a result.
482    *
483    * This function is guaranteed to be idempotent (since version 0.3.0).
484    *
485    * Concurrent calls to this function from different threads will block until
486    * preparation has completed. However, concurrent calls to this function from
487    * a single thread might not, i.e. the first call will block but subsequent
488    * calls might return before the first one. (Though they will be safe in every
489    * other respect.)
490    *
491    * @since 0.1.11
492    */
493   public async void prepare () throws GLib.Error
494     {
495       /* Once this async function returns, all the {@link Backend}s will have
496        * been prepared (though no {@link PersonaStore}s are guaranteed to be
497        * available yet). This last guarantee is new as of version 0.2.0. */
498
499       lock (this._is_prepared)
500         {
501           if (this._is_prepared || this._prepare_pending)
502             {
503               return;
504             }
505
506           try
507             {
508               this._prepare_pending = true;
509
510               this._backend_store.backend_available.connect (
511                   this._backend_available_cb);
512
513               /* Load any backends which already exist. This could happen if the
514                * BackendStore has stayed alive after being used by a previous
515                * IndividualAggregator instance. */
516               var backends = this._backend_store.enabled_backends.values;
517               foreach (var backend in backends)
518                 {
519                   this._backend_available_cb (this._backend_store, backend);
520                 }
521
522               /* Load any backends which haven't been loaded already. (Typically
523                * all of them.) */
524               yield this._backend_store.load_backends ();
525
526               this._is_prepared = true;
527               this.notify_property ("is-prepared");
528
529               /* Mark the aggregator as having reached a quiescent state if
530                * appropriate. This will typically only happen here in cases
531                * where the stores were all prepared and quiescent before the
532                * aggregator was created. */
533               if (this._is_quiescent == false)
534                 {
535                   this._notify_if_is_quiescent ();
536                 }
537             }
538           finally
539             {
540               this._prepare_pending = false;
541             }
542         }
543     }
544
545   /**
546    * Get all matches for a given {@link Individual}.
547    *
548    * @param matchee the individual to find matches for
549    * @param min_threshold the threshold for accepting a match
550    * @return a map from matched individuals to the degree with which they match
551    * `matchee` (which is guaranteed to at least equal `min_threshold`);
552    * if no matches could be found, an empty map is returned
553    *
554    * @since 0.5.1
555    */
556   public Map<Individual, MatchResult> get_potential_matches
557       (Individual matchee, MatchResult min_threshold = MatchResult.VERY_HIGH)
558     {
559       HashMap<Individual, MatchResult> matches =
560           new HashMap<Individual, MatchResult> ();
561       Folks.PotentialMatch matchObj = new Folks.PotentialMatch ();
562
563       foreach (var i in this._individuals.values)
564         {
565           if (i.id == matchee.id)
566                 continue;
567
568           var result = matchObj.potential_match (i, matchee);
569           if (result >= min_threshold)
570             {
571               matches.set (i, result);
572             }
573         }
574
575       return matches;
576     }
577
578   /**
579    * Get all combinations between all {@link Individual}s.
580    *
581    * @param min_threshold the threshold for accepting a match
582    * @return a map from each individual in the aggregator to a map of the
583    * other individuals in the aggregator which can be matched with that
584    * individual, mapped to the degree with which they match the original
585    * individual (which is guaranteed to at least equal `min_threshold`)
586    *
587    * @since 0.5.1
588    */
589   public Map<Individual, Map<Individual, MatchResult>>
590       get_all_potential_matches
591         (MatchResult min_threshold = MatchResult.VERY_HIGH)
592     {
593       HashMap<Individual, HashMap<Individual, MatchResult>> matches =
594         new HashMap<Individual, HashMap<Individual, MatchResult>> ();
595       var individuals = this._individuals.values.to_array ();
596       Folks.PotentialMatch matchObj = new Folks.PotentialMatch ();
597
598       for (var i = 0; i < individuals.length; i++)
599         {
600           var a = individuals[i];
601
602           HashMap<Individual, MatchResult>? _matches_a = matches.get (a);
603           HashMap<Individual, MatchResult> matches_a;
604           if (_matches_a == null)
605             {
606               matches_a = new HashMap<Individual, MatchResult> ();
607               matches.set (a, matches_a);
608             }
609           else
610             {
611               matches_a = (!) _matches_a;
612             }
613
614           for (var f = i + 1; f < individuals.length; f++)
615             {
616               var b = individuals[f];
617
618               HashMap<Individual, MatchResult>? _matches_b = matches.get (b);
619               HashMap<Individual, MatchResult> matches_b;
620               if (_matches_b == null)
621                 {
622                   matches_b = new HashMap<Individual, MatchResult> ();
623                   matches.set (b, matches_b);
624                 }
625               else
626                 {
627                   matches_b = (!) _matches_b;
628                 }
629
630               var result = matchObj.potential_match (a, b);
631
632               if (result >= min_threshold)
633                 {
634                   matches_a.set (b, result);
635                   matches_b.set (a, result);
636                 }
637             }
638         }
639
640       return matches;
641     }
642
643   private async void _add_backend (Backend backend)
644     {
645       if (!this._backends.contains (backend))
646         {
647           this._backends.add (backend);
648
649           backend.persona_store_added.connect (
650               this._backend_persona_store_added_cb);
651           backend.persona_store_removed.connect (
652               this._backend_persona_store_removed_cb);
653           backend.notify["is-quiescent"].connect (
654               this._backend_is_quiescent_changed_cb);
655
656           /* Handle the stores that have already been signaled. Since
657            * this might change while we are looping, get a copy first.
658            */
659           var stores = backend.persona_stores.values.to_array ();
660           foreach (var persona_store in stores)
661               {
662                 this._backend_persona_store_added_cb (backend, persona_store);
663               }
664         }
665     }
666
667   private void _backend_available_cb (BackendStore backend_store,
668       Backend backend)
669     {
670       /* Increase the number of non-quiescent backends we're waiting for.
671        * If we've already reached a quiescent state, this is ignored. If we
672        * haven't, this delays us reaching a quiescent state until the
673        * _backend_is_quiescent_changed_cb() callback is called for this
674        * backend. */
675       if (backend.is_quiescent == false)
676         {
677           this._non_quiescent_backend_count++;
678         }
679
680       this._add_backend.begin (backend);
681     }
682
683   private void _set_primary_store (PersonaStore store)
684     {
685       debug ("_set_primary_store()");
686
687       if (this._primary_store == store)
688         return;
689
690       /* We use the configured PersonaStore as the primary PersonaStore.
691        *
692        * If the type_id is `eds` we *must* know the actual store
693        * (address book) we are talking about or we might end up using
694        * a random store on every run.
695        */
696       if (store.type_id == this._configured_primary_store_type_id)
697         {
698           if ((store.type_id != "eds" &&
699                   this._configured_primary_store_id == "") ||
700               this._configured_primary_store_id == store.id)
701             {
702               debug ("Setting primary store to %p (type ID: %s, ID: %s)",
703                   store, store.type_id, store.id);
704
705               var previous_store = this._primary_store;
706               this._primary_store = store;
707
708               store.freeze_notify ();
709               if (previous_store != null)
710                 {
711                   ((!) previous_store).freeze_notify ();
712                   ((!) previous_store).is_primary_store = false;
713                 }
714               store.is_primary_store = true;
715               if (previous_store != null)
716                 ((!) previous_store).thaw_notify ();
717               store.thaw_notify ();
718
719               this.notify_property ("primary-store");
720             }
721         }
722     }
723
724   private void _backend_persona_store_added_cb (Backend backend,
725       PersonaStore store)
726     {
727       debug ("_backend_persona_store_added_cb(): backend: %s, store: %s (%p)",
728           backend.name, store.id, store);
729
730       var store_id = this._get_store_full_id (store.type_id, store.id);
731
732       this._maybe_configure_as_primary (store);
733       this._set_primary_store (store);
734
735       this._stores.set (store_id, store);
736       store.personas_changed.connect (this._personas_changed_cb);
737       store.notify["is-primary-store"].connect (
738           this._is_primary_store_changed_cb);
739       store.notify["is-quiescent"].connect (
740           this._persona_store_is_quiescent_changed_cb);
741       store.notify["is-user-set-default"].connect (
742           this._persona_store_is_user_set_default_changed_cb);
743
744       /* Increase the number of non-quiescent persona stores we're waiting for.
745        * If we've already reached a quiescent state, this is ignored. If we
746        * haven't, this delays us reaching a quiescent state until the
747        * _persona_store_is_quiescent_changed_cb() callback is called for this
748        * store. */
749       if (store.is_quiescent == false)
750         {
751           this._non_quiescent_persona_store_count++;
752         }
753
754       /* Handle any pre-existing personas in the store. This can happen if the
755        * store existed (and was prepared) before this IndividualAggregator was
756        * constructed. */
757       if (store.personas.size > 0)
758         {
759           var persona_set = new HashSet<Persona> ();
760           foreach (var p in store.personas.values)
761             {
762               persona_set.add (p);
763             }
764
765           this._personas_changed_cb (store, persona_set,
766               new HashSet<Persona> (), null, null,
767               GroupDetails.ChangeReason.NONE);
768         }
769
770       /* Prepare the store and receive a load of other personas-changed
771        * signals. */
772       store.prepare.begin ((obj, result) =>
773         {
774           try
775             {
776               store.prepare.end (result);
777             }
778           catch (GLib.Error e)
779             {
780               /* Translators: the first parameter is a persona store identifier
781                * and the second is an error message. */
782               warning (_("Error preparing persona store '%s': %s"), store_id,
783                   e.message);
784             }
785         });
786     }
787
788   private void _backend_persona_store_removed_cb (Backend backend,
789       PersonaStore store)
790     {
791       store.personas_changed.disconnect (this._personas_changed_cb);
792       store.notify["is-quiescent"].disconnect (
793           this._persona_store_is_quiescent_changed_cb);
794       store.notify["is-primary-store"].disconnect (
795           this._is_primary_store_changed_cb);
796       store.notify["is-user-set-default"].disconnect (
797           this._persona_store_is_user_set_default_changed_cb);
798
799       /* If we were still waiting on this persona store to reach a quiescent
800        * state, stop waiting. */
801       if (this._is_quiescent == false && store.is_quiescent == false)
802         {
803           this._non_quiescent_persona_store_count--;
804           this._notify_if_is_quiescent ();
805         }
806
807       /* no need to remove this store's personas from all the individuals, since
808        * they'll do that themselves (and emit their own 'removed' signal if
809        * necessary) */
810
811       if (this._primary_store == store)
812         {
813           debug ("Unsetting primary store as store %p (type ID: %s, ID: %s) " +
814               "has been removed", store, store.type_id, store.id);
815           this._primary_store = null;
816           this.notify_property ("primary-store");
817         }
818       this._stores.unset (this._get_store_full_id (store.type_id, store.id));
819     }
820
821   private string _get_store_full_id (string type_id, string id)
822     {
823       return type_id + ":" + id;
824     }
825
826   /* Emit the individuals-changed signal ensuring that null parameters are
827    * turned into empty sets, and both sets passed to signal handlers are
828    * read-only. */
829   private void _emit_individuals_changed (Set<Individual>? added,
830       Set<Individual>? removed,
831       MultiMap<Individual?, Individual?>? changes,
832       string? message = null,
833       Persona? actor = null,
834       GroupDetails.ChangeReason reason = GroupDetails.ChangeReason.NONE)
835     {
836       Set<Individual> _added;
837       Set<Individual> _removed;
838       MultiMap<Individual?, Individual?> _changes;
839
840       if ((added == null || ((!) added).size == 0) &&
841           (removed == null || ((!) removed).size == 0) &&
842           (changes == null || ((!) changes).size == 0))
843         {
844           /* Don't bother emitting it if nothing's changed */
845           return;
846         }
847
848       _added = (added != null) ? (!) added : new HashSet<Individual> ();
849       _removed = (removed != null) ? (!) removed : new HashSet<Individual> ();
850
851       if (changes != null)
852         {
853           _changes = (!) changes;
854         }
855       else
856         {
857           _changes = new HashMultiMap<Individual?, Individual?> ();
858         }
859
860       /* Debug output. */
861       if (this._debug.debug_output_enabled == true)
862         {
863           debug ("Emitting individuals-changed-detailed with %u mappings:",
864               _changes.size);
865
866           foreach (var removed_ind in _changes.get_keys ())
867             {
868               foreach (var added_ind in _changes.get (removed_ind))
869                 {
870                   debug ("    %s (%p) → %s (%p)",
871                       (removed_ind != null) ? ((!) removed_ind).id : "",
872                       removed_ind,
873                       (added_ind != null) ? ((!) added_ind).id : "", added_ind);
874
875                   if (removed_ind != null)
876                     {
877                       debug ("      Removed individual's personas:");
878
879                       foreach (var p in ((!) removed_ind).personas)
880                         {
881                           debug ("        %s (%p)", p.uid, p);
882                         }
883                     }
884
885                   if (added_ind != null)
886                     {
887                       debug ("      Added individual's personas:");
888
889                       foreach (var p in ((!) added_ind).personas)
890                         {
891                           debug ("        %s (%p)", p.uid, p);
892                         }
893                     }
894                 }
895             }
896         }
897
898       this.individuals_changed (_added.read_only_view, _removed.read_only_view,
899           message, actor, reason);
900       this.individuals_changed_detailed (_changes);
901     }
902
903   private void _connect_to_individual (Individual individual)
904     {
905       individual.removed.connect (this._individual_removed_cb);
906       this._individuals.set (individual.id, individual);
907     }
908
909   private void _disconnect_from_individual (Individual individual)
910     {
911       this._individuals.unset (individual.id);
912       individual.removed.disconnect (this._individual_removed_cb);
913     }
914
915   private void _add_personas (Set<Persona> added, ref Individual? user,
916       ref HashMultiMap<Individual?, Individual?> individuals_changes)
917     {
918       foreach (var persona in added)
919         {
920           PersonaStoreTrust trust_level = persona.store.trust_level;
921
922           /* These are the Individuals whose Personas will be linked together
923            * to form the `final_individual`.
924            * Since a given Persona can only be part of one Individual, and the
925            * code in Persona._set_personas() ensures that there are no duplicate
926            * Personas in a given Individual, ensuring that there are no
927            * duplicate Individuals in `candidate_inds` (by using a
928            * HashSet) guarantees that there will be no duplicate Personas
929            * in the `final_individual`. */
930           HashSet<Individual> candidate_inds = new HashSet<Individual> ();
931
932           var final_personas = new HashSet<Persona> ();
933
934           debug ("Aggregating persona '%s' on '%s'.", persona.uid, persona.iid);
935
936           /* If the Persona is the user, we *always* want to link it to the
937            * existing this.user. */
938           if (persona.is_user == true && user != null)
939             {
940               debug ("    Found candidate individual '%s' as user.",
941                   ((!) user).id);
942               candidate_inds.add ((!) user);
943             }
944
945           /* If we don't trust the PersonaStore at all, we can't link the
946            * Persona to any existing Individual */
947           if (trust_level != PersonaStoreTrust.NONE)
948             {
949               Individual? candidate_ind = this._link_map.lookup (persona.iid);
950               if (candidate_ind != null &&
951                   ((!) candidate_ind).trust_level != TrustLevel.NONE &&
952                   !candidate_inds.contains ((!) candidate_ind))
953                 {
954                   debug ("    Found candidate individual '%s' by IID '%s'.",
955                       ((!) candidate_ind).id, persona.iid);
956                   candidate_inds.add ((!) candidate_ind);
957                 }
958             }
959
960           if (persona.store.trust_level == PersonaStoreTrust.FULL)
961             {
962               /* If we trust the PersonaStore the Persona came from, we can
963                * attempt to link based on its linkable properties. */
964               foreach (unowned string foo in persona.linkable_properties)
965                 {
966                   /* FIXME: If we just use string prop_name directly in the
967                    * foreach, Vala doesn't copy it into the closure data, and
968                    * prop_name ends up as NULL. bgo#628336 */
969                   unowned string prop_name = foo;
970
971                   /* FIXME: can't be var because of bgo#638208 */
972                   unowned ObjectClass pclass = persona.get_class ();
973                   if (pclass.find_property (prop_name) == null)
974                     {
975                       warning (
976                           /* Translators: the parameter is a property name. */
977                           _("Unknown property '%s' in linkable property list."),
978                           prop_name);
979                       continue;
980                     }
981
982                   persona.linkable_property_to_links (prop_name, (l) =>
983                     {
984                       unowned string prop_linking_value = l;
985                       Individual? candidate_ind =
986                           this._link_map.lookup (prop_linking_value);
987
988                       if (candidate_ind != null &&
989                           ((!) candidate_ind).trust_level != TrustLevel.NONE &&
990                           !candidate_inds.contains ((!) candidate_ind))
991                         {
992                           debug ("    Found candidate individual '%s' by " +
993                               "linkable property '%s' = '%s'.",
994                               ((!) candidate_ind).id, prop_name,
995                               prop_linking_value);
996                           candidate_inds.add ((!) candidate_ind);
997                         }
998                     });
999                 }
1000             }
1001
1002           /* Ensure the original persona makes it into the final individual */
1003           final_personas.add (persona);
1004
1005           if (candidate_inds.size > 0 && this._linking_enabled == true)
1006             {
1007               /* The Persona's IID or linkable properties match one or more
1008                * linkable fields which are already in the link map, so we link
1009                * together all the Individuals we found to form a new
1010                * final_individual. Later, we remove the Personas from the old
1011                * Individuals so that the Individuals themselves are removed. */
1012               foreach (var individual in candidate_inds)
1013                 {
1014                   final_personas.add_all (individual.personas);
1015                 }
1016             }
1017           else if (candidate_inds.size > 0)
1018             {
1019               debug ("    Linking disabled.");
1020             }
1021           else
1022             {
1023               debug ("    Did not find any candidate individuals.");
1024             }
1025
1026           /* Create the final linked Individual */
1027           var final_individual = new Individual (final_personas);
1028           debug ("    Created new individual '%s' (%p) with personas:",
1029               final_individual.id, final_individual);
1030           foreach (var p in final_personas)
1031             {
1032               debug ("        %s (%p)", p.uid, p);
1033               this._add_persona_to_link_map (p, final_individual);
1034             }
1035
1036           uint num_mappings_added = 0;
1037
1038           foreach (var i in candidate_inds)
1039             {
1040               /* Transitively update the individuals_changes. We have to do this
1041                * in two stages as we can't modify individuals_changes while
1042                * iterating over it. */
1043               var transitive_updates = new HashSet<Individual?> ();
1044
1045               foreach (var k in individuals_changes.get_keys ())
1046                 {
1047                   if (i in individuals_changes.get (k))
1048                     {
1049                       transitive_updates.add (k);
1050                     }
1051                 }
1052
1053               foreach (var k in transitive_updates)
1054                 {
1055                   assert (individuals_changes.remove (k, i) == true);
1056
1057                   /* If we're saying the final_individual is replacing some of
1058                    * these candidate individuals, we don't also want to say that
1059                    * it's been added (by also emitting a mapping from
1060                    * null → final_individual). */
1061                   if (k != null)
1062                     {
1063                       individuals_changes.set (k, final_individual);
1064                       num_mappings_added++;
1065                     }
1066                 }
1067
1068               /* If there were no transitive changes to make, it's because this
1069                * candidate individual existed before this call to
1070                * _add_personas(), so it's safe to say it's being replaced by
1071                * the final_individual. */
1072               if (transitive_updates.size == 0)
1073                 {
1074                   individuals_changes.set (i, final_individual);
1075                   num_mappings_added++;
1076                 }
1077             }
1078
1079           /* If there were no candidate individuals or they were all freshly
1080            * added (i.e. mapped from null → candidate_individual), mark the
1081            * final_individual as added. */
1082           if (num_mappings_added == 0)
1083             {
1084               individuals_changes.set (null, final_individual);
1085             }
1086
1087           /* If the final Individual is the user, set them as such. */
1088           if (final_individual.is_user == true)
1089             user = final_individual;
1090         }
1091     }
1092
1093   private void _persona_linkable_property_changed_cb (Object obj,
1094       ParamSpec pspec)
1095     {
1096       /* The value of one of the linkable properties of one the personas has
1097        * changed, so that persona might require re-linking. We do this in a
1098        * simplistic and hacky way (which should work) by simply treating the
1099        * persona as if it's been removed and re-added. */
1100       var persona = (!) (obj as Persona);
1101
1102       debug ("Linkable property '%s' changed for persona '%s' " +
1103           "(is user: %s, IID: %s).", pspec.name, persona.uid,
1104           persona.is_user ? "yes" : "no", persona.iid);
1105
1106       var persona_set = new HashSet<Persona> ();
1107       persona_set.add (persona);
1108
1109       this._personas_changed_cb (persona.store, persona_set, persona_set,
1110           null, null, GroupDetails.ChangeReason.NONE);
1111     }
1112
1113   private void _connect_to_persona (Persona persona)
1114     {
1115       foreach (var prop_name in persona.linkable_properties)
1116         {
1117           persona.notify[prop_name].connect (
1118               this._persona_linkable_property_changed_cb);
1119         }
1120     }
1121
1122   private void _disconnect_from_persona (Persona persona)
1123     {
1124       foreach (var prop_name in persona.linkable_properties)
1125         {
1126           persona.notify[prop_name].disconnect (
1127               this._persona_linkable_property_changed_cb);
1128         }
1129     }
1130
1131   private void _add_persona_to_link_map (Persona persona, Individual individual)
1132     {
1133       debug ("Connecting to Persona: %s (is user: %s, IID: %s)", persona.uid,
1134           persona.is_user ? "yes" : "no", persona.iid);
1135       debug ("    Mapping to Individual: %s", individual.id);
1136
1137       /* Add the Persona to the link map. Its trust level will be reflected in
1138        * final_individual.trust_level, so other Personas won't be linked against
1139        * it in error if the trust level is NONE. */
1140       this._link_map.replace (persona.iid, individual);
1141
1142       /* Only allow linking on non-IID properties of the Persona if we fully
1143        * trust the PersonaStore it came from. */
1144       if (persona.store.trust_level == PersonaStoreTrust.FULL)
1145         {
1146           debug ("    Inserting links:");
1147
1148           /* Insert maps from the Persona's linkable properties to the
1149            * Individual. */
1150           foreach (unowned string prop_name in persona.linkable_properties)
1151             {
1152               debug ("        %s", prop_name);
1153
1154               /* FIXME: can't be var because of bgo#638208 */
1155               unowned ObjectClass pclass = persona.get_class ();
1156               if (pclass.find_property (prop_name) == null)
1157                 {
1158                   warning (
1159                       /* Translators: the parameter is a property name. */
1160                       _("Unknown property '%s' in linkable property list."),
1161                       prop_name);
1162                   continue;
1163                 }
1164
1165               persona.linkable_property_to_links (prop_name, (l) =>
1166                 {
1167                   unowned string prop_linking_value = l;
1168
1169                   debug ("            %s", prop_linking_value);
1170                   this._link_map.replace (prop_linking_value, individual);
1171                 });
1172             }
1173         }
1174     }
1175
1176   /* We remove individuals as a whole from the link map, rather than iterating
1177    * through the link map keys generated by their personas (as in
1178    * _add_persona_to_link_map()) because the values of the personas' linkable
1179    * properties may well have changed since we added the personas to the link
1180    * map. If that's the case, we don't want to end up leaving stale entries in
1181    * the link map, since that *will* cause problems later on. */
1182   private void _remove_individual_from_link_map (Individual individual)
1183     {
1184       debug ("Removing Individual '%s' from the link map.", individual.id);
1185
1186       var iter = HashTableIter<string, Individual> (this._link_map);
1187       string link_key;
1188       Individual link_individual;
1189
1190       while (iter.next (out link_key, out link_individual) == true)
1191         {
1192           if (link_individual == individual)
1193             {
1194               debug ("    %s → %s (%p)",
1195                   link_key, link_individual.id, link_individual);
1196
1197               iter.remove ();
1198             }
1199         }
1200     }
1201
1202   private void _personas_changed_cb (PersonaStore store,
1203       Set<Persona> added,
1204       Set<Persona> removed,
1205       string? message,
1206       Persona? actor,
1207       GroupDetails.ChangeReason reason)
1208     {
1209       var removed_individuals = new HashSet<Individual> ();
1210       var individuals_changes = new HashMultiMap<Individual?, Individual?> ();
1211       var relinked_personas = new HashSet<Persona> ();
1212       var replaced_individuals = new HashMap<Individual, Individual> ();
1213
1214       /* We store the value of this.user locally and only update it at the end
1215        * of the function to prevent spamming notifications of changes to the
1216        * property. */
1217       var user = this.user;
1218
1219       debug ("Removing Personas:");
1220
1221       foreach (var persona in removed)
1222         {
1223           debug ("    %s (is user: %s, IID: %s)", persona.uid,
1224               persona.is_user ? "yes" : "no", persona.iid);
1225
1226           /* Find the Individual containing the Persona (if any) and mark them
1227            * for removal (any other Personas they have which aren't being
1228            * removed will be re-linked into other Individuals). */
1229           Individual? ind = this._link_map.lookup (persona.iid);
1230           if (ind != null)
1231             {
1232               removed_individuals.add ((!) ind);
1233             }
1234
1235           /* Stop listening to notifications about the persona's linkable
1236            * properties. */
1237           this._disconnect_from_persona (persona);
1238         }
1239
1240       /* Remove the Individuals which were pointed to by the linkable properties
1241        * of the removed Personas. We can then re-link the other Personas in
1242        * those Individuals, since their links may have changed.
1243        * Note that we remove the Individual from this.individuals, meaning that
1244        * _individual_removed_cb() ignores this Individual. This allows us to
1245        * group together the IndividualAggregator.individuals_changed signals
1246        * for all the removed Individuals. */
1247       debug ("Removing Individuals due to removed links:");
1248       foreach (var individual in removed_individuals)
1249         {
1250           /* Ensure we don't remove the same Individual twice */
1251           if (this._individuals.has_key (individual.id) == false)
1252             continue;
1253
1254           debug ("    %s", individual.id);
1255
1256           /* Build a list of Personas which need relinking. Ensure we don't
1257            * include any of the Personas which have just been removed. */
1258           foreach (var persona in individual.personas)
1259             {
1260               if (removed.contains (persona) == true ||
1261                   relinked_personas.contains (persona) == true)
1262                 continue;
1263
1264               relinked_personas.add (persona);
1265             }
1266
1267           if (user == individual)
1268             user = null;
1269
1270           this._disconnect_from_individual (individual);
1271
1272           /* Remove the Individual's links from the link map */
1273           this._remove_individual_from_link_map (individual);
1274         }
1275
1276       debug ("Adding Personas:");
1277       foreach (var persona in added)
1278         {
1279           debug ("    %s (is user: %s, IID: %s)", persona.uid,
1280               persona.is_user ? "yes" : "no", persona.iid);
1281
1282           /* Connect to notifications about the persona's linkable
1283            * properties. */
1284           this._connect_to_persona (persona);
1285         }
1286
1287       if (added.size > 0)
1288         {
1289           this._add_personas (added, ref user, ref individuals_changes);
1290         }
1291
1292       debug ("Relinking Personas:");
1293       foreach (var persona in relinked_personas)
1294         {
1295           debug ("    %s (is user: %s, IID: %s)", persona.uid,
1296               persona.is_user ? "yes" : "no", persona.iid);
1297         }
1298
1299       this._add_personas (relinked_personas, ref user, ref individuals_changes);
1300
1301       /* Work out which final individuals have replaced the removed_individuals
1302        * and update individuals_changes accordingly. */
1303       foreach (var individual in removed_individuals)
1304         {
1305           var added_mapping = false;
1306
1307           foreach (var persona in individual.personas)
1308             {
1309               if (!(persona in removed) || (persona in added))
1310                 {
1311                   individuals_changes.remove (null, persona.individual);
1312                   individuals_changes.set (individual, persona.individual);
1313                   added_mapping = true;
1314                 }
1315             }
1316
1317           /* Has the individual been removed entirely? */
1318           if (added_mapping == false)
1319             {
1320               individuals_changes.set (individual, null);
1321             }
1322
1323           individual.personas = null;
1324         }
1325
1326       /* Notify of changes to this.user */
1327       this.user = user;
1328
1329       /* Signal the addition of new individuals and removal of old ones to the
1330        * aggregator */
1331       if (individuals_changes.size > 0)
1332         {
1333           var added_individuals = new HashSet<Individual> ();
1334
1335           /* Extract the deprecated added and removed sets from
1336            * individuals_changes, to be used in the individuals_changed
1337            * signal. */
1338           foreach (var old_ind in individuals_changes.get_keys ())
1339             {
1340               foreach (var new_ind in individuals_changes.get (old_ind))
1341                 {
1342                   assert (old_ind != null || new_ind != null);
1343
1344                   if (old_ind != null)
1345                     {
1346                       removed_individuals.add ((!) old_ind);
1347                     }
1348
1349                   if (new_ind != null)
1350                     {
1351                       added_individuals.add ((!) new_ind);
1352                       this._connect_to_individual ((!) new_ind);
1353                     }
1354
1355                   if (old_ind != null && new_ind != null)
1356                     {
1357                       replaced_individuals.set ((!) old_ind, (!) new_ind);
1358                     }
1359                 }
1360             }
1361
1362           this._emit_individuals_changed (added_individuals,
1363               removed_individuals, individuals_changes);
1364         }
1365
1366       /* Signal the replacement of various Individuals as a consequence of
1367        * linking. */
1368       debug ("Replacing Individuals due to linking:");
1369       var iter = replaced_individuals.map_iterator ();
1370       while (iter.next () == true)
1371         {
1372           var old_ind = iter.get_key ();
1373           var new_ind = iter.get_value ();
1374
1375           debug ("    %s (%p) → %s (%p)", old_ind.id, old_ind,
1376               new_ind.id, new_ind);
1377
1378           old_ind.replace (new_ind);
1379         }
1380
1381       /* Validate the link map. */
1382       if (this._debug.debug_output_enabled == true)
1383         {
1384           var iter2 = HashTableIter<string, Individual> (this._link_map);
1385           string link_key;
1386           Individual individual;
1387
1388           while (iter2.next (out link_key, out individual) == true)
1389             {
1390               if (this._individuals.get (individual.id) != individual)
1391                 {
1392                   warning ("Link map contains invalid mapping:\n" +
1393                       "    %s → %s (%p)",
1394                           link_key, individual.id, individual);
1395                   warning ("Individual %s (%p) personas:", individual.id,
1396                       individual);
1397                   foreach (var p in individual.personas)
1398                     {
1399                       warning ("    %s (%p)", p.uid, p);
1400                     }
1401                 }
1402             }
1403         }
1404     }
1405
1406   private void _is_primary_store_changed_cb (Object object, ParamSpec pspec)
1407     {
1408       /* Ensure that we only have one primary PersonaStore */
1409       var store = (PersonaStore) object;
1410       assert ((store.is_primary_store == true &&
1411               store == this._primary_store) ||
1412           (store.is_primary_store == false &&
1413               store != this._primary_store));
1414     }
1415
1416   private void _persona_store_is_quiescent_changed_cb (Object obj,
1417       ParamSpec pspec)
1418     {
1419       /* Have we reached a quiescent state yet? */
1420       if (this._non_quiescent_persona_store_count > 0)
1421         {
1422           this._non_quiescent_persona_store_count--;
1423           this._notify_if_is_quiescent ();
1424         }
1425     }
1426
1427   private void _backend_is_quiescent_changed_cb (Object obj, ParamSpec pspec)
1428     {
1429       if (this._non_quiescent_backend_count > 0)
1430         {
1431           this._non_quiescent_backend_count--;
1432           this._notify_if_is_quiescent ();
1433         }
1434     }
1435
1436   private void _notify_if_is_quiescent ()
1437     {
1438       if (this._non_quiescent_backend_count == 0 &&
1439           this._non_quiescent_persona_store_count == 0 &&
1440           this._is_quiescent == false)
1441         {
1442           if (this._configured_primary_store_type_id.length > 0 &&
1443               this._primary_store == null)
1444             {
1445               warning ("Failed to find primary PersonaStore with type ID " +
1446                   "'%s' and ID '%s'.\n" +
1447                   "Individuals will not be linked properly " +
1448                   "and creating new links between Personas will not work.\n" +
1449                   "The configured primary PersonaStore's backend may not be " +
1450                   "installed. If you are unsure, check with your " +
1451                   "distribution.",
1452                   this._configured_primary_store_type_id,
1453                   this._configured_primary_store_id);
1454             }
1455
1456           this._is_quiescent = true;
1457           this.notify_property ("is-quiescent");
1458         }
1459     }
1460
1461   private void _persona_store_is_user_set_default_changed_cb (Object obj,
1462       ParamSpec pspec)
1463     {
1464       var store = (PersonaStore) obj;
1465
1466       debug ("PersonaStore.is-user-set-default changed for store %p " +
1467           "(type ID: %s, ID: %s)", store, store.type_id, store.id);
1468
1469       if (this._maybe_configure_as_primary (store))
1470         this._set_primary_store (store);
1471     }
1472
1473   private bool _maybe_configure_as_primary (PersonaStore store)
1474     {
1475       debug ("_maybe_configure_as_primary()");
1476
1477       var configured = false;
1478
1479       if (!this._user_configured_primary_store &&
1480           store.is_user_set_default)
1481         {
1482           debug ("Setting primary store IDs to '%s' and '%s'.", store.type_id,
1483               store.id);
1484           this._configured_primary_store_type_id = store.type_id;
1485           this._configured_primary_store_id = store.id;
1486           configured = true;
1487         }
1488
1489       return configured;
1490     }
1491
1492   private void _individual_removed_cb (Individual i, Individual? replacement)
1493     {
1494       if (this.user == i)
1495         this.user = null;
1496
1497       /* Only signal if the individual is still in this.individuals. This allows
1498        * us to group removals together in, e.g., _personas_changed_cb(). */
1499       if (this._individuals.get (i.id) != i)
1500         return;
1501
1502       if (replacement != null)
1503         {
1504           debug ("Individual '%s' removed (replaced by '%s')", i.id,
1505               ((!) replacement).id);
1506         }
1507       else
1508         {
1509           debug ("Individual '%s' removed (not replaced)", i.id);
1510         }
1511
1512       /* If the individual has 0 personas, we've already signaled removal */
1513       if (i.personas.size > 0)
1514         {
1515           var changes = new HashMultiMap<Individual?, Individual?> ();
1516           var individuals = new HashSet<Individual> ();
1517
1518           individuals.add (i);
1519           changes.set (i, replacement);
1520
1521           this._emit_individuals_changed (null, individuals, changes);
1522         }
1523
1524       this._disconnect_from_individual (i);
1525     }
1526
1527   /**
1528    * Add a new persona in the given {@link PersonaStore} based on the `details`
1529    * provided.
1530    *
1531    * If the target store is offline, this function will throw
1532    * {@link IndividualAggregatorError.STORE_OFFLINE}. It's the responsibility of
1533    * the caller to cache details and re-try this function if it wishes to make
1534    * offline adds work.
1535    *
1536    * The details hash is a backend-specific mapping of key, value strings.
1537    * Common keys include:
1538    *
1539    *  * contact - service-specific contact ID
1540    *  * message - a user-readable message to pass to the persona being added
1541    *
1542    * If a {@link Persona} with the given details already exists in the store, no
1543    * error will be thrown and this function will return `null`.
1544    *
1545    * @param parent an optional {@link Individual} to add the new {@link Persona}
1546    * to. This persona will be appended to its ordered list of personas.
1547    * @param persona_store the {@link PersonaStore} to add the persona to
1548    * @param details a key-value map of details to use in creating the new
1549    * {@link Persona}
1550    * @return the new {@link Persona} or `null` if the corresponding
1551    * {@link Persona} already existed. If non-`null`, the new {@link Persona}
1552    * will also be added to a new or existing {@link Individual} as necessary.
1553    *
1554    * @since 0.3.5
1555    */
1556   public async Persona? add_persona_from_details (Individual? parent,
1557       PersonaStore persona_store,
1558       HashTable<string, Value?> details) throws IndividualAggregatorError
1559     {
1560       Persona? persona = null;
1561       try
1562         {
1563           var details_copy = this._asv_copy (details);
1564           persona = yield persona_store.add_persona_from_details (details_copy);
1565         }
1566       catch (PersonaStoreError e)
1567         {
1568           if (e is PersonaStoreError.STORE_OFFLINE)
1569             {
1570               throw new IndividualAggregatorError.STORE_OFFLINE (e.message);
1571             }
1572           else
1573             {
1574               var full_id = this._get_store_full_id (persona_store.type_id,
1575                   persona_store.id);
1576
1577               throw new IndividualAggregatorError.ADD_FAILED (
1578                   /* Translators: the first parameter is a store identifier
1579                    * and the second parameter is an error message. */
1580                   _("Failed to add contact for persona store ID '%s': %s"),
1581                   full_id, e.message);
1582             }
1583         }
1584
1585       if (parent != null && persona != null)
1586         {
1587           ((!) parent).personas.add ((!) persona);
1588         }
1589
1590       return persona;
1591     }
1592
1593   private HashTable<string, Value?> _asv_copy (HashTable<string, Value?> asv)
1594     {
1595       var retval = new HashTable<string, Value?> (str_hash, str_equal);
1596
1597       asv.foreach ((k, v) =>
1598         {
1599           retval.insert ((string) k, v);
1600         });
1601
1602       return retval;
1603     }
1604
1605   /**
1606    * Completely remove the individual and all of its personas from their
1607    * backing stores.
1608    *
1609    * @param individual the {@link Individual} to remove
1610    * @since 0.1.11
1611    */
1612   public async void remove_individual (Individual individual) throws GLib.Error
1613     {
1614       /* Removing personas changes the persona set so we need to make a copy
1615        * first */
1616       var personas = new HashSet<Persona> ();
1617       foreach (var p in individual.personas)
1618         {
1619           personas.add (p);
1620         }
1621
1622       foreach (var persona in personas)
1623         {
1624           yield persona.store.remove_persona (persona);
1625         }
1626     }
1627
1628   /**
1629    * Completely remove the persona from its backing store.
1630    *
1631    * This will leave other personas in the same individual alone.
1632    *
1633    * @param persona the {@link Persona} to remove
1634    * @since 0.1.11
1635    */
1636   public async void remove_persona (Persona persona) throws GLib.Error
1637     {
1638       yield persona.store.remove_persona (persona);
1639     }
1640
1641   /**
1642    * Link the given {@link Persona}s together.
1643    *
1644    * Create links between the given {@link Persona}s so that they form a single
1645    * {@link Individual}. The new {@link Individual} will be returned via the
1646    * {@link IndividualAggregator.individuals_changed} signal.
1647    *
1648    * Removal of the {@link Individual}s which the {@link Persona}s were in
1649    * before is signalled by {@link IndividualAggregator.individuals_changed} and
1650    * {@link Individual.removed}.
1651    *
1652    * @param personas the {@link Persona}s to be linked
1653    * @since 0.5.1
1654    */
1655   public async void link_personas (Set<Persona> personas)
1656       throws IndividualAggregatorError
1657     {
1658       if (this._primary_store == null)
1659         {
1660           throw new IndividualAggregatorError.NO_PRIMARY_STORE (
1661               _("Can’t link personas with no primary store.") + "\n" +
1662               _("Persona store ‘%s:%s’ is configured as primary, but could not be found or failed to load.") + "\n" +
1663               _("Check the service providing the persona store is running, or change the default store in that service or using the “%s” GConf key."),
1664               this._configured_primary_store_type_id,
1665               this._configured_primary_store_id, this._FOLKS_CONFIG_KEY);
1666         }
1667
1668       /* Don't bother linking if it's just one Persona */
1669       if (personas.size <= 1)
1670         return;
1671
1672       /* Disallow linking if it's disabled */
1673       if (this._linking_enabled == false)
1674         {
1675           debug ("Can't link Personas: linking disabled.");
1676           return;
1677         }
1678
1679       /* Create a new persona in the primary store which links together the
1680        * given personas */
1681       assert (((!) this._primary_store).type_id ==
1682           this._configured_primary_store_type_id);
1683
1684       /* `protocols_addrs_set` will be passed to the new Kf.Persona */
1685       var protocols_addrs_set = new HashMultiMap<string, ImFieldDetails> (
1686             null, null,
1687             (GLib.HashFunc) ImFieldDetails.hash,
1688             (GLib.EqualFunc) ImFieldDetails.equal);
1689       var web_service_addrs_set =
1690         new HashMultiMap<string, WebServiceFieldDetails> (
1691             null, null,
1692             (GLib.HashFunc) WebServiceFieldDetails.hash,
1693             (GLib.EqualFunc) WebServiceFieldDetails.equal);
1694
1695       /* List of local_ids */
1696       var local_ids = new Gee.HashSet<string> ();
1697
1698       foreach (var persona in personas)
1699         {
1700           if (persona is ImDetails)
1701             {
1702               ImDetails im_details = (ImDetails) persona;
1703
1704               /* protocols_addrs_set = union (all personas' IM addresses) */
1705               foreach (var protocol in im_details.im_addresses.get_keys ())
1706                 {
1707                   var im_addresses = im_details.im_addresses.get (protocol);
1708
1709                   foreach (var im_address in im_addresses)
1710                     {
1711                       protocols_addrs_set.set (protocol, im_address);
1712                     }
1713                 }
1714             }
1715
1716           if (persona is WebServiceDetails)
1717             {
1718               WebServiceDetails ws_details = (WebServiceDetails) persona;
1719
1720               /* web_service_addrs_set = union (all personas' WS addresses) */
1721               foreach (var web_service in
1722                   ws_details.web_service_addresses.get_keys ())
1723                 {
1724                   var ws_addresses =
1725                       ws_details.web_service_addresses.get (web_service);
1726
1727                   foreach (var ws_fd in ws_addresses)
1728                     web_service_addrs_set.set (web_service, ws_fd);
1729                 }
1730             }
1731
1732           if (persona is LocalIdDetails)
1733             {
1734               foreach (var id in ((LocalIdDetails) persona).local_ids)
1735                 {
1736                   local_ids.add (id);
1737                 }
1738             }
1739         }
1740
1741       var details = new HashTable<string, Value?> (str_hash, str_equal);
1742
1743       if (protocols_addrs_set.size > 0)
1744         {
1745           var im_addresses_value = Value (typeof (MultiMap));
1746           im_addresses_value.set_object (protocols_addrs_set);
1747           details.insert (
1748               (!) PersonaStore.detail_key (PersonaDetail.IM_ADDRESSES),
1749               im_addresses_value);
1750         }
1751
1752       if (web_service_addrs_set.size > 0)
1753         {
1754           var web_service_addresses_value = Value (typeof (MultiMap));
1755           web_service_addresses_value.set_object (web_service_addrs_set);
1756           details.insert (
1757               (!) PersonaStore.detail_key (PersonaDetail.WEB_SERVICE_ADDRESSES),
1758               web_service_addresses_value);
1759         }
1760
1761       if (local_ids.size > 0)
1762         {
1763           var local_ids_value = Value (typeof (Set));
1764           local_ids_value.set_object (local_ids);
1765           details.insert (
1766               (!) Folks.PersonaStore.detail_key (PersonaDetail.LOCAL_IDS),
1767               local_ids_value);
1768         }
1769
1770       yield this.add_persona_from_details (null,
1771           (!) this._primary_store, details);
1772     }
1773
1774   /**
1775    * Unlinks the given {@link Individual} into its constituent {@link Persona}s.
1776    *
1777    * This completely unlinks the given {@link Individual}, destroying all of
1778    * its writeable {@link Persona}s.
1779    *
1780    * The {@link Individual}'s removal is signalled by
1781    * {@link IndividualAggregator.individuals_changed} and
1782    * {@link Individual.removed}.
1783    *
1784    * The {@link Persona}s comprising the {@link Individual} will be re-linked
1785    * into one or more new {@link Individual}s, depending on how much linking
1786    * data remains (typically only implicit links remain). The addition of these
1787    * new {@link Individual}s will be signalled by
1788    * {@link IndividualAggregator.individuals_changed}.
1789    *
1790    * @param individual the {@link Individual} to unlink
1791    * @since 0.1.13
1792    */
1793   public async void unlink_individual (Individual individual) throws GLib.Error
1794     {
1795       if (this._linking_enabled == false)
1796         {
1797           debug ("Can't unlink Individual '%s': linking disabled.",
1798               individual.id);
1799           return;
1800         }
1801
1802       debug ("Unlinking Individual '%s', deleting Personas:", individual.id);
1803
1804       /* Remove all the Personas from writeable PersonaStores.
1805        *
1806        * We have to take a copy of the Persona list before removing the
1807        * Personas, as _personas_changed_cb() (which is called as a result of
1808        * calling _primary_store.remove_persona()) messes around with Persona
1809        * lists. */
1810       var personas = new HashSet<Persona> ();
1811       foreach (var p in individual.personas)
1812         {
1813           personas.add (p);
1814         }
1815
1816       foreach (var persona in personas)
1817         {
1818           /* Since persona.store != null, we know that
1819            * this._primary_store != null. */
1820           if (persona.store == this._primary_store)
1821             {
1822               debug ("    %s (is user: %s, IID: %s)", persona.uid,
1823                   persona.is_user ? "yes" : "no", persona.iid);
1824               yield ((!) this._primary_store).remove_persona (persona);
1825             }
1826         }
1827     }
1828
1829   /**
1830    * Ensure that the given property is writeable for the given
1831    * {@link Individual}.
1832    *
1833    * This makes sure that there is at least one {@link Persona} in the
1834    * individual which has `property_name` in its
1835    * {@link Persona.writeable_properties}. If no such persona exists in the
1836    * individual, a new one will be created and linked to the individual. (Note
1837    * that due to the design of the aggregator, this will result in the previous
1838    * individual being removed and replaced by a new one with the new persona;
1839    * listen to the {@link Individual.removed} signal to see the replacement.)
1840    *
1841    * It may not be possible to create a new persona which has the given property
1842    * as writeable. In that case, a
1843    * {@link IndividualAggregatorError.NO_PRIMARY_STORE} or
1844    * {@link IndividualAggregatorError.PROPERTY_NOT_WRITEABLE} error will be
1845    * thrown.
1846    *
1847    * @param individual the individual for which `property_name` should be
1848    * writeable
1849    * @param property_name the name of the property which needs to be writeable
1850    * (this should be in lower case using hyphens, e.g. “web-service-addresses”)
1851    * @return a persona (new or existing) which has the given property as
1852    * writeable
1853    *
1854    * @since 0.6.2
1855    */
1856   public async Persona ensure_individual_property_writeable (
1857       Individual individual, string property_name)
1858       throws IndividualAggregatorError
1859     {
1860       debug ("ensure_individual_property_writeable: %s, %s",
1861           individual.id, property_name);
1862
1863       /* See if the individual already contains the property we want. */
1864       foreach (var p1 in individual.personas)
1865         {
1866           if (property_name in p1.writeable_properties)
1867             {
1868               debug ("    Returning existing persona: %s", p1.uid);
1869               return p1;
1870             }
1871         }
1872
1873       /* Otherwise, create a new persona in the writeable store. If the
1874        * writeable store doesn't exist or doesn't support writing to the given
1875        * property, we try the other persona stores. */
1876       var details = new HashTable<string, Value?> (str_hash, str_equal);
1877       Persona? new_persona = null;
1878
1879       if (this._primary_store != null &&
1880           property_name in
1881               ((!) this._primary_store).always_writeable_properties)
1882         {
1883           try
1884             {
1885               debug ("    Using writeable store");
1886               new_persona = yield this.add_persona_from_details (null,
1887                   (!) this._primary_store, details);
1888             }
1889           catch (IndividualAggregatorError e1)
1890             {
1891               /* Ignore it */
1892               new_persona = null;
1893             }
1894         }
1895
1896       if (new_persona == null)
1897         {
1898           foreach (var s in this._stores.values)
1899             {
1900               if (s == this._primary_store ||
1901                   !(property_name in s.always_writeable_properties))
1902                 {
1903                   /* Skip the store we've just tried */
1904                   continue;
1905                 }
1906
1907               try
1908                 {
1909                   debug ("    Using store %s", s.id);
1910                   new_persona = yield this.add_persona_from_details (null, s,
1911                       details);
1912                 }
1913               catch (IndividualAggregatorError e2)
1914                 {
1915                   /* Ignore it */
1916                   new_persona = null;
1917                   continue;
1918                 }
1919             }
1920         }
1921
1922       /* Throw an error if we haven't managed to find a suitable store */
1923       if (new_persona == null && this._primary_store == null)
1924         {
1925           throw new IndividualAggregatorError.NO_PRIMARY_STORE (
1926               _("Can’t add personas with no primary store.") + "\n" +
1927               _("Persona store ‘%s:%s’ is configured as primary, but could not be found or failed to load.") + "\n" +
1928               _("Check the service providing the persona store is running, or change the default store in that service or using the “%s” GConf key."),
1929               this._configured_primary_store_type_id,
1930               this._configured_primary_store_id, this._FOLKS_CONFIG_KEY);
1931         }
1932       else if (new_persona == null)
1933         {
1934           throw new IndividualAggregatorError.PROPERTY_NOT_WRITEABLE (
1935               _("Can't write to requested property (“%s”) of the writeable store."),
1936               property_name);
1937         }
1938
1939       /* Link the persona to the existing individual. We can guarantee
1940        * new_persona != null because we'd have bailed out above otherwise. */
1941       var linking_personas = new HashSet<Persona> ();
1942       linking_personas.add ((!) new_persona);
1943
1944       foreach (var p2 in individual.personas)
1945         {
1946           linking_personas.add (p2);
1947         }
1948
1949       debug ("    Linking personas to ensure %s property is writeable.",
1950           property_name);
1951       yield this.link_personas (linking_personas);
1952
1953       return (!) new_persona;
1954     }
1955 }