* Travis Reitter <travis.reitter@collabora.co.uk>
*/
-using Folks;
using Gee;
using GLib;
public errordomain Folks.IndividualAggregatorError
{
/**
- * A specified {@link PersonaStore} could not be found.
- */
- STORE_NOT_FOUND,
-
- /**
* Adding a {@link Persona} to a {@link PersonaStore} failed.
*/
ADD_FAILED,
/**
* An operation which required the use of a writeable store failed because no
* writeable store was available.
+ *
+ * @since 0.1.13
*/
NO_WRITEABLE_STORE,
+
+ /**
+ * The {@link PersonaStore} was offline (ie, this is a temporary failure).
+ *
+ * @since 0.3.0
+ */
+ STORE_OFFLINE,
}
/**
- * Allows access to the {@link Individual}s which have been created through
+ * Stores {@link Individual}s which have been created through
* aggregation of all the {@link Persona}s provided by the various
- * {@link Backend}s. This is the main interface for client applications.
+ * {@link Backend}s.
+ *
+ * This is the main interface for client applications.
*/
public class Folks.IndividualAggregator : Object
{
- private BackendStore backend_store;
- private HashMap<string, PersonaStore> stores;
- private unowned PersonaStore writeable_store;
- private HashSet<Backend> backends;
- private HashTable<string, Individual> link_map;
+ private BackendStore _backend_store;
+ private HashMap<string, PersonaStore> _stores;
+ private unowned PersonaStore _writeable_store;
+ private HashSet<Backend> _backends;
+ private HashTable<string, Individual> _link_map;
+ private bool _linking_enabled = true;
+ private bool _is_prepared = false;
+ private Debug _debug;
+ private string _configured_writeable_store_type_id;
+ private static const string _FOLKS_CONFIG_KEY =
+ "/system/folks/backends/primary_store";
/**
- * A table mapping {@link Individual.id}s to their {@link Individual}s.
+ * Whether {@link IndividualAggregator.prepare} has successfully completed for
+ * this aggregator.
+ *
+ * @since 0.3.0
+ */
+ public bool is_prepared
+ {
+ get { return this._is_prepared; }
+ }
+
+ /**
+ * Our configured primary (writeable) store.
+ *
+ * Which one to use is decided (in order or precedence)
+ * by:
+ *
+ * - the FOLKS_WRITEABLE_STORE env var (mostly for debugging)
+ * - the GConf key set in _FOLKS_CONFIG_KEY (system set store)
+ * - going with the `key-file` store as the fall-back option
+ *
+ * @since 0.5.0
+ */
+ public PersonaStore primary_store
+ {
+ get { return this._writeable_store; }
+ }
+
+ private Map<string, Individual> _individuals;
+ private Map<string, Individual> _individuals_ro;
+
+ /**
+ * A map from {@link Individual.id}s to their {@link Individual}s.
*
* This is the canonical set of {@link Individual}s provided by this
* IndividualAggregator.
* {@link Individual}s may be added or removed using
* {@link IndividualAggregator.add_persona_from_details} and
* {@link IndividualAggregator.remove_individual}, respectively.
+ *
+ * @since 0.5.1
+ */
+ public Map<string, Individual> individuals
+ {
+ get { return this._individuals_ro; }
+ private set
+ {
+ this._individuals = value;
+ this._individuals_ro = this._individuals.read_only_view;
+ }
+ }
+
+ /**
+ * The {@link Individual} representing the user.
+ *
+ * If it exists, this holds the {@link Individual} who is the user: the
+ * {@link Individual} containing the {@link Persona}s who are the owners of
+ * the accounts for their respective backends.
+ *
+ * @since 0.3.0
*/
- public HashTable<string, Individual> individuals { get; private set; }
+ public Individual user { get; private set; }
/**
* Emitted when one or more {@link Individual}s are added to or removed from
* This will not be emitted until after {@link IndividualAggregator.prepare}
* has been called.
*
- * @param added a list of {@link Individual}s which have been removed
+ * @param added a list of {@link Individual}s which have been added
* @param removed a list of {@link Individual}s which have been removed
* @param message a string message from the backend, if any
* @param actor the {@link Persona} who made the change, if known
* @param reason the reason for the change
+ *
+ * @since 0.5.1
*/
- public signal void individuals_changed (GLib.List<Individual>? added,
- GLib.List<Individual>? removed,
+ public signal void individuals_changed (Set<Individual> added,
+ Set<Individual> removed,
string? message,
Persona? actor,
- Groups.ChangeReason reason);
+ GroupDetails.ChangeReason reason);
/* FIXME: make this a singleton? */
/**
*/
public IndividualAggregator ()
{
- this.stores = new HashMap<string, PersonaStore> ();
- this.individuals = new HashTable<string, Individual> (str_hash,
- str_equal);
- this.link_map = new HashTable<string, Individual> (str_hash, str_equal);
+ this._stores = new HashMap<string, PersonaStore> ();
+ this._individuals = new HashMap<string, Individual> ();
+ this._individuals_ro = this._individuals.read_only_view;
+ this._link_map = new HashTable<string, Individual> (str_hash, str_equal);
+
+ this._backends = new HashSet<Backend> ();
+ this._debug = Debug.dup ();
+ this._debug.print_status.connect (this._debug_print_status);
+
+ /* Check out the configured writeable store */
+ var store_type_id = Environment.get_variable ("FOLKS_WRITEABLE_STORE");
+ if (store_type_id != null)
+ {
+ this._configured_writeable_store_type_id = store_type_id;
+ }
+ else
+ {
+ this._configured_writeable_store_type_id = "key-file";
+ try
+ {
+ unowned GConf.Client client = GConf.Client.get_default ();
+ GConf.Value? val = client.get (this._FOLKS_CONFIG_KEY);
+ if (val != null)
+ this._configured_writeable_store_type_id = val.get_string ();
+ }
+ catch (GLib.Error e)
+ {
+ /* We ignore errors and go with the default store */
+ }
+ }
+
+ var disable_linking = Environment.get_variable ("FOLKS_DISABLE_LINKING");
+ if (disable_linking != null)
+ disable_linking = disable_linking.strip ().down ();
+ this._linking_enabled = (disable_linking == null ||
+ disable_linking == "no" || disable_linking == "0");
+
+ this._backend_store = BackendStore.dup ();
+ this._backend_store.backend_available.connect (
+ this._backend_available_cb);
+ }
+
+ ~IndividualAggregator ()
+ {
+ this._backend_store.backend_available.disconnect (
+ this._backend_available_cb);
+ this._backend_store = null;
+
+ this._debug.print_status.disconnect (this._debug_print_status);
+ }
+
+ private void _debug_print_status (Debug debug)
+ {
+ const string domain = Debug.STATUS_LOG_DOMAIN;
+ const LogLevelFlags level = LogLevelFlags.LEVEL_INFO;
+
+ debug.print_heading (domain, level, "IndividualAggregator (%p)", this);
+ debug.print_key_value_pairs (domain, level,
+ "Ref. count", this.ref_count.to_string (),
+ "Writeable store", "%p".printf (this._writeable_store),
+ "Linking enabled?", this._linking_enabled ? "yes" : "no",
+ "Prepared?", this._is_prepared ? "yes" : "no"
+ );
+
+ debug.print_line (domain, level,
+ "%u Individuals:", this.individuals.size);
+ debug.indent ();
+
+ foreach (var individual in this.individuals.values)
+ {
+ string trust_level = null;
+
+ switch (individual.trust_level)
+ {
+ case TrustLevel.NONE:
+ trust_level = "none";
+ break;
+ case TrustLevel.PERSONAS:
+ trust_level = "personas";
+ break;
+ default:
+ assert_not_reached ();
+ }
+
+ debug.print_heading (domain, level, "Individual (%p)", individual);
+ debug.print_key_value_pairs (domain, level,
+ "Ref. count", individual.ref_count.to_string (),
+ "ID", individual.id,
+ "User?", individual.is_user ? "yes" : "no",
+ "Trust level", trust_level
+ );
+ debug.print_line (domain, level, "%u Personas:",
+ individual.personas.size);
+
+ debug.indent ();
+
+ foreach (var persona in individual.personas)
+ {
+ debug.print_heading (domain, level, "Persona (%p)", persona);
+ debug.print_key_value_pairs (domain, level,
+ "Ref. count", persona.ref_count.to_string (),
+ "UID", persona.uid,
+ "IID", persona.iid,
+ "Display ID", persona.display_id,
+ "User?", persona.is_user ? "yes" : "no"
+ );
+ }
+
+ debug.unindent ();
+ }
- this.backends = new HashSet<Backend> ();
+ debug.unindent ();
- Debug.set_flags (Environment.get_variable ("FOLKS_DEBUG"));
+ debug.print_line (domain, level, "%u entries in the link map:",
+ this._link_map.size ());
+ debug.indent ();
- this.backend_store = new BackendStore ();
- this.backend_store.backend_available.connect (this.backend_available_cb);
+ var iter = HashTableIter<string, Individual> (this._link_map);
+ string link_key;
+ Individual individual;
+ while (iter.next (out link_key, out individual) == true)
+ {
+ debug.print_line (domain, level,
+ "%s → %p", link_key, individual);
+ }
+
+ debug.unindent ();
+
+ debug.print_line (domain, level, "");
}
/**
* {@link IndividualAggregator.individuals_changed} signal, or a race
* condition could occur, with the signal being emitted before your code has
* connected to them, and {@link Individual}s getting "lost" as a result.
+ *
+ * This function is guaranteed to be idempotent (since version 0.3.0).
+ *
+ * @since 0.1.11
*/
public async void prepare () throws GLib.Error
{
- this.backend_store.load_backends ();
+ /* Once this async function returns, all the {@link Backend}s will have
+ * been prepared (though no {@link PersonaStore}s are guaranteed to be
+ * available yet). This last guarantee is new as of version 0.2.0. */
+
+ lock (this._is_prepared)
+ {
+ if (!this._is_prepared)
+ {
+ yield this._backend_store.load_backends ();
+ this._is_prepared = true;
+ this.notify_property ("is-prepared");
+ }
+ }
}
- private void backend_available_cb (BackendStore backend_store,
- Backend backend)
+ /**
+ * Get all matches for a given {@link Individual}.
+ *
+ * @since 0.5.1
+ */
+ public Map<Individual, MatchResult> get_potential_matches
+ (Individual matchee, MatchResult min_threshold = MatchResult.VERY_HIGH)
{
- backend.persona_store_added.connect (this.backend_persona_store_added_cb);
- backend.persona_store_removed.connect (
- this.backend_persona_store_removed_cb);
+ HashMap<Individual, MatchResult> matches =
+ new HashMap<Individual, MatchResult> ();
+ Folks.PotentialMatch matchObj = new Folks.PotentialMatch ();
- backend.prepare.begin ((obj, result) =>
+ foreach (var i in this._individuals.values)
{
- try
+ if (i.id == matchee.id)
+ continue;
+
+ var result = matchObj.potential_match (i, matchee);
+ if (result >= min_threshold)
{
- backend.prepare.end (result);
+ matches.set (i, result);
}
- catch (GLib.Error e)
+ }
+
+ return matches;
+ }
+
+ /**
+ * Get all combinations between all {@link Individual}s.
+ *
+ * @since 0.5.1
+ */
+ public Map<Individual, Map<Individual, MatchResult>>
+ get_all_potential_matches
+ (MatchResult min_threshold = MatchResult.VERY_HIGH)
+ {
+ HashMap<Individual, HashMap<Individual, MatchResult>> matches =
+ new HashMap<Individual, HashMap<Individual, MatchResult>> ();
+ var individuals = this._individuals.values.to_array ();
+ Folks.PotentialMatch matchObj = new Folks.PotentialMatch ();
+
+ for (var i = 0; i < individuals.length; i++)
+ {
+ var a = individuals[i];
+ var matches_a = matches.get (a);
+ if (matches_a == null)
{
- warning ("Error preparing Backend '%s': %s", backend.name,
- e.message);
+ matches_a = new HashMap<Individual, MatchResult> ();
+ matches.set (a, matches_a);
}
- });
+
+ for (var f = i + 1; f < individuals.length; f++)
+ {
+ var b = individuals[f];
+ var matches_b = matches.get (b);
+ if (matches_b == null)
+ {
+ matches_b = new HashMap<Individual, MatchResult> ();
+ matches.set (b, matches_b);
+ }
+
+ var result = matchObj.potential_match (a, b);
+
+ if (result >= min_threshold)
+ {
+ matches_a.set (b, result);
+ matches_b.set (a, result);
+ }
+ }
+ }
+
+ return matches;
+ }
+
+ private async void _add_backend (Backend backend)
+ {
+ if (!this._backends.contains (backend))
+ {
+ this._backends.add (backend);
+
+ backend.persona_store_added.connect (
+ this._backend_persona_store_added_cb);
+ backend.persona_store_removed.connect (
+ this._backend_persona_store_removed_cb);
+
+ /* handle the stores that have already been signaled */
+ foreach (var persona_store in backend.persona_stores.values)
+ {
+ this._backend_persona_store_added_cb (backend, persona_store);
+ }
+ }
+ }
+
+ private void _backend_available_cb (BackendStore backend_store,
+ Backend backend)
+ {
+ this._add_backend.begin (backend);
}
- private void backend_persona_store_added_cb (Backend backend,
+ private void _backend_persona_store_added_cb (Backend backend,
PersonaStore store)
{
- string store_id = this.get_store_full_id (store.type_id, store.id);
+ var store_id = this._get_store_full_id (store.type_id, store.id);
- /* FIXME: We hardcode the key-file backend's singleton PersonaStore as the
- * only trusted and writeable PersonaStore for now. */
- if (store.type_id == "key-file")
+ /* We use the configured PersonaStore as the only trusted and writeable
+ * PersonaStore. */
+ if (store.type_id == this._configured_writeable_store_type_id)
{
store.is_writeable = true;
store.trust_level = PersonaStoreTrust.FULL;
- this.writeable_store = store;
+ this._writeable_store = store;
}
- this.stores.set (store_id, store);
- store.personas_changed.connect (this.personas_changed_cb);
- store.notify["is-writeable"].connect (this.is_writeable_changed_cb);
- store.notify["trust-level"].connect (this.trust_level_changed_cb);
+ this._stores.set (store_id, store);
+ store.personas_changed.connect (this._personas_changed_cb);
+ store.notify["is-writeable"].connect (this._is_writeable_changed_cb);
+ store.notify["trust-level"].connect (this._trust_level_changed_cb);
store.prepare.begin ((obj, result) =>
{
}
catch (GLib.Error e)
{
- warning ("Error preparing PersonaStore '%s': %s", store_id,
+ /* Translators: the first parameter is a persona store identifier
+ * and the second is an error message. */
+ warning (_("Error preparing persona store '%s': %s"), store_id,
e.message);
}
});
}
- private void backend_persona_store_removed_cb (Backend backend,
+ private void _backend_persona_store_removed_cb (Backend backend,
PersonaStore store)
{
- store.personas_changed.disconnect (this.personas_changed_cb);
- store.notify["trust-level"].disconnect (this.trust_level_changed_cb);
- store.notify["is-writeable"].disconnect (this.is_writeable_changed_cb);
+ store.personas_changed.disconnect (this._personas_changed_cb);
+ store.notify["trust-level"].disconnect (this._trust_level_changed_cb);
+ store.notify["is-writeable"].disconnect (this._is_writeable_changed_cb);
/* no need to remove this store's personas from all the individuals, since
* they'll do that themselves (and emit their own 'removed' signal if
* necessary) */
- if (this.writeable_store == store)
- this.writeable_store = null;
- this.stores.unset (this.get_store_full_id (store.type_id, store.id));
+ if (this._writeable_store == store)
+ this._writeable_store = null;
+ this._stores.unset (this._get_store_full_id (store.type_id, store.id));
}
- private string get_store_full_id (string type_id, string id)
+ private string _get_store_full_id (string type_id, string id)
{
return type_id + ":" + id;
}
- private GLib.List<Individual> add_personas (GLib.List<Persona> added)
+ /* Emit the individuals-changed signal ensuring that null parameters are
+ * turned into empty sets, and both sets passed to signal handlers are
+ * read-only. */
+ private void _emit_individuals_changed (Set<Individual>? added,
+ Set<Individual>? removed,
+ string? message = null,
+ Persona? actor = null,
+ GroupDetails.ChangeReason reason = GroupDetails.ChangeReason.NONE)
{
- GLib.List<Individual> added_individuals = new GLib.List<Individual> ();
+ var _added = added;
+ var _removed = removed;
- added.foreach ((p) =>
+ if ((added == null || added.size == 0) &&
+ (removed == null || removed.size == 0))
+ {
+ /* Don't bother emitting it if nothing's changed */
+ return;
+ }
+ else if (added == null)
+ {
+ _added = new HashSet<Individual> ();
+ }
+ else if (removed == null)
+ {
+ _removed = new HashSet<Individual> ();
+ }
+
+ this.individuals_changed (_added.read_only_view, _removed.read_only_view,
+ message, actor, reason);
+ }
+
+ private void _connect_to_individual (Individual individual)
+ {
+ individual.removed.connect (this._individual_removed_cb);
+ this._individuals.set (individual.id, individual);
+ }
+
+ private void _disconnect_from_individual (Individual individual)
+ {
+ this._individuals.unset (individual.id);
+ individual.removed.disconnect (this._individual_removed_cb);
+ }
+
+ private void _add_personas (Set<Persona> added,
+ ref HashSet<Individual> added_individuals,
+ ref HashMap<Individual, Individual> replaced_individuals,
+ ref Individual user)
+ {
+ /* Set of individuals which have been added as a result of the new
+ * personas. These will be returned in added_individuals, but have to be
+ * cached first so that we can ensure that we don't return any given
+ * individual in both added_individuals _and_ replaced_individuals. This
+ * can happen in the case that several of the added personas are linked
+ * together to form one final individual. In that case, a succession of
+ * newly linked individuals will be produced (one for each iteration of
+ * the loop over the added personas); only the *last one* of which should
+ * make its way into added_individuals. The rest should not even make
+ * their way into replaced_individuals, as they've existed only within the
+ * confines of this function call. */
+ HashSet<Individual> almost_added_individuals = new HashSet<Individual> ();
+
+ foreach (var persona in added)
{
- unowned Persona persona = (Persona) p;
PersonaStoreTrust trust_level = persona.store.trust_level;
/* These are the Individuals whose Personas will be linked together
- * to form the `final_individual`. We keep a list of the Individuals
- * for fast iteration, but also keep a set to ensure that we don't
- * get duplicate Individuals in the list.
+ * to form the `final_individual`.
* Since a given Persona can only be part of one Individual, and the
* code in Persona._set_personas() ensures that there are no duplicate
* Personas in a given Individual, ensuring that there are no
- * duplicate Individuals in `candidate_inds` guarantees that there
- * will be no duplicate Personas in the `final_individual`. */
- GLib.List<Individual> candidate_inds = null;
- HashSet<Individual> candidate_ind_set = new HashSet<Individual> ();
+ * duplicate Individuals in `candidate_inds` (by using a
+ * HashSet) guarantees that there will be no duplicate Personas
+ * in the `final_individual`. */
+ HashSet<Individual> candidate_inds = new HashSet<Individual> ();
- GLib.List<Persona> final_personas = new GLib.List<Persona> ();
+ var final_personas = new HashSet<Persona> ();
Individual final_individual = null;
debug ("Aggregating persona '%s' on '%s'.", persona.uid, persona.iid);
+ /* If the Persona is the user, we *always* want to link it to the
+ * existing this.user. */
+ if (persona.is_user == true && user != null)
+ {
+ debug (" Found candidate individual '%s' as user.", user.id);
+ candidate_inds.add (user);
+ }
+
/* If we don't trust the PersonaStore at all, we can't link the
* Persona to any existing Individual */
if (trust_level != PersonaStoreTrust.NONE)
{
- Individual candidate_ind = this.link_map.lookup (persona.iid);
+ var candidate_ind = this._link_map.lookup (persona.iid);
if (candidate_ind != null &&
- candidate_ind.trust_level != TrustLevel.NONE)
+ candidate_ind.trust_level != TrustLevel.NONE &&
+ !candidate_inds.contains (candidate_ind))
{
debug (" Found candidate individual '%s' by IID '%s'.",
candidate_ind.id, persona.iid);
- candidate_inds.prepend (candidate_ind);
- candidate_ind_set.add (candidate_ind);
+ candidate_inds.add (candidate_ind);
}
}
* prop_name ends up as NULL. bgo#628336 */
unowned string prop_name = foo;
+ /* FIXME: can't be var because of bgo#638208 */
unowned ObjectClass pclass = persona.get_class ();
if (pclass.find_property (prop_name) == null)
{
- warning ("Unknown property '%s' in linkable property " +
- "list.", prop_name);
+ warning (
+ /* Translators: the parameter is a property name. */
+ _("Unknown property '%s' in linkable property list."),
+ prop_name);
continue;
}
persona.linkable_property_to_links (prop_name, (l) =>
{
- string prop_linking_value = (string) l;
- Individual candidate_ind =
- this.link_map.lookup (prop_linking_value);
+ unowned string prop_linking_value = l;
+ var candidate_ind =
+ this._link_map.lookup (prop_linking_value);
if (candidate_ind != null &&
candidate_ind.trust_level != TrustLevel.NONE &&
- !candidate_ind_set.contains (candidate_ind))
+ !candidate_inds.contains (candidate_ind))
{
debug (" Found candidate individual '%s' by " +
"linkable property '%s' = '%s'.",
candidate_ind.id, prop_name, prop_linking_value);
- candidate_inds.prepend (candidate_ind);
- candidate_ind_set.add (candidate_ind);
+ candidate_inds.add (candidate_ind);
}
});
}
}
- /* Ensure the original persona makes it into the final persona */
- final_personas.prepend (persona);
+ /* Ensure the original persona makes it into the final individual */
+ final_personas.add (persona);
- if (candidate_inds != null)
+ if (candidate_inds.size > 0 && this._linking_enabled == true)
{
/* The Persona's IID or linkable properties match one or more
* linkable fields which are already in the link map, so we link
* together all the Individuals we found to form a new
* final_individual. Later, we remove the Personas from the old
* Individuals so that the Individuals themselves are removed. */
- candidate_inds.foreach ((i) =>
+ foreach (var individual in candidate_inds)
{
- unowned Individual individual = (Individual) i;
-
- /* FIXME: It would be faster to prepend a reversed copy of
- * `individual.personas`, then reverse the entire
- * `final_personas` list afterwards, but Vala won't let us.
- * We also have to reference each of `individual.personas`
- * manually, since copy() doesn't do that for us. */
- individual.personas.foreach ((p) =>
- {
- ((Persona) p).ref ();
- });
-
- final_personas.concat (individual.personas.copy ());
- });
+ final_personas.add_all (individual.personas);
+ }
+ }
+ else if (candidate_inds.size > 0)
+ {
+ debug (" Linking disabled.");
}
else
{
/* Create the final linked Individual */
final_individual = new Individual (final_personas);
-
debug (" Created new individual '%s' with personas:",
final_individual.id);
- final_personas.foreach ((i) =>
+ foreach (var p in final_personas)
{
- unowned Persona final_persona = (Persona) i;
+ var final_persona = (Persona) p;
- debug (" %s (%s)", final_persona.uid, final_persona.iid);
+ debug (" %s (is user: %s, IID: %s)", final_persona.uid,
+ final_persona.is_user ? "yes" : "no", final_persona.iid);
/* Add the Persona to the link map. Its trust level will be
* reflected in final_individual.trust_level, so other Personas
* won't be linked against it in error if the trust level is
* NONE. */
- this.link_map.replace (final_persona.iid, final_individual);
+ this._link_map.replace (final_persona.iid, final_individual);
/* Only allow linking on non-IID properties of the Persona if we
* fully trust the PersonaStore it came from. */
foreach (unowned string prop_name in
final_persona.linkable_properties)
{
+ /* FIXME: can't be var because of bgo#638208 */
unowned ObjectClass pclass = final_persona.get_class ();
if (pclass.find_property (prop_name) == null)
{
- warning ("Unknown property '%s' in linkable " +
- "property list.", prop_name);
+ warning (
+ /* Translators: the parameter is a property
+ * name. */
+ _("Unknown property '%s' in linkable property list."),
+ prop_name);
continue;
}
final_persona.linkable_property_to_links (prop_name,
(l) =>
{
- string prop_linking_value = (string) l;
+ unowned string prop_linking_value = l;
debug (" %s", prop_linking_value);
- this.link_map.replace (prop_linking_value,
+ this._link_map.replace (prop_linking_value,
final_individual);
});
}
}
- });
+ }
/* Remove the old Individuals. This has to be done here, as we need
* the final_individual. */
- candidate_inds.foreach ((i) =>
+ foreach (var i in candidate_inds)
{
- ((Individual) i).replace (final_individual);
- });
+ /* If the replaced individual was marked to be added to the
+ * aggregator, unmark it. */
+ if (almost_added_individuals.contains (i) == true)
+ almost_added_individuals.remove (i);
+ else
+ replaced_individuals.set (i, final_individual);
+ }
+ /* If the final Individual is the user, set them as such. */
+ if (final_individual.is_user == true)
+ user = final_individual;
+
+ /* Mark the final individual for addition later */
+ almost_added_individuals.add (final_individual);
+ }
+
+ /* Add the set of final individuals which weren't later replaced to the
+ * aggregator. */
+ foreach (var i in almost_added_individuals)
+ {
/* Add the new Individual to the aggregator */
- final_individual.removed.connect (this.individual_removed_cb);
- added_individuals.prepend (final_individual);
- this.individuals.insert (final_individual.id, final_individual);
- });
+ added_individuals.add (i);
+ this._connect_to_individual (i);
+ }
+ }
+
+ private void _remove_persona_from_link_map (Persona persona)
+ {
+ this._link_map.remove (persona.iid);
+
+ if (persona.store.trust_level == PersonaStoreTrust.FULL)
+ {
+ debug (" Removing links to %s:", persona.uid);
+
+ /* Remove maps from the Persona's linkable properties to
+ * Individuals. Add the Individuals to a list of Individuals to be
+ * removed. */
+ foreach (unowned string prop_name in persona.linkable_properties)
+ {
+ /* FIXME: can't be var because of bgo#638208 */
+ unowned ObjectClass pclass = persona.get_class ();
+ if (pclass.find_property (prop_name) == null)
+ {
+ warning (
+ /* Translators: the parameter is a property name. */
+ _("Unknown property '%s' in linkable property list."),
+ prop_name);
+ continue;
+ }
- /* FIXME: AAAARGH VALA GO AWAY */
- foreach (Individual i in added_individuals)
- i.ref ();
- return added_individuals.copy ();
+ persona.linkable_property_to_links (prop_name, (linking_value) =>
+ {
+ debug (" %s", linking_value);
+ this._link_map.remove (linking_value);
+ });
+ }
+ }
}
- private void personas_changed_cb (PersonaStore store,
- GLib.List<Persona>? added,
- GLib.List<Persona>? removed,
+ private void _personas_changed_cb (PersonaStore store,
+ Set<Persona> added,
+ Set<Persona> removed,
string? message,
Persona? actor,
- Groups.ChangeReason reason)
+ GroupDetails.ChangeReason reason)
{
- GLib.List<Individual> added_individuals = null,
- removed_individuals = null;
- GLib.List<Persona> relinked_personas = null;
- HashSet<Persona> removed_personas = new HashSet<Persona> (direct_hash,
- direct_equal);
-
- if (added != null)
- added_individuals = this.add_personas (added);
+ var added_individuals = new HashSet<Individual> ();
+ var removed_individuals = new HashSet<Individual> ();
+ var replaced_individuals = new HashMap<Individual, Individual> ();
+ var relinked_personas = new HashSet<Persona> ();
+ var removed_personas = new HashSet<Persona> (direct_hash, direct_equal);
+
+ /* We store the value of this.user locally and only update it at the end
+ * of the function to prevent spamming notifications of changes to the
+ * property. */
+ var user = this.user;
+
+ if (added.size > 0)
+ {
+ this._add_personas (added, ref added_individuals,
+ ref replaced_individuals, ref user);
+ }
debug ("Removing Personas:");
- removed.foreach ((p) =>
+ foreach (var persona in removed)
{
- unowned Persona persona = (Persona) p;
- PersonaStoreTrust trust_level = persona.store.trust_level;
-
- debug (" %s (%s)", persona.uid, persona.iid);
+ debug (" %s (is user: %s, IID: %s)", persona.uid,
+ persona.is_user ? "yes" : "no", persona.iid);
/* Build a hash table of the removed Personas so that we can quickly
* eliminate them from the list of Personas to relink, below. */
removed_personas.add (persona);
- /* Find the Individual containing the Persona and mark them for
- * removal (any other Personas they have which aren't being removed
- * will be re-linked into other Individuals). */
- Individual ind = this.link_map.lookup (persona.iid);
- removed_individuals.prepend (ind);
- this.link_map.remove (persona.iid);
+ /* Find the Individual containing the Persona (if any) and mark them
+ * for removal (any other Personas they have which aren't being
+ * removed will be re-linked into other Individuals). */
+ var ind = this._link_map.lookup (persona.iid);
+ if (ind != null)
+ removed_individuals.add (ind);
- if (trust_level == PersonaStoreTrust.FULL)
- {
- debug (" Removing links:");
-
- /* Remove maps from the Persona's linkable properties to
- * Individuals. Add the Individuals to a list of Individuals to be
- * removed. */
- foreach (string prop_name in persona.linkable_properties)
- {
- unowned ObjectClass pclass = persona.get_class ();
- if (pclass.find_property (prop_name) == null)
- {
- warning ("Unknown property '%s' in linkable property " +
- "list.", prop_name);
- continue;
- }
-
- persona.linkable_property_to_links (prop_name, (l) =>
- {
- string prop_linking_value = (string) l;
-
- debug (" %s", prop_linking_value);
- this.link_map.remove (prop_linking_value);
- });
- }
- }
- });
+ /* Remove the Persona's links from the link map */
+ this._remove_persona_from_link_map (persona);
+ }
/* Remove the Individuals which were pointed to by the linkable properties
* of the removed Personas. We can then re-link the other Personas in
* those Individuals, since their links may have changed.
* Note that we remove the Individual from this.individuals, meaning that
- * individual_removed_cb() ignores this Individual. This allows us to
+ * _individual_removed_cb() ignores this Individual. This allows us to
* group together the IndividualAggregator.individuals_changed signals
* for all the removed Individuals. */
debug ("Removing Individuals due to removed links:");
- foreach (Individual individual in removed_individuals)
+ foreach (var individual in removed_individuals)
{
/* Ensure we don't remove the same Individual twice */
- if (this.individuals.lookup (individual.id) == null)
+ if (this._individuals.has_key (individual.id) == false)
continue;
debug (" %s", individual.id);
/* Build a list of Personas which need relinking. Ensure we don't
* include any of the Personas which have just been removed. */
- foreach (unowned Persona p in individual.personas)
+ foreach (var persona in individual.personas)
{
- if (removed_personas.contains (p) == false)
- relinked_personas.prepend (p);
+ if (removed_personas.contains (persona) == true ||
+ relinked_personas.contains (persona) == true)
+ continue;
+
+ relinked_personas.add (persona);
+
+ /* Remove links to the Persona */
+ this._remove_persona_from_link_map (persona);
}
- this.individuals.remove (individual.id);
+ if (user == individual)
+ user = null;
+
+ this._disconnect_from_individual (individual);
individual.personas = null;
}
debug ("Relinking Personas:");
- foreach (unowned Persona persona in relinked_personas)
- debug (" %s (%s)", persona.uid, persona.iid);
+ foreach (var persona in relinked_personas)
+ {
+ debug (" %s (is user: %s, IID: %s)", persona.uid,
+ persona.is_user ? "yes" : "no", persona.iid);
+ }
+
+ this._add_personas (relinked_personas, ref added_individuals,
+ ref replaced_individuals, ref user);
+
+ /* Signal the removal of the replaced_individuals at the same time as the
+ * removed_individuals. (The only difference between replaced individuals
+ * and removed ones is that replaced individuals specify a replacement
+ * when they emit their Individual:removed signal. */
+ if (replaced_individuals != null)
+ {
+ MapIterator<Individual, Individual> iter =
+ replaced_individuals.map_iterator ();
+ while (iter.next () == true)
+ removed_individuals.add (iter.get_key ());
+ }
- /* FIXME: Vala is horrible with GLists */
- added_individuals.concat (this.add_personas (relinked_personas));
+ /* Notify of changes to this.user */
+ this.user = user;
/* Signal the addition of new individuals and removal of old ones to the
* aggregator */
- if (added_individuals != null || removed_individuals != null)
+ if (added_individuals.size > 0 || removed_individuals.size > 0)
+ {
+ this._emit_individuals_changed (added_individuals,
+ removed_individuals);
+ }
+
+ /* Signal the replacement of various Individuals as a consequence of
+ * linking. */
+ debug ("Replacing Individuals due to linking:");
+ var iter = replaced_individuals.map_iterator ();
+ while (iter.next () == true)
{
- this.individuals_changed (added_individuals, removed_individuals,
- null, null, 0);
+ iter.get_key ().replace (iter.get_value ());
}
}
- private void is_writeable_changed_cb (Object object, ParamSpec pspec)
+ private void _is_writeable_changed_cb (Object object, ParamSpec pspec)
{
/* Ensure that we only have one writeable PersonaStore */
- unowned PersonaStore store = (PersonaStore) object;
- assert ((store.is_writeable == true && store == this.writeable_store) ||
- (store.is_writeable == false && store != this.writeable_store));
+ var store = (PersonaStore) object;
+ assert ((store.is_writeable == true && store == this._writeable_store) ||
+ (store.is_writeable == false && store != this._writeable_store));
}
- private void trust_level_changed_cb (Object object, ParamSpec pspec)
+ private void _trust_level_changed_cb (Object object, ParamSpec pspec)
{
- /* FIXME: For the moment, assert that only the key-file backend's
- * singleton PersonaStore is trusted. */
- unowned PersonaStore store = (PersonaStore) object;
- if (store.type_id == "key-file")
+ /* Only our writeable_store can be fully trusted. */
+ var store = (PersonaStore) object;
+ if (this._writeable_store != null &&
+ store.type_id == this._writeable_store.type_id)
assert (store.trust_level == PersonaStoreTrust.FULL);
else
assert (store.trust_level != PersonaStoreTrust.FULL);
}
- private void individual_removed_cb (Individual i, Individual? replacement)
+ private void _individual_removed_cb (Individual i, Individual? replacement)
{
+ if (this.user == i)
+ this.user = null;
+
/* Only signal if the individual is still in this.individuals. This allows
- * us to group removals together in, e.g., personas_changed_cb(). */
- if (this.individuals.lookup (i.id) == null)
+ * us to group removals together in, e.g., _personas_changed_cb(). */
+ if (this._individuals.get (i.id) != i)
return;
- var i_list = new GLib.List<Individual> ();
- i_list.append (i);
+ var individuals = new HashSet<Individual> ();
+ individuals.add (i);
if (replacement != null)
{
debug ("Individual '%s' removed (not replaced)", i.id);
}
- this.individuals_changed (null, i_list, null, null, 0);
- this.individuals.remove (i.id);
+ /* If the individual has 0 personas, we've already signaled removal */
+ if (i.personas.size > 0)
+ {
+ this._emit_individuals_changed (null, individuals);
+ }
+
+ this._disconnect_from_individual (i);
}
/**
* Add a new persona in the given {@link PersonaStore} based on the `details`
* provided.
*
+ * If the target store is offline, this function will throw
+ * {@link IndividualAggregatorError.STORE_OFFLINE}. It's the responsibility of
+ * the caller to cache details and re-try this function if it wishes to make
+ * offline adds work.
+ *
* The details hash is a backend-specific mapping of key, value strings.
* Common keys include:
*
* * contact - service-specific contact ID
+ * * message - a user-readable message to pass to the persona being added
*
- * If `parent` is provided, the new persona will be appended to its ordered
- * list of personas.
+ * If a {@link Persona} with the given details already exists in the store, no
+ * error will be thrown and this function will return `null`.
*
* @param parent an optional {@link Individual} to add the new {@link Persona}
- * to
- * @param persona_store_type the {@link PersonaStore.type_id} of the
- * {@link PersonaStore} to use
- * @param persona_store_id the {@link PersonaStore.id} of the
- * {@link PersonaStore} to use
+ * to. This persona will be appended to its ordered list of personas.
+ * @param persona_store the {@link PersonaStore} to add the persona to
* @param details a key-value map of details to use in creating the new
* {@link Persona}
+ * @return the new {@link Persona} or `null` if the corresponding
+ * {@link Persona} already existed. If non-`null`, the new {@link Persona}
+ * will also be added to a new or existing {@link Individual} as necessary.
+ *
+ * @since 0.3.5
*/
public async Persona? add_persona_from_details (Individual? parent,
- string persona_store_type,
- string persona_store_id,
+ PersonaStore persona_store,
HashTable<string, Value?> details) throws IndividualAggregatorError
{
- var full_id = this.get_store_full_id (persona_store_type,
- persona_store_id);
- var store = this.stores[full_id];
-
- if (store == null)
- {
- throw new IndividualAggregatorError.STORE_NOT_FOUND (
- "no store known for type ID '%s' and ID '%s'", store.type_id,
- store.id);
- }
-
Persona persona = null;
try
{
- var details_copy = asv_copy (details);
- persona = yield store.add_persona_from_details (details_copy);
+ var details_copy = this._asv_copy (details);
+ persona = yield persona_store.add_persona_from_details (details_copy);
}
catch (PersonaStoreError e)
{
- throw new IndividualAggregatorError.ADD_FAILED (
- "failed to add contact for store type '%s', ID '%s': %s",
- persona_store_type, persona_store_id, e.message);
+ if (e is PersonaStoreError.STORE_OFFLINE)
+ {
+ throw new IndividualAggregatorError.STORE_OFFLINE (e.message);
+ }
+ else
+ {
+ var full_id = this._get_store_full_id (persona_store.type_id,
+ persona_store.id);
+
+ throw new IndividualAggregatorError.ADD_FAILED (
+ /* Translators: the first parameter is a store identifier
+ * and the second parameter is an error message. */
+ _("Failed to add contact for persona store ID '%s': %s"),
+ full_id, e.message);
+ }
}
if (parent != null && persona != null)
{
- var personas = parent.personas.copy ();
-
- personas.append (persona);
- parent.personas = personas;
+ parent.personas.add (persona);
}
return persona;
}
- private HashTable<string, Value?> asv_copy (HashTable<string, Value?> asv)
+ private HashTable<string, Value?> _asv_copy (HashTable<string, Value?> asv)
{
var retval = new HashTable<string, Value?> (str_hash, str_equal);
asv.foreach ((k, v) =>
{
- retval.insert ((string) k, (Value?) v);
+ retval.insert ((string) k, v);
});
return retval;
* backing stores.
*
* @param individual the {@link Individual} to remove
+ * @since 0.1.11
*/
public async void remove_individual (Individual individual) throws GLib.Error
{
- /* We have to iterate manually since using foreach() requires a sync
- * lambda function, meaning we can't yield on the remove_persona() call */
- unowned GLib.List<unowned Persona> i;
- for (i = individual.personas; i != null; i = i.next)
+ /* Removing personas changes the persona set so we need to make a copy
+ * first */
+ var personas = new HashSet<Persona> ();
+ foreach (var p in individual.personas)
+ {
+ personas.add (p);
+ }
+
+ foreach (var persona in personas)
{
- unowned Persona persona = (Persona) i.data;
yield persona.store.remove_persona (persona);
}
}
* This will leave other personas in the same individual alone.
*
* @param persona the {@link Persona} to remove
+ * @since 0.1.11
*/
public async void remove_persona (Persona persona) throws GLib.Error
{
yield persona.store.remove_persona (persona);
}
- /* FIXME: This should be GLib.List<Persona>, but Vala won't allow it */
- public async void link_personas (void *_personas)
- throws GLib.Error
+ /**
+ * Link the given {@link Persona}s together.
+ *
+ * Create links between the given {@link Persona}s so that they form a single
+ * {@link Individual}. The new {@link Individual} will be returned via the
+ * {@link IndividualAggregator.individuals_changed} signal.
+ *
+ * Removal of the {@link Individual}s which the {@link Persona}s were in
+ * before is signalled by {@link IndividualAggregator.individuals_changed} and
+ * {@link Individual.removed}.
+ *
+ * @param personas the {@link Persona}s to be linked
+ * @since 0.5.1
+ */
+ public async void link_personas (Set<Persona> personas)
+ throws IndividualAggregatorError
{
- unowned GLib.List<Persona> personas = (GLib.List<Persona>) _personas;
-
- if (this.writeable_store == null)
+ if (this._writeable_store == null)
{
throw new IndividualAggregatorError.NO_WRITEABLE_STORE (
- "Can't link personas with no writeable store.");
+ _("Can't link personas with no writeable store."));
}
/* Don't bother linking if it's just one Persona */
- if (personas.next == null)
+ if (personas.size <= 1)
return;
+ /* Disallow linking if it's disabled */
+ if (this._linking_enabled == false)
+ {
+ debug ("Can't link Personas: linking disabled.");
+ return;
+ }
+
/* Create a new persona in the writeable store which links together the
* given personas */
- /* FIXME: We hardcode this to use the key-file backend for now */
- assert (this.writeable_store.type_id == "key-file");
-
- /* `protocols_addrs_list` will be passed to the new Kf.Persona, whereas
- * `protocols_addrs_set` is used to ensure we don't get duplicate IM
- * addresses in the ordered set of addresses for each protocol in
- * `protocols_addrs_list`. It's temporary. */
- HashTable<string, GenericArray<string>> protocols_addrs_list =
- new HashTable<string, GenericArray<string>> (str_hash, str_equal);
- HashTable<string, HashSet<string>> protocols_addrs_set =
- new HashTable<string, HashSet<string>> (str_hash, str_equal);
+ assert (this._writeable_store.type_id ==
+ this._configured_writeable_store_type_id);
- personas.foreach ((p) =>
- {
- unowned Persona persona = (Persona) p;
+ /* `protocols_addrs_set` will be passed to the new Kf.Persona */
+ var protocols_addrs_set = new HashMultiMap<string, string> ();
+ var web_service_addrs_set = new HashMultiMap<string, string> ();
- if (!(persona is IMable))
- return;
+ /* List of local_ids */
+ var local_ids = new Gee.HashSet<string> ();
- ((IMable) persona).im_addresses.foreach ((k, v) =>
+ foreach (var persona in personas)
+ {
+ if (persona is ImDetails)
{
- unowned string protocol = (string) k;
- unowned GenericArray<string> addresses = (GenericArray<string>) v;
+ ImDetails im_details = (ImDetails) persona;
- GenericArray<string> address_list =
- protocols_addrs_list.lookup (protocol);
- HashSet<string> address_set = protocols_addrs_set.lookup (
- protocol);
-
- if (address_list == null || address_set == null)
+ /* protocols_addrs_set = union (all personas' IM addresses) */
+ foreach (var protocol in im_details.im_addresses.get_keys ())
{
- address_list = new GenericArray<string> ();
- address_set = new HashSet<string> ();
+ var im_addresses = im_details.im_addresses.get (protocol);
- protocols_addrs_list.insert (protocol, address_list);
- protocols_addrs_set.insert (protocol, address_set);
+ foreach (var im_address in im_addresses)
+ {
+ protocols_addrs_set.set (protocol, im_address);
+ }
}
+ }
- addresses.foreach ((a) =>
+ if (persona is WebServiceDetails)
+ {
+ WebServiceDetails ws_details = (WebServiceDetails) persona;
+
+ /* web_service_addrs_set = union (all personas' WS addresses) */
+ foreach (var web_service in
+ ws_details.web_service_addresses.get_keys ())
{
- unowned string address = (string) a;
+ var ws_addresses =
+ ws_details.web_service_addresses.get (web_service);
- /* Only add the IM address to the ordered set if it isn't
- * already a member. */
- if (!address_set.contains (address))
+ foreach (var ws_address in ws_addresses)
{
- address_list.add (address);
- address_set.add (address);
+ web_service_addrs_set.set (web_service, ws_address);
}
- });
- });
- });
+ }
+ }
- Value addresses_value = Value (typeof (HashTable));
- addresses_value.set_boxed (protocols_addrs_list);
+ if (persona is LocalIdDetails)
+ {
+ foreach (var id in ((LocalIdDetails) persona).local_ids)
+ {
+ local_ids.add (id);
+ }
+ }
+ }
- HashTable<string, Value?> details =
- new HashTable<string, Value?> (str_hash, str_equal);
- details.insert ("im-addresses", addresses_value);
+ var details = new HashTable<string, Value?> (str_hash, str_equal);
- yield this.add_persona_from_details (null, this.writeable_store.type_id,
- this.writeable_store.id, details);
+ if (protocols_addrs_set.size > 0)
+ {
+ var im_addresses_value = Value (typeof (MultiMap));
+ im_addresses_value.set_object (protocols_addrs_set);
+ details.insert (PersonaStore.detail_key (PersonaDetail.IM_ADDRESSES),
+ im_addresses_value);
+ }
+
+ if (web_service_addrs_set.size > 0)
+ {
+ var web_service_addresses_value = Value (typeof (MultiMap));
+ web_service_addresses_value.set_object (web_service_addrs_set);
+ details.insert (PersonaStore.detail_key
+ (PersonaDetail.WEB_SERVICE_ADDRESSES),
+ web_service_addresses_value);
+ }
+
+ if (local_ids.size > 0)
+ {
+ var local_ids_value = Value (typeof (Set<string>));
+ local_ids_value.set_object (local_ids);
+ details.insert (
+ Folks.PersonaStore.detail_key (PersonaDetail.LOCAL_IDS),
+ local_ids_value);
+ }
+
+ yield this.add_persona_from_details (null,
+ this._writeable_store, details);
}
+ /**
+ * Unlinks the given {@link Individual} into its constituent {@link Persona}s.
+ *
+ * This completely unlinks the given {@link Individual}, destroying all of
+ * its writeable {@link Persona}s.
+ *
+ * The {@link Individual}'s removal is signalled by
+ * {@link IndividualAggregator.individuals_changed} and
+ * {@link Individual.removed}.
+ *
+ * The {@link Persona}s comprising the {@link Individual} will be re-linked
+ * into one or more new {@link Individual}s, depending on how much linking
+ * data remains (typically only implicit links remain). The addition of these
+ * new {@link Individual}s will be signalled by
+ * {@link IndividualAggregator.individuals_changed}.
+ *
+ * @param individual the {@link Individual} to unlink
+ * @since 0.1.13
+ */
public async void unlink_individual (Individual individual) throws GLib.Error
{
- /* Remove all the Personas from writeable PersonaStores
- * We have to iterate manually since using foreach() requires a sync
- * lambda function, meaning we can't yield on the remove_persona() call */
+ if (this._linking_enabled == false)
+ {
+ debug ("Can't unlink Individual '%s': linking disabled.",
+ individual.id);
+ return;
+ }
+
debug ("Unlinking Individual '%s', deleting Personas:", individual.id);
- /* We have to take a copy of the Persona list before removing the
- * Personas, as personas_changed_cb() (which is called as a result of
- * calling writeable_store.remove_persona()) messes around with Persona
+ /* Remove all the Personas from writeable PersonaStores.
+ *
+ * We have to take a copy of the Persona list before removing the
+ * Personas, as _personas_changed_cb() (which is called as a result of
+ * calling _writeable_store.remove_persona()) messes around with Persona
* lists. */
- GLib.List<Persona> personas = individual.personas.copy ();
- foreach (Persona p in personas)
- p.ref ();
+ var personas = new HashSet<Persona> ();
+ foreach (var p in individual.personas)
+ {
+ personas.add (p);
+ }
- foreach (unowned Persona persona in personas)
+ foreach (var persona in personas)
{
- if (persona.store == this.writeable_store)
+ if (persona.store == this._writeable_store)
{
- debug (" %s (%s)", persona.uid, persona.iid);
- yield this.writeable_store.remove_persona (persona);
+ debug (" %s (is user: %s, IID: %s)", persona.uid,
+ persona.is_user ? "yes" : "no", persona.iid);
+ yield this._writeable_store.remove_persona (persona);
}
}
}