Commit 1441ae9d authored by Philip Withnall's avatar Philip Withnall
Browse files

core: Add core anti-linking support

This adds the core of the anti-linking support, based around a new
AntiLinkable interface. This will be implemented by Persona subclasses which
can store anti-linking information (in the form of a set of Persona UIDs
which the given Persona should never be linked to).

This approach allows anti-linking information to be stored with the personas
(presumably in the primary persona store) and thus it should be network
transparent. i.e. Using folks on two different computers with a Google
Contacts address book as primary should cause the anti-linking data to be
shared.

This also includes the necessary IndividualAggregator changes.

Sadly, no unit tests are included.

Closes: https://bugzilla.gnome.org/show_bug.cgi?id=629537
parent 2f442b49
...@@ -6,9 +6,10 @@ Dependencies: ...@@ -6,9 +6,10 @@ Dependencies:
Bugs fixed: Bugs fixed:
• Bug 673918 — Port to newer libgee • Bug 673918 — Port to newer libgee
• Bug 629537 — Support anti-linking
API changes: 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 Overview of changes from libfolks 0.7.1 to libfolks 0.7.2
========================================================= =========================================================
......
...@@ -90,6 +90,7 @@ libfolks_la_SOURCES = \ ...@@ -90,6 +90,7 @@ libfolks_la_SOURCES = \
potential-match.vala \ potential-match.vala \
avatar-cache.vala \ avatar-cache.vala \
object-cache.vala \ object-cache.vala \
anti-linkable.vala \
$(NULL) $(NULL)
if ENABLE_EDS if ENABLE_EDS
......
/*
* 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) 2010 Collabora Ltd.
* Copyright (C) 2012 Philip Withnall
* *
* This library is free software: you can redistribute it and/or modify * 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 * it under the terms of the GNU Lesser General Public License as published by
...@@ -16,6 +17,7 @@ ...@@ -16,6 +17,7 @@
* *
* Authors: * Authors:
* Travis Reitter <travis.reitter@collabora.co.uk> * Travis Reitter <travis.reitter@collabora.co.uk>
* Philip Withnall <philip@tecnocode.co.uk>
*/ */
using Gee; using Gee;
...@@ -976,7 +978,8 @@ public class Folks.IndividualAggregator : Object ...@@ -976,7 +978,8 @@ public class Folks.IndividualAggregator : Object
/* If the Persona is the user, we *always* want to link it to the /* If the Persona is the user, we *always* want to link it to the
* existing this.user. */ * 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.", debug (" Found candidate individual '%s' as user.",
((!) user).id); ((!) user).id);
...@@ -994,6 +997,8 @@ public class Folks.IndividualAggregator : Object ...@@ -994,6 +997,8 @@ public class Folks.IndividualAggregator : Object
{ {
if (candidate_ind != null && if (candidate_ind != null &&
((!) candidate_ind).trust_level != TrustLevel.NONE && ((!) candidate_ind).trust_level != TrustLevel.NONE &&
((!) candidate_ind).has_anti_link_with_persona (
persona) == false &&
candidate_inds.add ((!) candidate_ind)) candidate_inds.add ((!) candidate_ind))
{ {
debug (" Found candidate individual '%s' by " + debug (" Found candidate individual '%s' by " +
...@@ -1038,6 +1043,9 @@ public class Folks.IndividualAggregator : Object ...@@ -1038,6 +1043,9 @@ public class Folks.IndividualAggregator : Object
if (candidate_ind != null && if (candidate_ind != null &&
((!) candidate_ind).trust_level != ((!) candidate_ind).trust_level !=
TrustLevel.NONE && TrustLevel.NONE &&
((!) candidate_ind).
has_anti_link_with_persona (
persona) == false &&
candidate_inds.add ((!) candidate_ind)) candidate_inds.add ((!) candidate_ind))
{ {
debug (" Found candidate individual '%s'" + debug (" Found candidate individual '%s'" +
...@@ -1165,6 +1173,24 @@ public class Folks.IndividualAggregator : Object ...@@ -1165,6 +1173,24 @@ public class Folks.IndividualAggregator : Object
null, null, GroupDetails.ChangeReason.NONE); 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) private void _connect_to_persona (Persona persona)
{ {
foreach (var prop_name in persona.linkable_properties) foreach (var prop_name in persona.linkable_properties)
...@@ -1172,10 +1198,23 @@ public class Folks.IndividualAggregator : Object ...@@ -1172,10 +1198,23 @@ public class Folks.IndividualAggregator : Object
persona.notify[prop_name].connect ( persona.notify[prop_name].connect (
this._persona_linkable_property_changed_cb); 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) 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) foreach (var prop_name in persona.linkable_properties)
{ {
persona.notify[prop_name].disconnect ( persona.notify[prop_name].disconnect (
...@@ -1787,6 +1826,25 @@ public class Folks.IndividualAggregator : Object ...@@ -1787,6 +1826,25 @@ public class Folks.IndividualAggregator : Object
return; 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 /* Create a new persona in the primary store which links together the
* given personas */ * given personas */
assert (((!) this._primary_store).type_id == assert (((!) this._primary_store).type_id ==
...@@ -1921,29 +1979,50 @@ public class Folks.IndividualAggregator : Object ...@@ -1921,29 +1979,50 @@ public class Folks.IndividualAggregator : Object
return; 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 * Perversely, this requires that we ensure the anti-links property is
* Personas, as _personas_changed_cb() (which is called as a result of * writeable on all personas before continuing. Ignore errors from it in
* calling _primary_store.remove_persona()) messes around with Persona * the hope that everything works anyway.
* lists. */ *
var personas = new HashSet<Persona> (); * In the worst case, this will double the number of personas, since if
foreach (var p in individual.personas) * none of the personas have anti-links writeable, each will have to be
{ * linked with a new writeable persona. */
personas.add (p); 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 try
* this._primary_store != null. */ {
if (persona.store == this._primary_store) 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, debug (" Failed to ensure anti-links property is writeable " +
persona.is_user ? "yes" : "no", persona.iid); "(continuing anyway): %s", e1.message);
yield ((!) this._primary_store).remove_persona (persona);
} }
} }
} }
......
...@@ -2213,4 +2213,64 @@ public class Folks.Individual : Object, ...@@ -2213,4 +2213,64 @@ public class Folks.Individual : Object,
{ {
this._set_personas (null, replacement_individual); 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;
}
} }
...@@ -294,7 +294,14 @@ public enum Folks.PersonaDetail ...@@ -294,7 +294,14 @@ public enum Folks.PersonaDetail
* *
* @since 0.7.1 * @since 0.7.1
*/ */
LAST_CALL_INTERACTION_DATETIME LAST_CALL_INTERACTION_DATETIME,
/**
* Field for {@link AntiLinkable.anti_links}.
*
* @since UNRELEASED
*/
ANTI_LINKS,
} }
/** /**
...@@ -353,7 +360,8 @@ public abstract class Folks.PersonaStore : Object ...@@ -353,7 +360,8 @@ public abstract class Folks.PersonaStore : Object
"im-interaction-count", "im-interaction-count",
"last-im-interaction-datetime", "last-im-interaction-datetime",
"call-interaction-count", "call-interaction-count",
"last-call-interaction-datetime" "last-call-interaction-datetime",
"anti-links"
}; };
/** /**
......
...@@ -34,7 +34,8 @@ public enum Folks.MatchResult ...@@ -34,7 +34,8 @@ public enum Folks.MatchResult
* *
* This is used in situations where two individuals should never be linked, * 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 * 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 * @since 0.6.8
*/ */
...@@ -128,6 +129,13 @@ public class Folks.PotentialMatch : Object ...@@ -128,6 +129,13 @@ public class Folks.PotentialMatch : Object
return result; 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; result = MatchResult.VERY_LOW;
/* If individuals share gender. */ /* If individuals share gender. */
......
...@@ -252,7 +252,8 @@ private class Folks.Inspect.Utils ...@@ -252,7 +252,8 @@ private class Folks.Inspect.Utils
} }
else if (prop_name == "groups" || else if (prop_name == "groups" ||
prop_name == "local-ids" || 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 (); Set<string> groups = (Set<string>) prop_value.get_object ();
output_string = "{ "; output_string = "{ ";
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment