2 * Copyright (C) 2010 Collabora Ltd.
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.
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.
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/>.
18 * Travis Reitter <travis.reitter@collabora.co.uk>
25 * Errors from {@link IndividualAggregator}s.
27 public errordomain Folks.IndividualAggregatorError
30 * Adding a {@link Persona} to a {@link PersonaStore} failed.
35 * An operation which required the use of a writeable store failed because no
36 * writeable store was available.
43 * The {@link PersonaStore} was offline (ie, this is a temporary failure).
51 * Stores {@link Individual}s which have been created through
52 * aggregation of all the {@link Persona}s provided by the various
55 * This is the main interface for client applications.
57 public class Folks.IndividualAggregator : Object
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;
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";
74 * Whether {@link IndividualAggregator.prepare} has successfully completed for
79 public bool is_prepared
81 get { return this._is_prepared; }
85 * Our configured primary (writeable) store.
87 * Which one to use is decided (in order or precedence)
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
96 public PersonaStore? primary_store
98 get { return this._writeable_store; }
101 private Map<string, Individual> _individuals;
102 private Map<string, Individual> _individuals_ro;
105 * A map from {@link Individual.id}s to their {@link Individual}s.
107 * This is the canonical set of {@link Individual}s provided by this
108 * IndividualAggregator.
110 * {@link Individual}s may be added or removed using
111 * {@link IndividualAggregator.add_persona_from_details} and
112 * {@link IndividualAggregator.remove_individual}, respectively.
116 public Map<string, Individual> individuals
118 get { return this._individuals_ro; }
121 this._individuals = value;
122 this._individuals_ro = this._individuals.read_only_view;
127 * The {@link Individual} representing the user.
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.
135 public Individual user { get; private set; }
138 * Emitted when one or more {@link Individual}s are added to or removed from
141 * This will not be emitted until after {@link IndividualAggregator.prepare}
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
152 public signal void individuals_changed (Set<Individual> added,
153 Set<Individual> removed,
156 GroupDetails.ChangeReason reason);
158 /* FIXME: make this a singleton? */
160 * Create a new IndividualAggregator.
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.
167 * An example of how to set up an IndividualAggregator:
169 * IndividualAggregator agg = new IndividualAggregator ();
170 * agg.individuals_changed.connect (individuals_changed_cb);
174 public IndividualAggregator ()
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);
181 this._backends = new HashSet<Backend> ();
182 this._debug = Debug.dup ();
183 this._debug.print_status.connect (this._debug_print_status);
185 /* Check out the configured writeable store */
186 var store_config_ids = Environment.get_variable ("FOLKS_WRITEABLE_STORE");
187 if (store_config_ids != null)
189 this._set_writeable_store (store_config_ids);
194 this._configured_writeable_store_type_id = "eds";
195 this._configured_writeable_store_id = "system";
197 this._configured_writeable_store_type_id = "key-file";
198 this._configured_writeable_store_id = "";
203 unowned GConf.Client client = GConf.Client.get_default ();
204 GConf.Value? val = client.get (this._FOLKS_CONFIG_KEY);
206 this._set_writeable_store (val.get_string ());
210 /* We ignore errors and go with the default store */
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");
220 this._backend_store = BackendStore.dup ();
221 this._backend_store.backend_available.connect (
222 this._backend_available_cb);
225 ~IndividualAggregator ()
227 this._backend_store.backend_available.disconnect (
228 this._backend_available_cb);
229 this._backend_store = null;
231 this._debug.print_status.disconnect (this._debug_print_status);
234 private void _set_writeable_store (string store_config_ids)
236 if (store_config_ids.index_of (":") != -1)
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];
244 this._configured_writeable_store_type_id = store_config_ids;
245 this._configured_writeable_store_id = "";
249 private void _debug_print_status (Debug debug)
251 const string domain = Debug.STATUS_LOG_DOMAIN;
252 const LogLevelFlags level = LogLevelFlags.LEVEL_INFO;
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"
262 debug.print_line (domain, level,
263 "%u Individuals:", this.individuals.size);
266 foreach (var individual in this.individuals.values)
268 string trust_level = null;
270 switch (individual.trust_level)
272 case TrustLevel.NONE:
273 trust_level = "none";
275 case TrustLevel.PERSONAS:
276 trust_level = "personas";
279 assert_not_reached ();
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 (),
286 "User?", individual.is_user ? "yes" : "no",
287 "Trust level", trust_level
289 debug.print_line (domain, level, "%u Personas:",
290 individual.personas.size);
294 foreach (var persona in individual.personas)
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 (),
301 "Display ID", persona.display_id,
302 "User?", persona.is_user ? "yes" : "no"
311 debug.print_line (domain, level, "%u entries in the link map:",
312 this._link_map.size ());
315 var iter = HashTableIter<string, Individual> (this._link_map);
317 Individual individual;
318 while (iter.next (out link_key, out individual) == true)
320 debug.print_line (domain, level,
321 "%s → %p", link_key, individual);
326 debug.print_line (domain, level, "");
330 * Prepare the IndividualAggregator for use.
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.
338 * This function is guaranteed to be idempotent (since version 0.3.0).
342 public async void prepare () throws GLib.Error
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. */
348 lock (this._is_prepared)
350 if (!this._is_prepared && !this._prepare_pending)
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");
362 * Get all matches for a given {@link Individual}.
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
372 public Map<Individual, MatchResult> get_potential_matches
373 (Individual matchee, MatchResult min_threshold = MatchResult.VERY_HIGH)
375 HashMap<Individual, MatchResult> matches =
376 new HashMap<Individual, MatchResult> ();
377 Folks.PotentialMatch matchObj = new Folks.PotentialMatch ();
379 foreach (var i in this._individuals.values)
381 if (i.id == matchee.id)
384 var result = matchObj.potential_match (i, matchee);
385 if (result >= min_threshold)
387 matches.set (i, result);
395 * Get all combinations between all {@link Individual}s.
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`)
405 public Map<Individual, Map<Individual, MatchResult>>
406 get_all_potential_matches
407 (MatchResult min_threshold = MatchResult.VERY_HIGH)
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 ();
414 for (var i = 0; i < individuals.length; i++)
416 var a = individuals[i];
417 var matches_a = matches.get (a);
418 if (matches_a == null)
420 matches_a = new HashMap<Individual, MatchResult> ();
421 matches.set (a, matches_a);
424 for (var f = i + 1; f < individuals.length; f++)
426 var b = individuals[f];
427 var matches_b = matches.get (b);
428 if (matches_b == null)
430 matches_b = new HashMap<Individual, MatchResult> ();
431 matches.set (b, matches_b);
434 var result = matchObj.potential_match (a, b);
436 if (result >= min_threshold)
438 matches_a.set (b, result);
439 matches_b.set (a, result);
447 private async void _add_backend (Backend backend)
449 if (!this._backends.contains (backend))
451 this._backends.add (backend);
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);
458 /* handle the stores that have already been signaled */
459 foreach (var persona_store in backend.persona_stores.values)
461 this._backend_persona_store_added_cb (backend, persona_store);
466 private void _backend_available_cb (BackendStore backend_store,
469 this._add_backend.begin (backend);
472 private void _backend_persona_store_added_cb (Backend backend,
475 var store_id = this._get_store_full_id (store.type_id, store.id);
477 /* We use the configured PersonaStore as the only trusted and writeable
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.
484 if (store.type_id == this._configured_writeable_store_type_id)
486 if ((store.type_id != "eds" &&
487 this._configured_writeable_store_id == "") ||
488 this._configured_writeable_store_id == store.id)
490 store.is_writeable = true;
491 store.trust_level = PersonaStoreTrust.FULL;
492 this._writeable_store = store;
493 this.notify_property ("primary-store");
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);
502 store.prepare.begin ((obj, result) =>
506 store.prepare.end (result);
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,
518 private void _backend_persona_store_removed_cb (Backend backend,
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);
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
529 if (this._writeable_store == store)
531 this._writeable_store = null;
532 this.notify_property ("primary-store");
534 this._stores.unset (this._get_store_full_id (store.type_id, store.id));
537 private string _get_store_full_id (string type_id, string id)
539 return type_id + ":" + id;
542 /* Emit the individuals-changed signal ensuring that null parameters are
543 * turned into empty sets, and both sets passed to signal handlers are
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)
552 var _removed = removed;
554 if ((added == null || added.size == 0) &&
555 (removed == null || removed.size == 0))
557 /* Don't bother emitting it if nothing's changed */
560 else if (added == null)
562 _added = new HashSet<Individual> ();
564 else if (removed == null)
566 _removed = new HashSet<Individual> ();
569 this.individuals_changed (_added.read_only_view, _removed.read_only_view,
570 message, actor, reason);
573 private void _connect_to_individual (Individual individual)
575 individual.removed.connect (this._individual_removed_cb);
576 this._individuals.set (individual.id, individual);
579 private void _disconnect_from_individual (Individual individual)
581 this._individuals.unset (individual.id);
582 individual.removed.disconnect (this._individual_removed_cb);
585 private void _add_personas (Set<Persona> added,
586 ref HashSet<Individual> added_individuals,
587 ref HashMap<Individual, Individual> replaced_individuals,
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> ();
603 foreach (var persona in added)
605 PersonaStoreTrust trust_level = persona.store.trust_level;
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> ();
617 var final_personas = new HashSet<Persona> ();
618 Individual final_individual = null;
620 debug ("Aggregating persona '%s' on '%s'.", persona.uid, persona.iid);
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)
626 debug (" Found candidate individual '%s' as user.", user.id);
627 candidate_inds.add (user);
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)
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))
639 debug (" Found candidate individual '%s' by IID '%s'.",
640 candidate_ind.id, persona.iid);
641 candidate_inds.add (candidate_ind);
645 if (persona.store.trust_level == PersonaStoreTrust.FULL)
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)
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;
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)
661 /* Translators: the parameter is a property name. */
662 _("Unknown property '%s' in linkable property list."),
667 persona.linkable_property_to_links (prop_name, (l) =>
669 unowned string prop_linking_value = l;
671 this._link_map.lookup (prop_linking_value);
673 if (candidate_ind != null &&
674 candidate_ind.trust_level != TrustLevel.NONE &&
675 !candidate_inds.contains (candidate_ind))
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);
686 /* Ensure the original persona makes it into the final individual */
687 final_personas.add (persona);
689 if (candidate_inds.size > 0 && this._linking_enabled == true)
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)
698 final_personas.add_all (individual.personas);
701 else if (candidate_inds.size > 0)
703 debug (" Linking disabled.");
707 debug (" Did not find any candidate individuals.");
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)
716 var final_persona = (Persona) p;
718 debug (" %s (is user: %s, IID: %s)", final_persona.uid,
719 final_persona.is_user ? "yes" : "no", final_persona.iid);
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
725 this._link_map.replace (final_persona.iid, final_individual);
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)
731 debug (" Inserting links:");
733 /* Insert maps from the Persona's linkable properties to the
735 foreach (unowned string prop_name in
736 final_persona.linkable_properties)
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)
743 /* Translators: the parameter is a property
745 _("Unknown property '%s' in linkable property list."),
750 final_persona.linkable_property_to_links (prop_name,
753 unowned string prop_linking_value = l;
755 debug (" %s", prop_linking_value);
756 this._link_map.replace (prop_linking_value,
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)
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);
772 replaced_individuals.set (i, final_individual);
775 /* If the final Individual is the user, set them as such. */
776 if (final_individual.is_user == true)
777 user = final_individual;
779 /* Mark the final individual for addition later */
780 almost_added_individuals.add (final_individual);
783 /* Add the set of final individuals which weren't later replaced to the
785 foreach (var i in almost_added_individuals)
787 /* Add the new Individual to the aggregator */
788 added_individuals.add (i);
789 this._connect_to_individual (i);
793 private void _remove_persona_from_link_map (Persona persona)
795 this._link_map.remove (persona.iid);
797 if (persona.store.trust_level == PersonaStoreTrust.FULL)
799 debug (" Removing links to %s:", persona.uid);
801 /* Remove maps from the Persona's linkable properties to
802 * Individuals. Add the Individuals to a list of Individuals to be
804 foreach (unowned string prop_name in persona.linkable_properties)
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)
811 /* Translators: the parameter is a property name. */
812 _("Unknown property '%s' in linkable property list."),
817 persona.linkable_property_to_links (prop_name, (linking_value) =>
819 debug (" %s", linking_value);
820 this._link_map.remove (linking_value);
826 private void _personas_changed_cb (PersonaStore store,
828 Set<Persona> removed,
831 GroupDetails.ChangeReason reason)
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);
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
842 var user = this.user;
844 debug ("Removing Personas:");
846 foreach (var persona in removed)
848 debug (" %s (is user: %s, IID: %s)", persona.uid,
849 persona.is_user ? "yes" : "no", persona.iid);
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);
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);
860 removed_individuals.add (ind);
862 /* Remove the Persona's links from the link map */
863 this._remove_persona_from_link_map (persona);
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)
876 /* Ensure we don't remove the same Individual twice */
877 if (this._individuals.has_key (individual.id) == false)
880 debug (" %s", individual.id);
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)
886 if (removed_personas.contains (persona) == true ||
887 relinked_personas.contains (persona) == true)
890 relinked_personas.add (persona);
892 /* Remove links to the Persona */
893 this._remove_persona_from_link_map (persona);
896 if (user == individual)
899 this._disconnect_from_individual (individual);
900 individual.personas = null;
903 debug ("Adding Personas:");
904 foreach (var persona in added)
906 debug (" %s (is user: %s, IID: %s)", persona.uid,
907 persona.is_user ? "yes" : "no", persona.iid);
912 this._add_personas (added, ref added_individuals,
913 ref replaced_individuals, ref user);
916 debug ("Relinking Personas:");
917 foreach (var persona in relinked_personas)
919 debug (" %s (is user: %s, IID: %s)", persona.uid,
920 persona.is_user ? "yes" : "no", persona.iid);
923 this._add_personas (relinked_personas, ref added_individuals,
924 ref replaced_individuals, ref user);
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)
932 MapIterator<Individual, Individual> iter =
933 replaced_individuals.map_iterator ();
934 while (iter.next () == true)
935 removed_individuals.add (iter.get_key ());
938 /* Notify of changes to this.user */
941 /* Signal the addition of new individuals and removal of old ones to the
943 if (added_individuals.size > 0 || removed_individuals.size > 0)
945 this._emit_individuals_changed (added_individuals,
946 removed_individuals);
949 /* Signal the replacement of various Individuals as a consequence of
951 debug ("Replacing Individuals due to linking:");
952 var iter = replaced_individuals.map_iterator ();
953 while (iter.next () == true)
955 iter.get_key ().replace (iter.get_value ());
959 private void _is_writeable_changed_cb (Object object, ParamSpec pspec)
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));
967 private void _trust_level_changed_cb (Object object, ParamSpec pspec)
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);
975 assert (store.trust_level != PersonaStoreTrust.FULL);
978 private void _individual_removed_cb (Individual i, Individual? replacement)
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)
988 var individuals = new HashSet<Individual> ();
991 if (replacement != null)
993 debug ("Individual '%s' removed (replaced by '%s')", i.id,
998 debug ("Individual '%s' removed (not replaced)", i.id);
1001 /* If the individual has 0 personas, we've already signaled removal */
1002 if (i.personas.size > 0)
1004 this._emit_individuals_changed (null, individuals);
1007 this._disconnect_from_individual (i);
1011 * Add a new persona in the given {@link PersonaStore} based on the `details`
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.
1019 * The details hash is a backend-specific mapping of key, value strings.
1020 * Common keys include:
1022 * * contact - service-specific contact ID
1023 * * message - a user-readable message to pass to the persona being added
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`.
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
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.
1039 public async Persona? add_persona_from_details (Individual? parent,
1040 PersonaStore persona_store,
1041 HashTable<string, Value?> details) throws IndividualAggregatorError
1043 Persona persona = null;
1046 var details_copy = this._asv_copy (details);
1047 persona = yield persona_store.add_persona_from_details (details_copy);
1049 catch (PersonaStoreError e)
1051 if (e is PersonaStoreError.STORE_OFFLINE)
1053 throw new IndividualAggregatorError.STORE_OFFLINE (e.message);
1057 var full_id = this._get_store_full_id (persona_store.type_id,
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);
1068 if (parent != null && persona != null)
1070 parent.personas.add (persona);
1076 private HashTable<string, Value?> _asv_copy (HashTable<string, Value?> asv)
1078 var retval = new HashTable<string, Value?> (str_hash, str_equal);
1080 asv.foreach ((k, v) =>
1082 retval.insert ((string) k, v);
1089 * Completely remove the individual and all of its personas from their
1092 * @param individual the {@link Individual} to remove
1095 public async void remove_individual (Individual individual) throws GLib.Error
1097 /* Removing personas changes the persona set so we need to make a copy
1099 var personas = new HashSet<Persona> ();
1100 foreach (var p in individual.personas)
1105 foreach (var persona in personas)
1107 yield persona.store.remove_persona (persona);
1112 * Completely remove the persona from its backing store.
1114 * This will leave other personas in the same individual alone.
1116 * @param persona the {@link Persona} to remove
1119 public async void remove_persona (Persona persona) throws GLib.Error
1121 yield persona.store.remove_persona (persona);
1125 * Link the given {@link Persona}s together.
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.
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}.
1135 * @param personas the {@link Persona}s to be linked
1138 public async void link_personas (Set<Persona> personas)
1139 throws IndividualAggregatorError
1141 if (this._writeable_store == null)
1143 throw new IndividualAggregatorError.NO_WRITEABLE_STORE (
1144 _("Can't link personas with no writeable store."));
1147 /* Don't bother linking if it's just one Persona */
1148 if (personas.size <= 1)
1151 /* Disallow linking if it's disabled */
1152 if (this._linking_enabled == false)
1154 debug ("Can't link Personas: linking disabled.");
1158 /* Create a new persona in the writeable store which links together the
1160 assert (this._writeable_store.type_id ==
1161 this._configured_writeable_store_type_id);
1163 /* `protocols_addrs_set` will be passed to the new Kf.Persona */
1164 var protocols_addrs_set = new HashMultiMap<string, ImFieldDetails> (
1166 (GLib.HashFunc) ImFieldDetails.hash,
1167 (GLib.EqualFunc) ImFieldDetails.equal);
1168 var web_service_addrs_set =
1169 new HashMultiMap<string, WebServiceFieldDetails> (
1171 (GLib.HashFunc) WebServiceFieldDetails.hash,
1172 (GLib.EqualFunc) WebServiceFieldDetails.equal);
1174 /* List of local_ids */
1175 var local_ids = new Gee.HashSet<string> ();
1177 foreach (var persona in personas)
1179 if (persona is ImDetails)
1181 ImDetails im_details = (ImDetails) persona;
1183 /* protocols_addrs_set = union (all personas' IM addresses) */
1184 foreach (var protocol in im_details.im_addresses.get_keys ())
1186 var im_addresses = im_details.im_addresses.get (protocol);
1188 foreach (var im_address in im_addresses)
1190 protocols_addrs_set.set (protocol, im_address);
1195 if (persona is WebServiceDetails)
1197 WebServiceDetails ws_details = (WebServiceDetails) persona;
1199 /* web_service_addrs_set = union (all personas' WS addresses) */
1200 foreach (var web_service in
1201 ws_details.web_service_addresses.get_keys ())
1204 ws_details.web_service_addresses.get (web_service);
1206 foreach (var ws_fd in ws_addresses)
1207 web_service_addrs_set.set (web_service, ws_fd);
1211 if (persona is LocalIdDetails)
1213 foreach (var id in ((LocalIdDetails) persona).local_ids)
1220 var details = new HashTable<string, Value?> (str_hash, str_equal);
1222 if (protocols_addrs_set.size > 0)
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);
1230 if (web_service_addrs_set.size > 0)
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);
1239 if (local_ids.size > 0)
1241 var local_ids_value = Value (typeof (Set<string>));
1242 local_ids_value.set_object (local_ids);
1244 Folks.PersonaStore.detail_key (PersonaDetail.LOCAL_IDS),
1248 yield this.add_persona_from_details (null,
1249 this._writeable_store, details);
1253 * Unlinks the given {@link Individual} into its constituent {@link Persona}s.
1255 * This completely unlinks the given {@link Individual}, destroying all of
1256 * its writeable {@link Persona}s.
1258 * The {@link Individual}'s removal is signalled by
1259 * {@link IndividualAggregator.individuals_changed} and
1260 * {@link Individual.removed}.
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}.
1268 * @param individual the {@link Individual} to unlink
1271 public async void unlink_individual (Individual individual) throws GLib.Error
1273 if (this._linking_enabled == false)
1275 debug ("Can't unlink Individual '%s': linking disabled.",
1280 debug ("Unlinking Individual '%s', deleting Personas:", individual.id);
1282 /* Remove all the Personas from writeable PersonaStores.
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
1288 var personas = new HashSet<Persona> ();
1289 foreach (var p in individual.personas)
1294 foreach (var persona in personas)
1296 if (persona.store == this._writeable_store)
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);