Use the key-file backend for writes in tests for non-writeable backends.
[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   NO_WRITEABLE_STORE,
41
42   /**
43    * The {@link PersonaStore} was offline (ie, this is a temporary failure).
44    *
45    * @since 0.3.0
46    */
47   STORE_OFFLINE,
48 }
49
50 /**
51  * Stores {@link Individual}s which have been created through
52  * aggregation of all the {@link Persona}s provided by the various
53  * {@link Backend}s.
54  *
55  * This is the main interface for client applications.
56  */
57 public class Folks.IndividualAggregator : Object
58 {
59   private BackendStore _backend_store;
60   private HashMap<string, PersonaStore> _stores;
61   private unowned PersonaStore? _writeable_store = null;
62   private HashSet<Backend> _backends;
63   private HashTable<string, Individual> _link_map;
64   private bool _linking_enabled = true;
65   private bool _is_prepared = false;
66   private bool _prepare_pending = false;
67   private Debug _debug;
68   private string _configured_writeable_store_type_id;
69   private string _configured_writeable_store_id;
70   private static const string _FOLKS_CONFIG_KEY =
71     "/system/folks/backends/primary_store";
72
73   /**
74    * Whether {@link IndividualAggregator.prepare} has successfully completed for
75    * this aggregator.
76    *
77    * @since 0.3.0
78    */
79   public bool is_prepared
80     {
81       get { return this._is_prepared; }
82     }
83
84   /**
85    * Our configured primary (writeable) store.
86    *
87    * Which one to use is decided (in order or precedence)
88    * by:
89    *
90    * - the FOLKS_WRITEABLE_STORE env var (mostly for debugging)
91    * - the GConf key set in _FOLKS_CONFIG_KEY (system set store)
92    * - going with the `key-file` or `eds` store as the fall-back option
93    *
94    * @since 0.5.0
95    */
96   public PersonaStore? primary_store
97     {
98       get { return this._writeable_store; }
99     }
100
101   private Map<string, Individual> _individuals;
102   private Map<string, Individual> _individuals_ro;
103
104   /**
105    * A map from {@link Individual.id}s to their {@link Individual}s.
106    *
107    * This is the canonical set of {@link Individual}s provided by this
108    * IndividualAggregator.
109    *
110    * {@link Individual}s may be added or removed using
111    * {@link IndividualAggregator.add_persona_from_details} and
112    * {@link IndividualAggregator.remove_individual}, respectively.
113    *
114    * @since 0.5.1
115    */
116   public Map<string, Individual> individuals
117     {
118       get { return this._individuals_ro; }
119       private set
120         {
121           this._individuals = value;
122           this._individuals_ro = this._individuals.read_only_view;
123         }
124     }
125
126   /**
127    * The {@link Individual} representing the user.
128    *
129    * If it exists, this holds the {@link Individual} who is the user: the
130    * {@link Individual} containing the {@link Persona}s who are the owners of
131    * the accounts for their respective backends.
132    *
133    * @since 0.3.0
134    */
135   public Individual user { get; private set; }
136
137   /**
138    * Emitted when one or more {@link Individual}s are added to or removed from
139    * the aggregator.
140    *
141    * This will not be emitted until after {@link IndividualAggregator.prepare}
142    * has been called.
143    *
144    * @param added a list of {@link Individual}s which have been added
145    * @param removed a list of {@link Individual}s which have been removed
146    * @param message a string message from the backend, if any
147    * @param actor the {@link Persona} who made the change, if known
148    * @param reason the reason for the change
149    *
150    * @since 0.5.1
151    */
152   public signal void individuals_changed (Set<Individual> added,
153       Set<Individual> removed,
154       string? message,
155       Persona? actor,
156       GroupDetails.ChangeReason reason);
157
158   /* FIXME: make this a singleton? */
159   /**
160    * Create a new IndividualAggregator.
161    *
162    * Clients should connect to the
163    * {@link IndividualAggregator.individuals_changed} signal, then call
164    * {@link IndividualAggregator.prepare} to load the backends and start
165    * aggregating individuals.
166    *
167    * An example of how to set up an IndividualAggregator:
168    * {{{
169    *   IndividualAggregator agg = new IndividualAggregator ();
170    *   agg.individuals_changed.connect (individuals_changed_cb);
171    *   agg.prepare ();
172    * }}}
173    */
174   public IndividualAggregator ()
175     {
176       this._stores = new HashMap<string, PersonaStore> ();
177       this._individuals = new HashMap<string, Individual> ();
178       this._individuals_ro = this._individuals.read_only_view;
179       this._link_map = new HashTable<string, Individual> (str_hash, str_equal);
180
181       this._backends = new HashSet<Backend> ();
182       this._debug = Debug.dup ();
183       this._debug.print_status.connect (this._debug_print_status);
184
185       /* Check out the configured writeable store */
186       var store_config_ids = Environment.get_variable ("FOLKS_WRITEABLE_STORE");
187       if (store_config_ids != null)
188         {
189           this._set_writeable_store (store_config_ids);
190         }
191       else
192         {
193 #if ENABLE_EDS
194           this._configured_writeable_store_type_id = "eds";
195           this._configured_writeable_store_id = "system";
196 #else
197           this._configured_writeable_store_type_id = "key-file";
198           this._configured_writeable_store_id = "";
199 #endif
200
201           try
202             {
203               unowned GConf.Client client = GConf.Client.get_default ();
204               GConf.Value? val = client.get (this._FOLKS_CONFIG_KEY);
205               if (val != null)
206                 this._set_writeable_store (val.get_string ());
207             }
208           catch (GLib.Error e)
209             {
210               /* We ignore errors and go with the default store */
211             }
212         }
213
214       var disable_linking = Environment.get_variable ("FOLKS_DISABLE_LINKING");
215       if (disable_linking != null)
216         disable_linking = disable_linking.strip ().down ();
217       this._linking_enabled = (disable_linking == null ||
218           disable_linking == "no" || disable_linking == "0");
219
220       this._backend_store = BackendStore.dup ();
221       this._backend_store.backend_available.connect (
222           this._backend_available_cb);
223     }
224
225   ~IndividualAggregator ()
226     {
227       this._backend_store.backend_available.disconnect (
228           this._backend_available_cb);
229       this._backend_store = null;
230
231       this._debug.print_status.disconnect (this._debug_print_status);
232     }
233
234   private void _set_writeable_store (string store_config_ids)
235     {
236       if (store_config_ids.index_of (":") != -1)
237         {
238           var ids = store_config_ids.split (":", 2);
239           this._configured_writeable_store_type_id = ids[0];
240           this._configured_writeable_store_id = ids[1];
241         }
242       else
243         {
244           this._configured_writeable_store_type_id = store_config_ids;
245           this._configured_writeable_store_id = "";
246         }
247     }
248
249   private void _debug_print_status (Debug debug)
250     {
251       const string domain = Debug.STATUS_LOG_DOMAIN;
252       const LogLevelFlags level = LogLevelFlags.LEVEL_INFO;
253
254       debug.print_heading (domain, level, "IndividualAggregator (%p)", this);
255       debug.print_key_value_pairs (domain, level,
256           "Ref. count", this.ref_count.to_string (),
257           "Writeable store", "%p".printf (this._writeable_store),
258           "Linking enabled?", this._linking_enabled ? "yes" : "no",
259           "Prepared?", this._is_prepared ? "yes" : "no"
260       );
261
262       debug.print_line (domain, level,
263           "%u Individuals:", this.individuals.size);
264       debug.indent ();
265
266       foreach (var individual in this.individuals.values)
267         {
268           string trust_level = null;
269
270           switch (individual.trust_level)
271             {
272               case TrustLevel.NONE:
273                 trust_level = "none";
274                 break;
275               case TrustLevel.PERSONAS:
276                 trust_level = "personas";
277                 break;
278               default:
279                 assert_not_reached ();
280             }
281
282           debug.print_heading (domain, level, "Individual (%p)", individual);
283           debug.print_key_value_pairs (domain, level,
284               "Ref. count", individual.ref_count.to_string (),
285               "ID", individual.id,
286               "User?", individual.is_user ? "yes" : "no",
287               "Trust level", trust_level
288           );
289           debug.print_line (domain, level, "%u Personas:",
290               individual.personas.size);
291
292           debug.indent ();
293
294           foreach (var persona in individual.personas)
295             {
296               debug.print_heading (domain, level, "Persona (%p)", persona);
297               debug.print_key_value_pairs (domain, level,
298                   "Ref. count", persona.ref_count.to_string (),
299                   "UID", persona.uid,
300                   "IID", persona.iid,
301                   "Display ID", persona.display_id,
302                   "User?", persona.is_user ? "yes" : "no"
303               );
304             }
305
306           debug.unindent ();
307         }
308
309       debug.unindent ();
310
311       debug.print_line (domain, level, "%u entries in the link map:",
312           this._link_map.size ());
313       debug.indent ();
314
315       var iter = HashTableIter<string, Individual> (this._link_map);
316       string link_key;
317       Individual individual;
318       while (iter.next (out link_key, out individual) == true)
319         {
320           debug.print_line (domain, level,
321               "%s â†’ %p", link_key, individual);
322         }
323
324       debug.unindent ();
325
326       debug.print_line (domain, level, "");
327     }
328
329   /**
330    * Prepare the IndividualAggregator for use.
331    *
332    * This loads all the available backends and prepares them for use by the
333    * IndividualAggregator. This should be called //after// connecting to the
334    * {@link IndividualAggregator.individuals_changed} signal, or a race
335    * condition could occur, with the signal being emitted before your code has
336    * connected to them, and {@link Individual}s getting "lost" as a result.
337    *
338    * This function is guaranteed to be idempotent (since version 0.3.0).
339    *
340    * @since 0.1.11
341    */
342   public async void prepare () throws GLib.Error
343     {
344       /* Once this async function returns, all the {@link Backend}s will have
345        * been prepared (though no {@link PersonaStore}s are guaranteed to be
346        * available yet). This last guarantee is new as of version 0.2.0. */
347
348       lock (this._is_prepared)
349         {
350           if (!this._is_prepared && !this._prepare_pending)
351             {
352               this._prepare_pending = true;
353               yield this._backend_store.load_backends ();
354               this._is_prepared = true;
355               this._prepare_pending = false;
356               this.notify_property ("is-prepared");
357             }
358         }
359     }
360
361   /**
362    * Get all matches for a given {@link Individual}.
363    *
364    * @param matchee the individual to find matches for
365    * @param min_threshold the threshold for accepting a match
366    * @return a map from matched individuals to the degree with which they match
367    * `matchee` (which is guaranteed to at least equal `min_threshold`);
368    * if no matches could be found, an empty map is returned
369    *
370    * @since 0.5.1
371    */
372   public Map<Individual, MatchResult> get_potential_matches
373       (Individual matchee, MatchResult min_threshold = MatchResult.VERY_HIGH)
374     {
375       HashMap<Individual, MatchResult> matches =
376           new HashMap<Individual, MatchResult> ();
377       Folks.PotentialMatch matchObj = new Folks.PotentialMatch ();
378
379       foreach (var i in this._individuals.values)
380         {
381           if (i.id == matchee.id)
382                 continue;
383
384           var result = matchObj.potential_match (i, matchee);
385           if (result >= min_threshold)
386             {
387               matches.set (i, result);
388             }
389         }
390
391       return matches;
392     }
393
394   /**
395    * Get all combinations between all {@link Individual}s.
396    *
397    * @param min_threshold the threshold for accepting a match
398    * @return a map from each individual in the aggregator to a map of the
399    * other individuals in the aggregator which can be matched with that
400    * individual, mapped to the degree with which they match the original
401    * individual (which is guaranteed to at least equal `min_threshold`)
402    *
403    * @since 0.5.1
404    */
405   public Map<Individual, Map<Individual, MatchResult>>
406       get_all_potential_matches
407         (MatchResult min_threshold = MatchResult.VERY_HIGH)
408     {
409       HashMap<Individual, HashMap<Individual, MatchResult>> matches =
410         new HashMap<Individual, HashMap<Individual, MatchResult>> ();
411       var individuals = this._individuals.values.to_array ();
412       Folks.PotentialMatch matchObj = new Folks.PotentialMatch ();
413
414       for (var i = 0; i < individuals.length; i++)
415         {
416           var a = individuals[i];
417           var matches_a = matches.get (a);
418           if (matches_a == null)
419             {
420               matches_a = new HashMap<Individual, MatchResult> ();
421               matches.set (a, matches_a);
422             }
423
424           for (var f = i + 1; f < individuals.length; f++)
425             {
426               var b = individuals[f];
427               var matches_b = matches.get (b);
428               if (matches_b == null)
429                 {
430                   matches_b = new HashMap<Individual, MatchResult> ();
431                   matches.set (b, matches_b);
432                 }
433
434               var result = matchObj.potential_match (a, b);
435
436               if (result >= min_threshold)
437                 {
438                   matches_a.set (b, result);
439                   matches_b.set (a, result);
440                 }
441             }
442         }
443
444       return matches;
445     }
446
447   private async void _add_backend (Backend backend)
448     {
449       if (!this._backends.contains (backend))
450         {
451           this._backends.add (backend);
452
453           backend.persona_store_added.connect (
454               this._backend_persona_store_added_cb);
455           backend.persona_store_removed.connect (
456               this._backend_persona_store_removed_cb);
457
458           /* handle the stores that have already been signaled */
459           foreach (var persona_store in backend.persona_stores.values)
460               {
461                 this._backend_persona_store_added_cb (backend, persona_store);
462               }
463         }
464     }
465
466   private void _backend_available_cb (BackendStore backend_store,
467       Backend backend)
468     {
469       this._add_backend.begin (backend);
470     }
471
472   private void _backend_persona_store_added_cb (Backend backend,
473       PersonaStore store)
474     {
475       var store_id = this._get_store_full_id (store.type_id, store.id);
476
477       /* We use the configured PersonaStore as the only trusted and writeable
478        * PersonaStore.
479        *
480        * If the type_id is `eds` we *must* know the actual store
481        * (address book) we are talking about or we might end up using
482        * a random store on every run.
483        */
484       if (store.type_id == this._configured_writeable_store_type_id)
485         {
486           if ((store.type_id != "eds" &&
487                   this._configured_writeable_store_id == "") ||
488               this._configured_writeable_store_id == store.id)
489             {
490               store.is_writeable = true;
491               store.trust_level = PersonaStoreTrust.FULL;
492               this._writeable_store = store;
493               this.notify_property ("primary-store");
494             }
495         }
496
497       this._stores.set (store_id, store);
498       store.personas_changed.connect (this._personas_changed_cb);
499       store.notify["is-writeable"].connect (this._is_writeable_changed_cb);
500       store.notify["trust-level"].connect (this._trust_level_changed_cb);
501
502       store.prepare.begin ((obj, result) =>
503         {
504           try
505             {
506               store.prepare.end (result);
507             }
508           catch (GLib.Error e)
509             {
510               /* Translators: the first parameter is a persona store identifier
511                * and the second is an error message. */
512               warning (_("Error preparing persona store '%s': %s"), store_id,
513                   e.message);
514             }
515         });
516     }
517
518   private void _backend_persona_store_removed_cb (Backend backend,
519       PersonaStore store)
520     {
521       store.personas_changed.disconnect (this._personas_changed_cb);
522       store.notify["trust-level"].disconnect (this._trust_level_changed_cb);
523       store.notify["is-writeable"].disconnect (this._is_writeable_changed_cb);
524
525       /* no need to remove this store's personas from all the individuals, since
526        * they'll do that themselves (and emit their own 'removed' signal if
527        * necessary) */
528
529       if (this._writeable_store == store)
530         {
531           this._writeable_store = null;
532           this.notify_property ("primary-store");
533         }
534       this._stores.unset (this._get_store_full_id (store.type_id, store.id));
535     }
536
537   private string _get_store_full_id (string type_id, string id)
538     {
539       return type_id + ":" + id;
540     }
541
542   /* Emit the individuals-changed signal ensuring that null parameters are
543    * turned into empty sets, and both sets passed to signal handlers are
544    * read-only. */
545   private void _emit_individuals_changed (Set<Individual>? added,
546       Set<Individual>? removed,
547       string? message = null,
548       Persona? actor = null,
549       GroupDetails.ChangeReason reason = GroupDetails.ChangeReason.NONE)
550     {
551       var _added = added;
552       var _removed = removed;
553
554       if ((added == null || added.size == 0) &&
555           (removed == null || removed.size == 0))
556         {
557           /* Don't bother emitting it if nothing's changed */
558           return;
559         }
560       else if (added == null)
561         {
562           _added = new HashSet<Individual> ();
563         }
564       else if (removed == null)
565         {
566           _removed = new HashSet<Individual> ();
567         }
568
569       this.individuals_changed (_added.read_only_view, _removed.read_only_view,
570           message, actor, reason);
571     }
572
573   private void _connect_to_individual (Individual individual)
574     {
575       individual.removed.connect (this._individual_removed_cb);
576       this._individuals.set (individual.id, individual);
577     }
578
579   private void _disconnect_from_individual (Individual individual)
580     {
581       this._individuals.unset (individual.id);
582       individual.removed.disconnect (this._individual_removed_cb);
583     }
584
585   private void _add_personas (Set<Persona> added,
586       ref HashSet<Individual> added_individuals,
587       ref HashMap<Individual, Individual> replaced_individuals,
588       ref Individual user)
589     {
590       /* Set of individuals which have been added as a result of the new
591        * personas. These will be returned in added_individuals, but have to be
592        * cached first so that we can ensure that we don't return any given
593        * individual in both added_individuals _and_ replaced_individuals. This
594        * can happen in the case that several of the added personas are linked
595        * together to form one final individual. In that case, a succession of
596        * newly linked individuals will be produced (one for each iteration of
597        * the loop over the added personas); only the *last one* of which should
598        * make its way into added_individuals. The rest should not even make
599        * their way into replaced_individuals, as they've existed only within the
600        * confines of this function call. */
601       HashSet<Individual> almost_added_individuals = new HashSet<Individual> ();
602
603       foreach (var persona in added)
604         {
605           PersonaStoreTrust trust_level = persona.store.trust_level;
606
607           /* These are the Individuals whose Personas will be linked together
608            * to form the `final_individual`.
609            * Since a given Persona can only be part of one Individual, and the
610            * code in Persona._set_personas() ensures that there are no duplicate
611            * Personas in a given Individual, ensuring that there are no
612            * duplicate Individuals in `candidate_inds` (by using a
613            * HashSet) guarantees that there will be no duplicate Personas
614            * in the `final_individual`. */
615           HashSet<Individual> candidate_inds = new HashSet<Individual> ();
616
617           var final_personas = new HashSet<Persona> ();
618           Individual final_individual = null;
619
620           debug ("Aggregating persona '%s' on '%s'.", persona.uid, persona.iid);
621
622           /* If the Persona is the user, we *always* want to link it to the
623            * existing this.user. */
624           if (persona.is_user == true && user != null)
625             {
626               debug ("    Found candidate individual '%s' as user.", user.id);
627               candidate_inds.add (user);
628             }
629
630           /* If we don't trust the PersonaStore at all, we can't link the
631            * Persona to any existing Individual */
632           if (trust_level != PersonaStoreTrust.NONE)
633             {
634               var candidate_ind = this._link_map.lookup (persona.iid);
635               if (candidate_ind != null &&
636                   candidate_ind.trust_level != TrustLevel.NONE &&
637                   !candidate_inds.contains (candidate_ind))
638                 {
639                   debug ("    Found candidate individual '%s' by IID '%s'.",
640                       candidate_ind.id, persona.iid);
641                   candidate_inds.add (candidate_ind);
642                 }
643             }
644
645           if (persona.store.trust_level == PersonaStoreTrust.FULL)
646             {
647               /* If we trust the PersonaStore the Persona came from, we can
648                * attempt to link based on its linkable properties. */
649               foreach (unowned string foo in persona.linkable_properties)
650                 {
651                   /* FIXME: If we just use string prop_name directly in the
652                    * foreach, Vala doesn't copy it into the closure data, and
653                    * prop_name ends up as NULL. bgo#628336 */
654                   unowned string prop_name = foo;
655
656                   /* FIXME: can't be var because of bgo#638208 */
657                   unowned ObjectClass pclass = persona.get_class ();
658                   if (pclass.find_property (prop_name) == null)
659                     {
660                       warning (
661                           /* Translators: the parameter is a property name. */
662                           _("Unknown property '%s' in linkable property list."),
663                           prop_name);
664                       continue;
665                     }
666
667                   persona.linkable_property_to_links (prop_name, (l) =>
668                     {
669                       unowned string prop_linking_value = l;
670                       var candidate_ind =
671                           this._link_map.lookup (prop_linking_value);
672
673                       if (candidate_ind != null &&
674                           candidate_ind.trust_level != TrustLevel.NONE &&
675                           !candidate_inds.contains (candidate_ind))
676                         {
677                           debug ("    Found candidate individual '%s' by " +
678                               "linkable property '%s' = '%s'.",
679                               candidate_ind.id, prop_name, prop_linking_value);
680                           candidate_inds.add (candidate_ind);
681                         }
682                     });
683                 }
684             }
685
686           /* Ensure the original persona makes it into the final individual */
687           final_personas.add (persona);
688
689           if (candidate_inds.size > 0 && this._linking_enabled == true)
690             {
691               /* The Persona's IID or linkable properties match one or more
692                * linkable fields which are already in the link map, so we link
693                * together all the Individuals we found to form a new
694                * final_individual. Later, we remove the Personas from the old
695                * Individuals so that the Individuals themselves are removed. */
696               foreach (var individual in candidate_inds)
697                 {
698                   final_personas.add_all (individual.personas);
699                 }
700             }
701           else if (candidate_inds.size > 0)
702             {
703               debug ("    Linking disabled.");
704             }
705           else
706             {
707               debug ("    Did not find any candidate individuals.");
708             }
709
710           /* Create the final linked Individual */
711           final_individual = new Individual (final_personas);
712           debug ("    Created new individual '%s' with personas:",
713               final_individual.id);
714           foreach (var p in final_personas)
715             {
716               var final_persona = (Persona) p;
717
718               debug ("        %s (is user: %s, IID: %s)", final_persona.uid,
719                   final_persona.is_user ? "yes" : "no", final_persona.iid);
720
721               /* Add the Persona to the link map. Its trust level will be
722                * reflected in final_individual.trust_level, so other Personas
723                * won't be linked against it in error if the trust level is
724                * NONE. */
725               this._link_map.replace (final_persona.iid, final_individual);
726
727               /* Only allow linking on non-IID properties of the Persona if we
728                * fully trust the PersonaStore it came from. */
729               if (final_persona.store.trust_level == PersonaStoreTrust.FULL)
730                 {
731                   debug ("        Inserting links:");
732
733                   /* Insert maps from the Persona's linkable properties to the
734                    * Individual. */
735                   foreach (unowned string prop_name in
736                       final_persona.linkable_properties)
737                     {
738                       /* FIXME: can't be var because of bgo#638208 */
739                       unowned ObjectClass pclass = final_persona.get_class ();
740                       if (pclass.find_property (prop_name) == null)
741                         {
742                           warning (
743                               /* Translators: the parameter is a property
744                                * name. */
745                               _("Unknown property '%s' in linkable property list."),
746                               prop_name);
747                           continue;
748                         }
749
750                       final_persona.linkable_property_to_links (prop_name,
751                           (l) =>
752                         {
753                           unowned string prop_linking_value = l;
754
755                           debug ("            %s", prop_linking_value);
756                           this._link_map.replace (prop_linking_value,
757                               final_individual);
758                         });
759                     }
760                 }
761             }
762
763           /* Remove the old Individuals. This has to be done here, as we need
764            * the final_individual. */
765           foreach (var i in candidate_inds)
766             {
767               /* If the replaced individual was marked to be added to the
768                * aggregator, unmark it. */
769               if (almost_added_individuals.contains (i) == true)
770                 almost_added_individuals.remove (i);
771               else
772                 replaced_individuals.set (i, final_individual);
773             }
774
775           /* If the final Individual is the user, set them as such. */
776           if (final_individual.is_user == true)
777             user = final_individual;
778
779           /* Mark the final individual for addition later */
780           almost_added_individuals.add (final_individual);
781         }
782
783       /* Add the set of final individuals which weren't later replaced to the
784        * aggregator. */
785       foreach (var i in almost_added_individuals)
786         {
787           /* Add the new Individual to the aggregator */
788           added_individuals.add (i);
789           this._connect_to_individual (i);
790         }
791     }
792
793   private void _remove_persona_from_link_map (Persona persona)
794     {
795       this._link_map.remove (persona.iid);
796
797       if (persona.store.trust_level == PersonaStoreTrust.FULL)
798         {
799           debug ("    Removing links to %s:", persona.uid);
800
801           /* Remove maps from the Persona's linkable properties to
802            * Individuals. Add the Individuals to a list of Individuals to be
803            * removed. */
804           foreach (unowned string prop_name in persona.linkable_properties)
805             {
806               /* FIXME: can't be var because of bgo#638208 */
807               unowned ObjectClass pclass = persona.get_class ();
808               if (pclass.find_property (prop_name) == null)
809                 {
810                   warning (
811                       /* Translators: the parameter is a property name. */
812                       _("Unknown property '%s' in linkable property list."),
813                       prop_name);
814                   continue;
815                 }
816
817               persona.linkable_property_to_links (prop_name, (linking_value) =>
818                 {
819                   debug ("        %s", linking_value);
820                   this._link_map.remove (linking_value);
821                 });
822             }
823         }
824     }
825
826   private void _personas_changed_cb (PersonaStore store,
827       Set<Persona> added,
828       Set<Persona> removed,
829       string? message,
830       Persona? actor,
831       GroupDetails.ChangeReason reason)
832     {
833       var added_individuals = new HashSet<Individual> ();
834       var removed_individuals = new HashSet<Individual> ();
835       var replaced_individuals = new HashMap<Individual, Individual> ();
836       var relinked_personas = new HashSet<Persona> ();
837       var removed_personas = new HashSet<Persona> (direct_hash, direct_equal);
838
839       /* We store the value of this.user locally and only update it at the end
840        * of the function to prevent spamming notifications of changes to the
841        * property. */
842       var user = this.user;
843
844       debug ("Removing Personas:");
845
846       foreach (var persona in removed)
847         {
848           debug ("    %s (is user: %s, IID: %s)", persona.uid,
849               persona.is_user ? "yes" : "no", persona.iid);
850
851           /* Build a hash table of the removed Personas so that we can quickly
852            * eliminate them from the list of Personas to relink, below. */
853           removed_personas.add (persona);
854
855           /* Find the Individual containing the Persona (if any) and mark them
856            * for removal (any other Personas they have which aren't being
857            * removed will be re-linked into other Individuals). */
858           var ind = this._link_map.lookup (persona.iid);
859           if (ind != null)
860             removed_individuals.add (ind);
861
862           /* Remove the Persona's links from the link map */
863           this._remove_persona_from_link_map (persona);
864         }
865
866       /* Remove the Individuals which were pointed to by the linkable properties
867        * of the removed Personas. We can then re-link the other Personas in
868        * those Individuals, since their links may have changed.
869        * Note that we remove the Individual from this.individuals, meaning that
870        * _individual_removed_cb() ignores this Individual. This allows us to
871        * group together the IndividualAggregator.individuals_changed signals
872        * for all the removed Individuals. */
873       debug ("Removing Individuals due to removed links:");
874       foreach (var individual in removed_individuals)
875         {
876           /* Ensure we don't remove the same Individual twice */
877           if (this._individuals.has_key (individual.id) == false)
878             continue;
879
880           debug ("    %s", individual.id);
881
882           /* Build a list of Personas which need relinking. Ensure we don't
883            * include any of the Personas which have just been removed. */
884           foreach (var persona in individual.personas)
885             {
886               if (removed_personas.contains (persona) == true ||
887                   relinked_personas.contains (persona) == true)
888                 continue;
889
890               relinked_personas.add (persona);
891
892               /* Remove links to the Persona */
893               this._remove_persona_from_link_map (persona);
894             }
895
896           if (user == individual)
897             user = null;
898
899           this._disconnect_from_individual (individual);
900           individual.personas = null;
901         }
902
903       debug ("Adding Personas:");
904       foreach (var persona in added)
905         {
906           debug ("    %s (is user: %s, IID: %s)", persona.uid,
907               persona.is_user ? "yes" : "no", persona.iid);
908         }
909
910       if (added.size > 0)
911         {
912           this._add_personas (added, ref added_individuals,
913               ref replaced_individuals, ref user);
914         }
915
916       debug ("Relinking Personas:");
917       foreach (var persona in relinked_personas)
918         {
919           debug ("    %s (is user: %s, IID: %s)", persona.uid,
920               persona.is_user ? "yes" : "no", persona.iid);
921         }
922
923       this._add_personas (relinked_personas, ref added_individuals,
924           ref replaced_individuals, ref user);
925
926       /* Signal the removal of the replaced_individuals at the same time as the
927        * removed_individuals. (The only difference between replaced individuals
928        * and removed ones is that replaced individuals specify a replacement
929        * when they emit their Individual:removed signal. */
930       if (replaced_individuals != null)
931         {
932           MapIterator<Individual, Individual> iter =
933               replaced_individuals.map_iterator ();
934           while (iter.next () == true)
935             removed_individuals.add (iter.get_key ());
936         }
937
938       /* Notify of changes to this.user */
939       this.user = user;
940
941       /* Signal the addition of new individuals and removal of old ones to the
942        * aggregator */
943       if (added_individuals.size > 0 || removed_individuals.size > 0)
944         {
945           this._emit_individuals_changed (added_individuals,
946               removed_individuals);
947         }
948
949       /* Signal the replacement of various Individuals as a consequence of
950        * linking. */
951       debug ("Replacing Individuals due to linking:");
952       var iter = replaced_individuals.map_iterator ();
953       while (iter.next () == true)
954         {
955           iter.get_key ().replace (iter.get_value ());
956         }
957     }
958
959   private void _is_writeable_changed_cb (Object object, ParamSpec pspec)
960     {
961       /* Ensure that we only have one writeable PersonaStore */
962       var store = (PersonaStore) object;
963       assert ((store.is_writeable == true && store == this._writeable_store) ||
964           (store.is_writeable == false && store != this._writeable_store));
965     }
966
967   private void _trust_level_changed_cb (Object object, ParamSpec pspec)
968     {
969       /* Only our writeable_store can be fully trusted. */
970       var store = (PersonaStore) object;
971       if (this._writeable_store != null &&
972           store.type_id == this._writeable_store.type_id)
973         assert (store.trust_level == PersonaStoreTrust.FULL);
974       else
975         assert (store.trust_level != PersonaStoreTrust.FULL);
976     }
977
978   private void _individual_removed_cb (Individual i, Individual? replacement)
979     {
980       if (this.user == i)
981         this.user = null;
982
983       /* Only signal if the individual is still in this.individuals. This allows
984        * us to group removals together in, e.g., _personas_changed_cb(). */
985       if (this._individuals.get (i.id) != i)
986         return;
987
988       var individuals = new HashSet<Individual> ();
989       individuals.add (i);
990
991       if (replacement != null)
992         {
993           debug ("Individual '%s' removed (replaced by '%s')", i.id,
994               replacement.id);
995         }
996       else
997         {
998           debug ("Individual '%s' removed (not replaced)", i.id);
999         }
1000
1001       /* If the individual has 0 personas, we've already signaled removal */
1002       if (i.personas.size > 0)
1003         {
1004           this._emit_individuals_changed (null, individuals);
1005         }
1006
1007       this._disconnect_from_individual (i);
1008     }
1009
1010   /**
1011    * Add a new persona in the given {@link PersonaStore} based on the `details`
1012    * provided.
1013    *
1014    * If the target store is offline, this function will throw
1015    * {@link IndividualAggregatorError.STORE_OFFLINE}. It's the responsibility of
1016    * the caller to cache details and re-try this function if it wishes to make
1017    * offline adds work.
1018    *
1019    * The details hash is a backend-specific mapping of key, value strings.
1020    * Common keys include:
1021    *
1022    *  * contact - service-specific contact ID
1023    *  * message - a user-readable message to pass to the persona being added
1024    *
1025    * If a {@link Persona} with the given details already exists in the store, no
1026    * error will be thrown and this function will return `null`.
1027    *
1028    * @param parent an optional {@link Individual} to add the new {@link Persona}
1029    * to. This persona will be appended to its ordered list of personas.
1030    * @param persona_store the {@link PersonaStore} to add the persona to
1031    * @param details a key-value map of details to use in creating the new
1032    * {@link Persona}
1033    * @return the new {@link Persona} or `null` if the corresponding
1034    * {@link Persona} already existed. If non-`null`, the new {@link Persona}
1035    * will also be added to a new or existing {@link Individual} as necessary.
1036    *
1037    * @since 0.3.5
1038    */
1039   public async Persona? add_persona_from_details (Individual? parent,
1040       PersonaStore persona_store,
1041       HashTable<string, Value?> details) throws IndividualAggregatorError
1042     {
1043       Persona persona = null;
1044       try
1045         {
1046           var details_copy = this._asv_copy (details);
1047           persona = yield persona_store.add_persona_from_details (details_copy);
1048         }
1049       catch (PersonaStoreError e)
1050         {
1051           if (e is PersonaStoreError.STORE_OFFLINE)
1052             {
1053               throw new IndividualAggregatorError.STORE_OFFLINE (e.message);
1054             }
1055           else
1056             {
1057               var full_id = this._get_store_full_id (persona_store.type_id,
1058                   persona_store.id);
1059
1060               throw new IndividualAggregatorError.ADD_FAILED (
1061                   /* Translators: the first parameter is a store identifier
1062                    * and the second parameter is an error message. */
1063                   _("Failed to add contact for persona store ID '%s': %s"),
1064                   full_id, e.message);
1065             }
1066         }
1067
1068       if (parent != null && persona != null)
1069         {
1070           parent.personas.add (persona);
1071         }
1072
1073       return persona;
1074     }
1075
1076   private HashTable<string, Value?> _asv_copy (HashTable<string, Value?> asv)
1077     {
1078       var retval = new HashTable<string, Value?> (str_hash, str_equal);
1079
1080       asv.foreach ((k, v) =>
1081         {
1082           retval.insert ((string) k, v);
1083         });
1084
1085       return retval;
1086     }
1087
1088   /**
1089    * Completely remove the individual and all of its personas from their
1090    * backing stores.
1091    *
1092    * @param individual the {@link Individual} to remove
1093    * @since 0.1.11
1094    */
1095   public async void remove_individual (Individual individual) throws GLib.Error
1096     {
1097       /* Removing personas changes the persona set so we need to make a copy
1098        * first */
1099       var personas = new HashSet<Persona> ();
1100       foreach (var p in individual.personas)
1101         {
1102           personas.add (p);
1103         }
1104
1105       foreach (var persona in personas)
1106         {
1107           yield persona.store.remove_persona (persona);
1108         }
1109     }
1110
1111   /**
1112    * Completely remove the persona from its backing store.
1113    *
1114    * This will leave other personas in the same individual alone.
1115    *
1116    * @param persona the {@link Persona} to remove
1117    * @since 0.1.11
1118    */
1119   public async void remove_persona (Persona persona) throws GLib.Error
1120     {
1121       yield persona.store.remove_persona (persona);
1122     }
1123
1124   /**
1125    * Link the given {@link Persona}s together.
1126    *
1127    * Create links between the given {@link Persona}s so that they form a single
1128    * {@link Individual}. The new {@link Individual} will be returned via the
1129    * {@link IndividualAggregator.individuals_changed} signal.
1130    *
1131    * Removal of the {@link Individual}s which the {@link Persona}s were in
1132    * before is signalled by {@link IndividualAggregator.individuals_changed} and
1133    * {@link Individual.removed}.
1134    *
1135    * @param personas the {@link Persona}s to be linked
1136    * @since 0.5.1
1137    */
1138   public async void link_personas (Set<Persona> personas)
1139       throws IndividualAggregatorError
1140     {
1141       if (this._writeable_store == null)
1142         {
1143           throw new IndividualAggregatorError.NO_WRITEABLE_STORE (
1144               _("Can't link personas with no writeable store."));
1145         }
1146
1147       /* Don't bother linking if it's just one Persona */
1148       if (personas.size <= 1)
1149         return;
1150
1151       /* Disallow linking if it's disabled */
1152       if (this._linking_enabled == false)
1153         {
1154           debug ("Can't link Personas: linking disabled.");
1155           return;
1156         }
1157
1158       /* Create a new persona in the writeable store which links together the
1159        * given personas */
1160       assert (this._writeable_store.type_id ==
1161           this._configured_writeable_store_type_id);
1162
1163       /* `protocols_addrs_set` will be passed to the new Kf.Persona */
1164       var protocols_addrs_set = new HashMultiMap<string, ImFieldDetails> (
1165             null, null,
1166             (GLib.HashFunc) ImFieldDetails.hash,
1167             (GLib.EqualFunc) ImFieldDetails.equal);
1168       var web_service_addrs_set =
1169         new HashMultiMap<string, WebServiceFieldDetails> (
1170             null, null,
1171             (GLib.HashFunc) WebServiceFieldDetails.hash,
1172             (GLib.EqualFunc) WebServiceFieldDetails.equal);
1173
1174       /* List of local_ids */
1175       var local_ids = new Gee.HashSet<string> ();
1176
1177       foreach (var persona in personas)
1178         {
1179           if (persona is ImDetails)
1180             {
1181               ImDetails im_details = (ImDetails) persona;
1182
1183               /* protocols_addrs_set = union (all personas' IM addresses) */
1184               foreach (var protocol in im_details.im_addresses.get_keys ())
1185                 {
1186                   var im_addresses = im_details.im_addresses.get (protocol);
1187
1188                   foreach (var im_address in im_addresses)
1189                     {
1190                       protocols_addrs_set.set (protocol, im_address);
1191                     }
1192                 }
1193             }
1194
1195           if (persona is WebServiceDetails)
1196             {
1197               WebServiceDetails ws_details = (WebServiceDetails) persona;
1198
1199               /* web_service_addrs_set = union (all personas' WS addresses) */
1200               foreach (var web_service in
1201                   ws_details.web_service_addresses.get_keys ())
1202                 {
1203                   var ws_addresses =
1204                       ws_details.web_service_addresses.get (web_service);
1205
1206                   foreach (var ws_fd in ws_addresses)
1207                     web_service_addrs_set.set (web_service, ws_fd);
1208                 }
1209             }
1210
1211           if (persona is LocalIdDetails)
1212             {
1213               foreach (var id in ((LocalIdDetails) persona).local_ids)
1214                 {
1215                   local_ids.add (id);
1216                 }
1217             }
1218         }
1219
1220       var details = new HashTable<string, Value?> (str_hash, str_equal);
1221
1222       if (protocols_addrs_set.size > 0)
1223         {
1224           var im_addresses_value = Value (typeof (MultiMap));
1225           im_addresses_value.set_object (protocols_addrs_set);
1226           details.insert (PersonaStore.detail_key (PersonaDetail.IM_ADDRESSES),
1227               im_addresses_value);
1228         }
1229
1230       if (web_service_addrs_set.size > 0)
1231         {
1232           var web_service_addresses_value = Value (typeof (MultiMap));
1233           web_service_addresses_value.set_object (web_service_addrs_set);
1234           details.insert (PersonaStore.detail_key
1235               (PersonaDetail.WEB_SERVICE_ADDRESSES),
1236               web_service_addresses_value);
1237         }
1238
1239       if (local_ids.size > 0)
1240         {
1241           var local_ids_value = Value (typeof (Set<string>));
1242           local_ids_value.set_object (local_ids);
1243           details.insert (
1244               Folks.PersonaStore.detail_key (PersonaDetail.LOCAL_IDS),
1245               local_ids_value);
1246         }
1247
1248       yield this.add_persona_from_details (null,
1249           this._writeable_store, details);
1250     }
1251
1252   /**
1253    * Unlinks the given {@link Individual} into its constituent {@link Persona}s.
1254    *
1255    * This completely unlinks the given {@link Individual}, destroying all of
1256    * its writeable {@link Persona}s.
1257    *
1258    * The {@link Individual}'s removal is signalled by
1259    * {@link IndividualAggregator.individuals_changed} and
1260    * {@link Individual.removed}.
1261    *
1262    * The {@link Persona}s comprising the {@link Individual} will be re-linked
1263    * into one or more new {@link Individual}s, depending on how much linking
1264    * data remains (typically only implicit links remain). The addition of these
1265    * new {@link Individual}s will be signalled by
1266    * {@link IndividualAggregator.individuals_changed}.
1267    *
1268    * @param individual the {@link Individual} to unlink
1269    * @since 0.1.13
1270    */
1271   public async void unlink_individual (Individual individual) throws GLib.Error
1272     {
1273       if (this._linking_enabled == false)
1274         {
1275           debug ("Can't unlink Individual '%s': linking disabled.",
1276               individual.id);
1277           return;
1278         }
1279
1280       debug ("Unlinking Individual '%s', deleting Personas:", individual.id);
1281
1282       /* Remove all the Personas from writeable PersonaStores.
1283        *
1284        * We have to take a copy of the Persona list before removing the
1285        * Personas, as _personas_changed_cb() (which is called as a result of
1286        * calling _writeable_store.remove_persona()) messes around with Persona
1287        * lists. */
1288       var personas = new HashSet<Persona> ();
1289       foreach (var p in individual.personas)
1290         {
1291           personas.add (p);
1292         }
1293
1294       foreach (var persona in personas)
1295         {
1296           if (persona.store == this._writeable_store)
1297             {
1298               debug ("    %s (is user: %s, IID: %s)", persona.uid,
1299                   persona.is_user ? "yes" : "no", persona.iid);
1300               yield this._writeable_store.remove_persona (persona);
1301             }
1302         }
1303     }
1304 }