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.
40 [Deprecated (since = "0.6.2.1",
41 replacement = "IndividualAggregatorError.NO_PRIMARY_STORE")]
45 * The {@link PersonaStore} was offline (ie, this is a temporary failure).
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.
58 PROPERTY_NOT_WRITEABLE,
61 * An operation which required the use of a primary store failed because no
62 * primary store was available.
70 * Stores {@link Individual}s which have been created through
71 * aggregation of all the {@link Persona}s provided by the various
74 * This is the main interface for client applications.
76 public class Folks.IndividualAggregator : Object
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;
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";
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;
110 * Whether {@link IndividualAggregator.prepare} has successfully completed for
115 public bool is_prepared
117 get { return this._is_prepared; }
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
127 * It's guaranteed that this property's value will only ever change after
128 * {@link IndividualAggregator.is_prepared} has changed to `true`.
132 public bool is_quiescent
134 get { return this._is_quiescent; }
138 * Our configured primary (writeable) store.
140 * Which one to use is decided (in order or precedence)
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
149 public PersonaStore? primary_store
151 get { return this._primary_store; }
154 private Map<string, Individual> _individuals;
155 private Map<string, Individual> _individuals_ro;
158 * A map from {@link Individual.id}s to their {@link Individual}s.
160 * This is the canonical set of {@link Individual}s provided by this
161 * IndividualAggregator.
163 * {@link Individual}s may be added or removed using
164 * {@link IndividualAggregator.add_persona_from_details} and
165 * {@link IndividualAggregator.remove_individual}, respectively.
169 public Map<string, Individual> individuals
171 get { return this._individuals_ro; }
174 this._individuals = value;
175 this._individuals_ro = this._individuals.read_only_view;
180 * The {@link Individual} representing the user.
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.
188 public Individual? user { get; private set; }
191 * Emitted when one or more {@link Individual}s are added to or removed from
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.
199 * This will not be emitted until after {@link IndividualAggregator.prepare}
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
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,
216 GroupDetails.ChangeReason reason);
219 * Emitted when one or more {@link Individual}s are added to or removed from
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.
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
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
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.
242 * Individuals which have been removed will be listed in the multi-map as
243 * mappings from the removed individual to `null`.
245 * This will not be emitted until after {@link IndividualAggregator.prepare}
248 * @param changes a mapping of old {@link Individual}s to new
249 * {@link Individual}s for the individuals which have changed in the
254 public signal void individuals_changed_detailed (
255 MultiMap<Individual?, Individual?> changes);
257 /* FIXME: make this a singleton? */
259 * Create a new IndividualAggregator.
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.
267 * An example of how to set up an IndividualAggregator:
269 * IndividualAggregator agg = new IndividualAggregator ();
270 * agg.individuals_changed_detailed.connect (individuals_changed_cb);
274 public IndividualAggregator ()
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);
286 this._backends = new HashSet<Backend> ();
287 this._debug = Debug.dup ();
288 this._debug.print_status.connect (this._debug_print_status);
290 /* Check out the configured primary store */
291 var store_config_ids = Environment.get_variable ("FOLKS_PRIMARY_STORE");
292 if (store_config_ids == null)
294 store_config_ids = Environment.get_variable ("FOLKS_WRITEABLE_STORE");
295 if (store_config_ids != null)
297 var deprecated_warn = "FOLKS_WRITEABLE_STORE is deprecated, ";
298 deprecated_warn += "use FOLKS_PRIMARY_STORE";
299 warning (deprecated_warn);
303 if (store_config_ids != null)
305 debug ("Setting primary store IDs from environment variable.");
306 this._configure_primary_store ((!) store_config_ids);
310 debug ("Setting primary store IDs to defaults.");
311 if (BuildConf.HAVE_EDS)
313 this._configured_primary_store_type_id = "eds";
314 this._configured_primary_store_id = "system";
318 this._configured_primary_store_type_id = "key-file";
319 this._configured_primary_store_id = "";
324 unowned GConf.Client client = GConf.Client.get_default ();
325 GConf.Value? val = client.get (this._FOLKS_CONFIG_KEY);
328 string? val_str = ((!) val).get_string ();
332 debug ("Setting primary store IDs from GConf.");
333 this._configure_primary_store ((!) val_str);
339 /* We ignore errors and go with the default store */
343 debug ("Primary store IDs are '%s' and '%s'.",
344 this._configured_primary_store_type_id,
345 this._configured_primary_store_id);
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");
353 this._backend_store = BackendStore.dup ();
355 debug ("Constructing IndividualAggregator %p", this);
358 ~IndividualAggregator ()
360 debug ("Destroying IndividualAggregator %p", this);
362 this._backend_store.backend_available.disconnect (
363 this._backend_available_cb);
365 this._debug.print_status.disconnect (this._debug_print_status);
368 private void _configure_primary_store (string store_config_ids)
370 debug ("_configure_primary_store to '%s'", store_config_ids);
371 this._user_configured_primary_store = true;
373 if (store_config_ids.index_of (":") != -1)
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];
381 this._configured_primary_store_type_id = store_config_ids;
382 this._configured_primary_store_id = "";
386 private void _debug_print_status (Debug debug)
388 const string domain = Debug.STATUS_LOG_DOMAIN;
389 const LogLevelFlags level = LogLevelFlags.LEVEL_INFO;
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
401 : "no (%u backends, %u persona stores left)".printf (
402 this._non_quiescent_backend_count,
403 this._non_quiescent_persona_store_count)
406 debug.print_line (domain, level,
407 "%u Individuals:", this.individuals.size);
410 foreach (var individual in this.individuals.values)
412 string? trust_level = null;
414 switch (individual.trust_level)
416 case TrustLevel.NONE:
417 trust_level = "none";
419 case TrustLevel.PERSONAS:
420 trust_level = "personas";
423 assert_not_reached ();
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 (),
430 "User?", individual.is_user ? "yes" : "no",
431 "Trust level", trust_level
433 debug.print_line (domain, level, "%u Personas:",
434 individual.personas.size);
438 foreach (var persona in individual.personas)
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 (),
445 "Display ID", persona.display_id,
446 "User?", persona.is_user ? "yes" : "no"
455 debug.print_line (domain, level, "%u entries in the link map:",
456 this._link_map.size ());
459 var iter = HashTableIter<string, Individual> (this._link_map);
461 Individual individual;
462 while (iter.next (out link_key, out individual) == true)
464 debug.print_line (domain, level,
465 "%s → %p", link_key, individual);
470 debug.print_line (domain, level, "");
474 * Prepare the IndividualAggregator for use.
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.
483 * This function is guaranteed to be idempotent (since version 0.3.0).
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
493 public async void prepare () throws GLib.Error
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. */
499 lock (this._is_prepared)
501 if (this._is_prepared || this._prepare_pending)
508 this._prepare_pending = true;
510 this._backend_store.backend_available.connect (
511 this._backend_available_cb);
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)
519 this._backend_available_cb (this._backend_store, backend);
522 /* Load any backends which haven't been loaded already. (Typically
524 yield this._backend_store.load_backends ();
526 this._is_prepared = true;
527 this.notify_property ("is-prepared");
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)
535 this._notify_if_is_quiescent ();
540 this._prepare_pending = false;
546 * Get all matches for a given {@link Individual}.
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
556 public Map<Individual, MatchResult> get_potential_matches
557 (Individual matchee, MatchResult min_threshold = MatchResult.VERY_HIGH)
559 HashMap<Individual, MatchResult> matches =
560 new HashMap<Individual, MatchResult> ();
561 Folks.PotentialMatch matchObj = new Folks.PotentialMatch ();
563 foreach (var i in this._individuals.values)
565 if (i.id == matchee.id)
568 var result = matchObj.potential_match (i, matchee);
569 if (result >= min_threshold)
571 matches.set (i, result);
579 * Get all combinations between all {@link Individual}s.
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`)
589 public Map<Individual, Map<Individual, MatchResult>>
590 get_all_potential_matches
591 (MatchResult min_threshold = MatchResult.VERY_HIGH)
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 ();
598 for (var i = 0; i < individuals.length; i++)
600 var a = individuals[i];
602 HashMap<Individual, MatchResult>? _matches_a = matches.get (a);
603 HashMap<Individual, MatchResult> matches_a;
604 if (_matches_a == null)
606 matches_a = new HashMap<Individual, MatchResult> ();
607 matches.set (a, matches_a);
611 matches_a = (!) _matches_a;
614 for (var f = i + 1; f < individuals.length; f++)
616 var b = individuals[f];
618 HashMap<Individual, MatchResult>? _matches_b = matches.get (b);
619 HashMap<Individual, MatchResult> matches_b;
620 if (_matches_b == null)
622 matches_b = new HashMap<Individual, MatchResult> ();
623 matches.set (b, matches_b);
627 matches_b = (!) _matches_b;
630 var result = matchObj.potential_match (a, b);
632 if (result >= min_threshold)
634 matches_a.set (b, result);
635 matches_b.set (a, result);
643 private async void _add_backend (Backend backend)
645 if (!this._backends.contains (backend))
647 this._backends.add (backend);
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);
656 /* Handle the stores that have already been signaled. Since
657 * this might change while we are looping, get a copy first.
659 var stores = backend.persona_stores.values.to_array ();
660 foreach (var persona_store in stores)
662 this._backend_persona_store_added_cb (backend, persona_store);
667 private void _backend_available_cb (BackendStore backend_store,
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
675 if (backend.is_quiescent == false)
677 this._non_quiescent_backend_count++;
680 this._add_backend.begin (backend);
683 private void _set_primary_store (PersonaStore store)
685 debug ("_set_primary_store()");
687 if (this._primary_store == store)
690 /* We use the configured PersonaStore as the primary PersonaStore.
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.
696 if (store.type_id == this._configured_primary_store_type_id)
698 if ((store.type_id != "eds" &&
699 this._configured_primary_store_id == "") ||
700 this._configured_primary_store_id == store.id)
702 debug ("Setting primary store to %p (type ID: %s, ID: %s)",
703 store, store.type_id, store.id);
705 var previous_store = this._primary_store;
706 this._primary_store = store;
708 store.freeze_notify ();
709 if (previous_store != null)
711 ((!) previous_store).freeze_notify ();
712 ((!) previous_store).is_primary_store = false;
714 store.is_primary_store = true;
715 if (previous_store != null)
716 ((!) previous_store).thaw_notify ();
717 store.thaw_notify ();
719 this.notify_property ("primary-store");
724 private void _backend_persona_store_added_cb (Backend backend,
727 debug ("_backend_persona_store_added_cb(): backend: %s, store: %s (%p)",
728 backend.name, store.id, store);
730 var store_id = this._get_store_full_id (store.type_id, store.id);
732 this._maybe_configure_as_primary (store);
733 this._set_primary_store (store);
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);
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
749 if (store.is_quiescent == false)
751 this._non_quiescent_persona_store_count++;
754 /* Handle any pre-existing personas in the store. This can happen if the
755 * store existed (and was prepared) before this IndividualAggregator was
757 if (store.personas.size > 0)
759 var persona_set = new HashSet<Persona> ();
760 foreach (var p in store.personas.values)
765 this._personas_changed_cb (store, persona_set,
766 new HashSet<Persona> (), null, null,
767 GroupDetails.ChangeReason.NONE);
770 /* Prepare the store and receive a load of other personas-changed
772 store.prepare.begin ((obj, result) =>
776 store.prepare.end (result);
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,
788 private void _backend_persona_store_removed_cb (Backend backend,
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);
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)
803 this._non_quiescent_persona_store_count--;
804 this._notify_if_is_quiescent ();
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
811 if (this._primary_store == store)
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");
818 this._stores.unset (this._get_store_full_id (store.type_id, store.id));
821 private string _get_store_full_id (string type_id, string id)
823 return type_id + ":" + id;
826 /* Emit the individuals-changed signal ensuring that null parameters are
827 * turned into empty sets, and both sets passed to signal handlers are
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)
836 Set<Individual> _added;
837 Set<Individual> _removed;
838 MultiMap<Individual?, Individual?> _changes;
840 if ((added == null || ((!) added).size == 0) &&
841 (removed == null || ((!) removed).size == 0) &&
842 (changes == null || ((!) changes).size == 0))
844 /* Don't bother emitting it if nothing's changed */
848 _added = (added != null) ? (!) added : new HashSet<Individual> ();
849 _removed = (removed != null) ? (!) removed : new HashSet<Individual> ();
853 _changes = (!) changes;
857 _changes = new HashMultiMap<Individual?, Individual?> ();
861 if (this._debug.debug_output_enabled == true)
863 debug ("Emitting individuals-changed-detailed with %u mappings:",
866 foreach (var removed_ind in _changes.get_keys ())
868 foreach (var added_ind in _changes.get (removed_ind))
870 debug (" %s (%p) → %s (%p)",
871 (removed_ind != null) ? ((!) removed_ind).id : "",
873 (added_ind != null) ? ((!) added_ind).id : "", added_ind);
875 if (removed_ind != null)
877 debug (" Removed individual's personas:");
879 foreach (var p in ((!) removed_ind).personas)
881 debug (" %s (%p)", p.uid, p);
885 if (added_ind != null)
887 debug (" Added individual's personas:");
889 foreach (var p in ((!) added_ind).personas)
891 debug (" %s (%p)", p.uid, p);
898 this.individuals_changed (_added.read_only_view, _removed.read_only_view,
899 message, actor, reason);
900 this.individuals_changed_detailed (_changes);
903 private void _connect_to_individual (Individual individual)
905 individual.removed.connect (this._individual_removed_cb);
906 this._individuals.set (individual.id, individual);
909 private void _disconnect_from_individual (Individual individual)
911 this._individuals.unset (individual.id);
912 individual.removed.disconnect (this._individual_removed_cb);
915 private void _add_personas (Set<Persona> added, ref Individual? user,
916 ref HashMultiMap<Individual?, Individual?> individuals_changes)
918 foreach (var persona in added)
920 PersonaStoreTrust trust_level = persona.store.trust_level;
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> ();
932 var final_personas = new HashSet<Persona> ();
934 debug ("Aggregating persona '%s' on '%s'.", persona.uid, persona.iid);
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)
940 debug (" Found candidate individual '%s' as user.",
942 candidate_inds.add ((!) user);
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)
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))
954 debug (" Found candidate individual '%s' by IID '%s'.",
955 ((!) candidate_ind).id, persona.iid);
956 candidate_inds.add ((!) candidate_ind);
960 if (persona.store.trust_level == PersonaStoreTrust.FULL)
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)
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;
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)
976 /* Translators: the parameter is a property name. */
977 _("Unknown property '%s' in linkable property list."),
982 persona.linkable_property_to_links (prop_name, (l) =>
984 unowned string prop_linking_value = l;
985 Individual? candidate_ind =
986 this._link_map.lookup (prop_linking_value);
988 if (candidate_ind != null &&
989 ((!) candidate_ind).trust_level != TrustLevel.NONE &&
990 !candidate_inds.contains ((!) candidate_ind))
992 debug (" Found candidate individual '%s' by " +
993 "linkable property '%s' = '%s'.",
994 ((!) candidate_ind).id, prop_name,
996 candidate_inds.add ((!) candidate_ind);
1002 /* Ensure the original persona makes it into the final individual */
1003 final_personas.add (persona);
1005 if (candidate_inds.size > 0 && this._linking_enabled == true)
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)
1014 final_personas.add_all (individual.personas);
1017 else if (candidate_inds.size > 0)
1019 debug (" Linking disabled.");
1023 debug (" Did not find any candidate individuals.");
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)
1032 debug (" %s (%p)", p.uid, p);
1033 this._add_persona_to_link_map (p, final_individual);
1036 uint num_mappings_added = 0;
1038 foreach (var i in candidate_inds)
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?> ();
1045 foreach (var k in individuals_changes.get_keys ())
1047 if (i in individuals_changes.get (k))
1049 transitive_updates.add (k);
1053 foreach (var k in transitive_updates)
1055 assert (individuals_changes.remove (k, i) == true);
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). */
1063 individuals_changes.set (k, final_individual);
1064 num_mappings_added++;
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)
1074 individuals_changes.set (i, final_individual);
1075 num_mappings_added++;
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)
1084 individuals_changes.set (null, final_individual);
1087 /* If the final Individual is the user, set them as such. */
1088 if (final_individual.is_user == true)
1089 user = final_individual;
1093 private void _persona_linkable_property_changed_cb (Object obj,
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);
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);
1106 var persona_set = new HashSet<Persona> ();
1107 persona_set.add (persona);
1109 this._personas_changed_cb (persona.store, persona_set, persona_set,
1110 null, null, GroupDetails.ChangeReason.NONE);
1113 private void _connect_to_persona (Persona persona)
1115 foreach (var prop_name in persona.linkable_properties)
1117 persona.notify[prop_name].connect (
1118 this._persona_linkable_property_changed_cb);
1122 private void _disconnect_from_persona (Persona persona)
1124 foreach (var prop_name in persona.linkable_properties)
1126 persona.notify[prop_name].disconnect (
1127 this._persona_linkable_property_changed_cb);
1131 private void _add_persona_to_link_map (Persona persona, Individual individual)
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);
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);
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)
1146 debug (" Inserting links:");
1148 /* Insert maps from the Persona's linkable properties to the
1150 foreach (unowned string prop_name in persona.linkable_properties)
1152 debug (" %s", prop_name);
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)
1159 /* Translators: the parameter is a property name. */
1160 _("Unknown property '%s' in linkable property list."),
1165 persona.linkable_property_to_links (prop_name, (l) =>
1167 unowned string prop_linking_value = l;
1169 debug (" %s", prop_linking_value);
1170 this._link_map.replace (prop_linking_value, individual);
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)
1184 debug ("Removing Individual '%s' from the link map.", individual.id);
1186 var iter = HashTableIter<string, Individual> (this._link_map);
1188 Individual link_individual;
1190 while (iter.next (out link_key, out link_individual) == true)
1192 if (link_individual == individual)
1194 debug (" %s → %s (%p)",
1195 link_key, link_individual.id, link_individual);
1202 private void _personas_changed_cb (PersonaStore store,
1204 Set<Persona> removed,
1207 GroupDetails.ChangeReason reason)
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> ();
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
1217 var user = this.user;
1219 debug ("Removing Personas:");
1221 foreach (var persona in removed)
1223 debug (" %s (is user: %s, IID: %s)", persona.uid,
1224 persona.is_user ? "yes" : "no", persona.iid);
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);
1232 removed_individuals.add ((!) ind);
1235 /* Stop listening to notifications about the persona's linkable
1237 this._disconnect_from_persona (persona);
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)
1250 /* Ensure we don't remove the same Individual twice */
1251 if (this._individuals.has_key (individual.id) == false)
1254 debug (" %s", individual.id);
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)
1260 if (removed.contains (persona) == true ||
1261 relinked_personas.contains (persona) == true)
1264 relinked_personas.add (persona);
1267 if (user == individual)
1270 this._disconnect_from_individual (individual);
1272 /* Remove the Individual's links from the link map */
1273 this._remove_individual_from_link_map (individual);
1276 debug ("Adding Personas:");
1277 foreach (var persona in added)
1279 debug (" %s (is user: %s, IID: %s)", persona.uid,
1280 persona.is_user ? "yes" : "no", persona.iid);
1282 /* Connect to notifications about the persona's linkable
1284 this._connect_to_persona (persona);
1289 this._add_personas (added, ref user, ref individuals_changes);
1292 debug ("Relinking Personas:");
1293 foreach (var persona in relinked_personas)
1295 debug (" %s (is user: %s, IID: %s)", persona.uid,
1296 persona.is_user ? "yes" : "no", persona.iid);
1299 this._add_personas (relinked_personas, ref user, ref individuals_changes);
1301 /* Work out which final individuals have replaced the removed_individuals
1302 * and update individuals_changes accordingly. */
1303 foreach (var individual in removed_individuals)
1305 var added_mapping = false;
1307 foreach (var persona in individual.personas)
1309 if (!(persona in removed) || (persona in added))
1311 individuals_changes.remove (null, persona.individual);
1312 individuals_changes.set (individual, persona.individual);
1313 added_mapping = true;
1317 /* Has the individual been removed entirely? */
1318 if (added_mapping == false)
1320 individuals_changes.set (individual, null);
1323 individual.personas = null;
1326 /* Notify of changes to this.user */
1329 /* Signal the addition of new individuals and removal of old ones to the
1331 if (individuals_changes.size > 0)
1333 var added_individuals = new HashSet<Individual> ();
1335 /* Extract the deprecated added and removed sets from
1336 * individuals_changes, to be used in the individuals_changed
1338 foreach (var old_ind in individuals_changes.get_keys ())
1340 foreach (var new_ind in individuals_changes.get (old_ind))
1342 assert (old_ind != null || new_ind != null);
1344 if (old_ind != null)
1346 removed_individuals.add ((!) old_ind);
1349 if (new_ind != null)
1351 added_individuals.add ((!) new_ind);
1352 this._connect_to_individual ((!) new_ind);
1355 if (old_ind != null && new_ind != null)
1357 replaced_individuals.set ((!) old_ind, (!) new_ind);
1362 this._emit_individuals_changed (added_individuals,
1363 removed_individuals, individuals_changes);
1366 /* Signal the replacement of various Individuals as a consequence of
1368 debug ("Replacing Individuals due to linking:");
1369 var iter = replaced_individuals.map_iterator ();
1370 while (iter.next () == true)
1372 var old_ind = iter.get_key ();
1373 var new_ind = iter.get_value ();
1375 debug (" %s (%p) → %s (%p)", old_ind.id, old_ind,
1376 new_ind.id, new_ind);
1378 old_ind.replace (new_ind);
1381 /* Validate the link map. */
1382 if (this._debug.debug_output_enabled == true)
1384 var iter2 = HashTableIter<string, Individual> (this._link_map);
1386 Individual individual;
1388 while (iter2.next (out link_key, out individual) == true)
1390 if (this._individuals.get (individual.id) != individual)
1392 warning ("Link map contains invalid mapping:\n" +
1394 link_key, individual.id, individual);
1395 warning ("Individual %s (%p) personas:", individual.id,
1397 foreach (var p in individual.personas)
1399 warning (" %s (%p)", p.uid, p);
1406 private void _is_primary_store_changed_cb (Object object, ParamSpec pspec)
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));
1416 private void _persona_store_is_quiescent_changed_cb (Object obj,
1419 /* Have we reached a quiescent state yet? */
1420 if (this._non_quiescent_persona_store_count > 0)
1422 this._non_quiescent_persona_store_count--;
1423 this._notify_if_is_quiescent ();
1427 private void _backend_is_quiescent_changed_cb (Object obj, ParamSpec pspec)
1429 if (this._non_quiescent_backend_count > 0)
1431 this._non_quiescent_backend_count--;
1432 this._notify_if_is_quiescent ();
1436 private void _notify_if_is_quiescent ()
1438 if (this._non_quiescent_backend_count == 0 &&
1439 this._non_quiescent_persona_store_count == 0 &&
1440 this._is_quiescent == false)
1442 if (this._configured_primary_store_type_id.length > 0 &&
1443 this._primary_store == null)
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 " +
1452 this._configured_primary_store_type_id,
1453 this._configured_primary_store_id);
1456 this._is_quiescent = true;
1457 this.notify_property ("is-quiescent");
1461 private void _persona_store_is_user_set_default_changed_cb (Object obj,
1464 var store = (PersonaStore) obj;
1466 debug ("PersonaStore.is-user-set-default changed for store %p " +
1467 "(type ID: %s, ID: %s)", store, store.type_id, store.id);
1469 if (this._maybe_configure_as_primary (store))
1470 this._set_primary_store (store);
1473 private bool _maybe_configure_as_primary (PersonaStore store)
1475 debug ("_maybe_configure_as_primary()");
1477 var configured = false;
1479 if (!this._user_configured_primary_store &&
1480 store.is_user_set_default)
1482 debug ("Setting primary store IDs to '%s' and '%s'.", store.type_id,
1484 this._configured_primary_store_type_id = store.type_id;
1485 this._configured_primary_store_id = store.id;
1492 private void _individual_removed_cb (Individual i, Individual? replacement)
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)
1502 if (replacement != null)
1504 debug ("Individual '%s' removed (replaced by '%s')", i.id,
1505 ((!) replacement).id);
1509 debug ("Individual '%s' removed (not replaced)", i.id);
1512 /* If the individual has 0 personas, we've already signaled removal */
1513 if (i.personas.size > 0)
1515 var changes = new HashMultiMap<Individual?, Individual?> ();
1516 var individuals = new HashSet<Individual> ();
1518 individuals.add (i);
1519 changes.set (i, replacement);
1521 this._emit_individuals_changed (null, individuals, changes);
1524 this._disconnect_from_individual (i);
1528 * Add a new persona in the given {@link PersonaStore} based on the `details`
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.
1536 * The details hash is a backend-specific mapping of key, value strings.
1537 * Common keys include:
1539 * * contact - service-specific contact ID
1540 * * message - a user-readable message to pass to the persona being added
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`.
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
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.
1556 public async Persona? add_persona_from_details (Individual? parent,
1557 PersonaStore persona_store,
1558 HashTable<string, Value?> details) throws IndividualAggregatorError
1560 Persona? persona = null;
1563 var details_copy = this._asv_copy (details);
1564 persona = yield persona_store.add_persona_from_details (details_copy);
1566 catch (PersonaStoreError e)
1568 if (e is PersonaStoreError.STORE_OFFLINE)
1570 throw new IndividualAggregatorError.STORE_OFFLINE (e.message);
1574 var full_id = this._get_store_full_id (persona_store.type_id,
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);
1585 if (parent != null && persona != null)
1587 ((!) parent).personas.add ((!) persona);
1593 private HashTable<string, Value?> _asv_copy (HashTable<string, Value?> asv)
1595 var retval = new HashTable<string, Value?> (str_hash, str_equal);
1597 asv.foreach ((k, v) =>
1599 retval.insert ((string) k, v);
1606 * Completely remove the individual and all of its personas from their
1609 * @param individual the {@link Individual} to remove
1612 public async void remove_individual (Individual individual) throws GLib.Error
1614 /* Removing personas changes the persona set so we need to make a copy
1616 var personas = new HashSet<Persona> ();
1617 foreach (var p in individual.personas)
1622 foreach (var persona in personas)
1624 yield persona.store.remove_persona (persona);
1629 * Completely remove the persona from its backing store.
1631 * This will leave other personas in the same individual alone.
1633 * @param persona the {@link Persona} to remove
1636 public async void remove_persona (Persona persona) throws GLib.Error
1638 yield persona.store.remove_persona (persona);
1642 * Link the given {@link Persona}s together.
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.
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}.
1652 * @param personas the {@link Persona}s to be linked
1655 public async void link_personas (Set<Persona> personas)
1656 throws IndividualAggregatorError
1658 if (this._primary_store == null)
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);
1668 /* Don't bother linking if it's just one Persona */
1669 if (personas.size <= 1)
1672 /* Disallow linking if it's disabled */
1673 if (this._linking_enabled == false)
1675 debug ("Can't link Personas: linking disabled.");
1679 /* Create a new persona in the primary store which links together the
1681 assert (((!) this._primary_store).type_id ==
1682 this._configured_primary_store_type_id);
1684 /* `protocols_addrs_set` will be passed to the new Kf.Persona */
1685 var protocols_addrs_set = new HashMultiMap<string, ImFieldDetails> (
1687 (GLib.HashFunc) ImFieldDetails.hash,
1688 (GLib.EqualFunc) ImFieldDetails.equal);
1689 var web_service_addrs_set =
1690 new HashMultiMap<string, WebServiceFieldDetails> (
1692 (GLib.HashFunc) WebServiceFieldDetails.hash,
1693 (GLib.EqualFunc) WebServiceFieldDetails.equal);
1695 /* List of local_ids */
1696 var local_ids = new Gee.HashSet<string> ();
1698 foreach (var persona in personas)
1700 if (persona is ImDetails)
1702 ImDetails im_details = (ImDetails) persona;
1704 /* protocols_addrs_set = union (all personas' IM addresses) */
1705 foreach (var protocol in im_details.im_addresses.get_keys ())
1707 var im_addresses = im_details.im_addresses.get (protocol);
1709 foreach (var im_address in im_addresses)
1711 protocols_addrs_set.set (protocol, im_address);
1716 if (persona is WebServiceDetails)
1718 WebServiceDetails ws_details = (WebServiceDetails) persona;
1720 /* web_service_addrs_set = union (all personas' WS addresses) */
1721 foreach (var web_service in
1722 ws_details.web_service_addresses.get_keys ())
1725 ws_details.web_service_addresses.get (web_service);
1727 foreach (var ws_fd in ws_addresses)
1728 web_service_addrs_set.set (web_service, ws_fd);
1732 if (persona is LocalIdDetails)
1734 foreach (var id in ((LocalIdDetails) persona).local_ids)
1741 var details = new HashTable<string, Value?> (str_hash, str_equal);
1743 if (protocols_addrs_set.size > 0)
1745 var im_addresses_value = Value (typeof (MultiMap));
1746 im_addresses_value.set_object (protocols_addrs_set);
1748 (!) PersonaStore.detail_key (PersonaDetail.IM_ADDRESSES),
1749 im_addresses_value);
1752 if (web_service_addrs_set.size > 0)
1754 var web_service_addresses_value = Value (typeof (MultiMap));
1755 web_service_addresses_value.set_object (web_service_addrs_set);
1757 (!) PersonaStore.detail_key (PersonaDetail.WEB_SERVICE_ADDRESSES),
1758 web_service_addresses_value);
1761 if (local_ids.size > 0)
1763 var local_ids_value = Value (typeof (Set));
1764 local_ids_value.set_object (local_ids);
1766 (!) Folks.PersonaStore.detail_key (PersonaDetail.LOCAL_IDS),
1770 yield this.add_persona_from_details (null,
1771 (!) this._primary_store, details);
1775 * Unlinks the given {@link Individual} into its constituent {@link Persona}s.
1777 * This completely unlinks the given {@link Individual}, destroying all of
1778 * its writeable {@link Persona}s.
1780 * The {@link Individual}'s removal is signalled by
1781 * {@link IndividualAggregator.individuals_changed} and
1782 * {@link Individual.removed}.
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}.
1790 * @param individual the {@link Individual} to unlink
1793 public async void unlink_individual (Individual individual) throws GLib.Error
1795 if (this._linking_enabled == false)
1797 debug ("Can't unlink Individual '%s': linking disabled.",
1802 debug ("Unlinking Individual '%s', deleting Personas:", individual.id);
1804 /* Remove all the Personas from writeable PersonaStores.
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
1810 var personas = new HashSet<Persona> ();
1811 foreach (var p in individual.personas)
1816 foreach (var persona in personas)
1818 /* Since persona.store != null, we know that
1819 * this._primary_store != null. */
1820 if (persona.store == this._primary_store)
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);
1830 * Ensure that the given property is writeable for the given
1831 * {@link Individual}.
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.)
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
1847 * @param individual the individual for which `property_name` should be
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
1856 public async Persona ensure_individual_property_writeable (
1857 Individual individual, string property_name)
1858 throws IndividualAggregatorError
1860 debug ("ensure_individual_property_writeable: %s, %s",
1861 individual.id, property_name);
1863 /* See if the individual already contains the property we want. */
1864 foreach (var p1 in individual.personas)
1866 if (property_name in p1.writeable_properties)
1868 debug (" Returning existing persona: %s", p1.uid);
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;
1879 if (this._primary_store != null &&
1881 ((!) this._primary_store).always_writeable_properties)
1885 debug (" Using writeable store");
1886 new_persona = yield this.add_persona_from_details (null,
1887 (!) this._primary_store, details);
1889 catch (IndividualAggregatorError e1)
1896 if (new_persona == null)
1898 foreach (var s in this._stores.values)
1900 if (s == this._primary_store ||
1901 !(property_name in s.always_writeable_properties))
1903 /* Skip the store we've just tried */
1909 debug (" Using store %s", s.id);
1910 new_persona = yield this.add_persona_from_details (null, s,
1913 catch (IndividualAggregatorError e2)
1922 /* Throw an error if we haven't managed to find a suitable store */
1923 if (new_persona == null && this._primary_store == null)
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);
1932 else if (new_persona == null)
1934 throw new IndividualAggregatorError.PROPERTY_NOT_WRITEABLE (
1935 _("Can't write to requested property (“%s”) of the writeable store."),
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);
1944 foreach (var p2 in individual.personas)
1946 linking_personas.add (p2);
1949 debug (" Linking personas to ensure %s property is writeable.",
1951 yield this.link_personas (linking_personas);
1953 return (!) new_persona;