Bugs fixed:
• Bug 673918 — Port to newer libgee
+• Bug 629537 — Support anti-linking
API changes:
-
+• Add AntiLinkable interface and implement it on Kf.Persona and Edsf.Persona
Overview of changes from libfolks 0.7.1 to libfolks 0.7.2
=========================================================
potential-match.vala \
avatar-cache.vala \
object-cache.vala \
+ anti-linkable.vala \
$(NULL)
if ENABLE_EDS
--- /dev/null
+/*
+ * Copyright (C) 2011, 2012 Philip Withnall
+ *
+ * This library is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this library. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * Authors:
+ * Philip Withnall <philip@tecnocode.co.uk>
+ */
+
+using Gee;
+using GLib;
+
+/**
+ * Interface for {@link Persona} subclasses from backends which support storage
+ * of, anti-linking data.
+ *
+ * Anti-links are stored as a set of {@link Persona.uid}s with each
+ * {@link Persona} (A), specifying that A must not be linked into an
+ * {@link Individual} with any of the personas in its anti-links set.
+ *
+ * @since UNRELEASED
+ */
+public interface Folks.AntiLinkable : Folks.Persona
+{
+ /**
+ * UIDs of anti-linked {@link Persona}s.
+ *
+ * The {@link Persona}s identified by their UIDs in this set are guaranteed to
+ * not be linked to this {@link Persona}, even if their linkable properties
+ * match.
+ *
+ * No UIDs may be `null`. Well-formed but non-existent UIDs (i.e. UIDs which
+ * can be successfully parsed, but which don't currently correspond to a
+ * {@link Persona} instance) are permitted, as personas may appear and
+ * disappear over time.
+ *
+ * It is expected, but not guaranteed, that anti-links made between personas
+ * will be reciprocal. That is, if persona A lists persona B's UID in its
+ * {@link AntiLinkable.anti_links} set, persona B will typically also list
+ * persona A in its anti-links set.
+ *
+ * @since UNRELEASED
+ */
+ public abstract Set<string> anti_links { get; set; }
+
+ /**
+ * Change the {@link Persona}'s set of anti-links.
+ *
+ * It's preferred to call this rather than setting
+ * {@link AntiLinkable.anti_links} directly, as this method gives error
+ * notification and will only return once the anti-links have been written
+ * to the relevant backing store (or the operation's failed).
+ *
+ * It should be noted that {@link IndividualAggregator.link_personas} and
+ * {@link IndividualAggregator.unlink_individual} will modify the anti-links
+ * sets of the personas they touch, in order to remove and add anti-links,
+ * respectively. It is expected that these {@link IndividualAggregator}
+ * methods will be used to modify anti-links indirectly, rather than calling
+ * {@link AntiLinkable.change_anti_links} directly.
+ *
+ * @param anti_links the new set of anti-links from this persona
+ * @throws PropertyError if setting the anti-links failed
+ * @since UNRELEASED
+ */
+ public virtual async void change_anti_links (Set<string> anti_links)
+ throws PropertyError
+ {
+ /* Default implementation. */
+ throw new PropertyError.NOT_WRITEABLE (
+ _("Anti-links are not writeable on this contact."));
+ }
+
+ /**
+ * Check for an anti-link with another persona.
+ *
+ * This will return `true` if `other_persona`'s UID is listed in this
+ * persona's anti-links set. Note that this check is not symmetric.
+ *
+ * @param other_persona the persona to check is anti-linked
+ * @return `true` if an anti-link exists, `false` otherwise
+ * @since UNRELEASED
+ */
+ public bool has_anti_link_with_persona (Persona other_persona)
+ {
+ return (other_persona.uid in this.anti_links);
+ }
+
+ /**
+ * Add anti-links to other personas.
+ *
+ * The UIDs of all personas in `other_personas` will be added to this
+ * persona's anti-links set and the changes propagated to backends.
+ *
+ * Any attempt to anti-link a persona with itself is not an error, but is
+ * ignored.
+ *
+ * @param other_personas the personas to anti-link to this one
+ * @throws PropertyError if setting the anti-links failed
+ * @since UNRELEASED
+ */
+ public async void add_anti_links (Set<Persona> other_personas)
+ throws PropertyError
+ {
+ var new_anti_links = new HashSet<string> ();
+ new_anti_links.add_all (this.anti_links);
+
+ foreach (var p in other_personas)
+ {
+ /* Don't anti-link ourselves. */
+ if (p == this)
+ {
+ continue;
+ }
+
+ new_anti_links.add (p.uid);
+ }
+
+ yield this.change_anti_links (new_anti_links);
+ }
+
+ /**
+ * Remove anti-links to other personas.
+ *
+ * The UIDs of all personas in `other_personas` will be removed from this
+ * persona's anti-links set and the changes propagated to backends.
+ *
+ * @param other_personas the personas to remove anti-links from this one
+ * @throws PropertyError if setting the anti-links failed
+ * @since UNRELEASED
+ */
+ public async void remove_anti_links (Set<Persona> other_personas)
+ throws PropertyError
+ {
+ var new_anti_links = new HashSet<string> ();
+ new_anti_links.add_all (this.anti_links);
+
+ foreach (var p in other_personas)
+ {
+ new_anti_links.remove (p.uid);
+ }
+
+ yield this.change_anti_links (new_anti_links);
+ }
+}
+
+/* vim: filetype=vala textwidth=80 tabstop=2 expandtab: */
/*
* Copyright (C) 2010 Collabora Ltd.
+ * Copyright (C) 2012 Philip Withnall
*
* This library is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
*
* Authors:
* Travis Reitter <travis.reitter@collabora.co.uk>
+ * Philip Withnall <philip@tecnocode.co.uk>
*/
using Gee;
/* If the Persona is the user, we *always* want to link it to the
* existing this.user. */
- if (persona.is_user == true && user != null)
+ if (persona.is_user == true && user != null &&
+ ((!) user).has_anti_link_with_persona (persona) == false)
{
debug (" Found candidate individual '%s' as user.",
((!) user).id);
{
if (candidate_ind != null &&
((!) candidate_ind).trust_level != TrustLevel.NONE &&
+ ((!) candidate_ind).has_anti_link_with_persona (
+ persona) == false &&
candidate_inds.add ((!) candidate_ind))
{
debug (" Found candidate individual '%s' by " +
if (candidate_ind != null &&
((!) candidate_ind).trust_level !=
TrustLevel.NONE &&
+ ((!) candidate_ind).
+ has_anti_link_with_persona (
+ persona) == false &&
candidate_inds.add ((!) candidate_ind))
{
debug (" Found candidate individual '%s'" +
null, null, GroupDetails.ChangeReason.NONE);
}
+ private void _persona_anti_links_changed_cb (Object obj, ParamSpec pspec)
+ {
+ var persona = obj as Persona;
+
+ /* The anti-links associated with the persona has changed, so that persona
+ * might require re-linking. We do this in a simplistic and hacky way
+ * (which should work) by simply treating the persona as if it's been
+ * removed and re-added. */
+ debug ("Anti-links changed for persona '%s' (is user: %s, IID: %s).",
+ persona.uid, persona.is_user ? "yes" : "no", persona.iid);
+
+ var persona_set = new HashSet<Persona> ();
+ persona_set.add (persona);
+
+ this._personas_changed_cb (persona.store, persona_set, persona_set,
+ null, null, GroupDetails.ChangeReason.NONE);
+ }
+
private void _connect_to_persona (Persona persona)
{
foreach (var prop_name in persona.linkable_properties)
persona.notify[prop_name].connect (
this._persona_linkable_property_changed_cb);
}
+
+ var al = persona as AntiLinkable;
+ if (al != null)
+ {
+ al.notify["anti-links"].connect (this._persona_anti_links_changed_cb);
+ }
}
private void _disconnect_from_persona (Persona persona)
{
+ var al = persona as AntiLinkable;
+ if (al != null)
+ {
+ al.notify["anti-links"].disconnect (
+ this._persona_anti_links_changed_cb);
+ }
+
foreach (var prop_name in persona.linkable_properties)
{
persona.notify[prop_name].disconnect (
return;
}
+ /* Remove all edges in the connected graph between the personas from the
+ * anti-link map to ensure that linking the personas actually succeeds. */
+ foreach (var p in personas)
+ {
+ var al = p as AntiLinkable;
+ if (al != null)
+ {
+ try
+ {
+ yield ((!) al).remove_anti_links (personas);
+ }
+ catch (PropertyError e)
+ {
+ throw new IndividualAggregatorError.PROPERTY_NOT_WRITEABLE (
+ _("Anti-links can't be removed between personas being linked."));
+ }
+ }
+ }
+
/* Create a new persona in the primary store which links together the
* given personas */
assert (((!) this._primary_store).type_id ==
return;
}
- debug ("Unlinking Individual '%s', deleting Personas:", individual.id);
+ debug ("Unlinking Individual '%s':", individual.id);
- /* Remove all the Personas from writeable PersonaStores.
+ /* Add all edges in the connected graph between the personas to the
+ * anti-link map to ensure that unlinking the personas actually succeeds,
+ * and that they aren't immediately re-linked.
*
- * 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 _primary_store.remove_persona()) messes around with Persona
- * lists. */
- var personas = new HashSet<Persona> ();
- foreach (var p in individual.personas)
- {
- personas.add (p);
- }
+ * Perversely, this requires that we ensure the anti-links property is
+ * writeable on all personas before continuing. Ignore errors from it in
+ * the hope that everything works anyway.
+ *
+ * In the worst case, this will double the number of personas, since if
+ * none of the personas have anti-links writeable, each will have to be
+ * linked with a new writeable persona. */
+ var individual_personas = new HashSet<Persona> (); /* as we modify it */
+ individual_personas.add_all (individual.personas);
- foreach (var persona in personas)
+ debug (" Inserting anti-links:");
+ foreach (var pers in individual_personas)
{
- /* Since persona.store != null, we know that
- * this._primary_store != null. */
- if (persona.store == this._primary_store)
+ try
+ {
+ var personas = new HashSet<Persona> ();
+ personas.add (pers);
+ message ("Anti-linking persona '%s' (%p)", pers.uid, pers);
+
+ var writeable_persona =
+ yield this._ensure_personas_property_writeable (personas,
+ "anti-links");
+ message ("Writeable persona '%s' (%p)", writeable_persona.uid, writeable_persona);
+
+ /* Make sure not to anti-link the new persona to pers. */
+ var anti_link_personas = new HashSet<Persona> ();
+ anti_link_personas.add_all (individual_personas);
+ anti_link_personas.remove (pers);
+
+ var al = writeable_persona as AntiLinkable;
+ assert (al != null);
+ yield ((!) al).add_anti_links (anti_link_personas);
+ message ("");
+ }
+ catch (IndividualAggregatorError e1)
{
- debug (" %s (is user: %s, IID: %s)", persona.uid,
- persona.is_user ? "yes" : "no", persona.iid);
- yield ((!) this._primary_store).remove_persona (persona);
+ debug (" Failed to ensure anti-links property is writeable " +
+ "(continuing anyway): %s", e1.message);
}
}
}
{
this._set_personas (null, replacement_individual);
}
+
+ /**
+ * Anti-linked with a persona?
+ *
+ * Check whether this individual is anti-linked to {@link Persona} `p` at all.
+ * If so, `true` will be returned — `false` will be returned otherwise.
+ *
+ * Note that this will check for anti-links in either direction, since
+ * anti-links are not necessarily symmetric.
+ *
+ * @param p persona to check for anti-links with
+ * @return `true` if this individual is anti-linked with persona `p`; `false`
+ * otherwise
+ * @since UNRELEASED
+ */
+ public bool has_anti_link_with_persona (Persona p)
+ {
+ var al = p as AntiLinkable;
+
+ foreach (var persona in this._persona_set)
+ {
+ var pl = persona as AntiLinkable;
+
+ if ((al != null && ((!) al).has_anti_link_with_persona (persona)) ||
+ (pl != null && ((!) pl).has_anti_link_with_persona (p)))
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Anti-linked with an individual?
+ *
+ * Check whether this individual is anti-linked to any of the {@link Persona}s
+ * in {@link Individual} `i`. If so, `true` will be returned — `false` will be
+ * returned otherwise.
+ *
+ * Note that this will check for anti-links in either direction, since
+ * anti-links are not necessarily symmetric.
+ *
+ * @param i individual to check for anti-links with
+ * @return `true` if this individual is anti-linked with individual `i`;
+ * `false` otherwise
+ * @since UNRELEASED
+ */
+ public bool has_anti_link_with_individual (Individual i)
+ {
+ foreach (var p in i.personas)
+ {
+ if (this.has_anti_link_with_persona (p) == true)
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
}
*
* @since 0.7.1
*/
- LAST_CALL_INTERACTION_DATETIME
+ LAST_CALL_INTERACTION_DATETIME,
+
+ /**
+ * Field for {@link AntiLinkable.anti_links}.
+ *
+ * @since UNRELEASED
+ */
+ ANTI_LINKS,
}
/**
"im-interaction-count",
"last-im-interaction-datetime",
"call-interaction-count",
- "last-call-interaction-datetime"
+ "last-call-interaction-datetime",
+ "anti-links"
};
/**
*
* This is used in situations where two individuals should never be linked,
* such as when one of them has a {@link Individual.trust_level} of
- * {@link TrustLevel.NONE}.
+ * {@link TrustLevel.NONE}, or when the individuals are explicitly
+ * anti-linked.
*
* @since 0.6.8
*/
return result;
}
+ /* Similarly, immediately discount a match if the individuals have been
+ * anti-linked by the user. */
+ if (a.has_anti_link_with_individual (b))
+ {
+ return result;
+ }
+
result = MatchResult.VERY_LOW;
/* If individuals share gender. */
}
else if (prop_name == "groups" ||
prop_name == "local-ids" ||
- prop_name == "supported-fields")
+ prop_name == "supported-fields" ||
+ prop_name == "anti-links")
{
Set<string> groups = (Set<string>) prop_value.get_object ();
output_string = "{ ";