using Folks;
/**
+ * Trust level for an {@link Individual} for use in the UI.
+ *
+ * @since 0.1.15
+ */
+public enum Folks.TrustLevel
+{
+ /**
+ * The {@link Individual}'s {@link Persona}s aren't trusted at all.
+ *
+ * This is the trust level for an {@link Individual} which contains one or
+ * more {@link Persona}s which cannot be guaranteed to be the same
+ * {@link Persona}s as were originally linked together.
+ *
+ * For example, an {@link Individual} containing a link-local XMPP
+ * {@link Persona} would have this trust level, since someone else could
+ * easily spoof the link-local XMPP {@link Persona}'s identity.
+ *
+ * @since 0.1.15
+ */
+ NONE,
+
+ /**
+ * The {@link Individual}'s {@link Persona}s are trusted.
+ *
+ * This trust level is for {@link Individual}s where it can be guaranteed
+ * that all the {@link Persona}s are the same ones as when they were
+ * originally linked together.
+ *
+ * Note that this doesn't guarantee that the user who behind each
+ * {@link Persona} is who they claim to be.
+ *
+ * @since 0.1.15
+ */
+ PERSONAS
+}
+
+/**
* A physical person, aggregated from the various {@link Persona}s the person
* might have, such as their different IM addresses or vCard entries.
*/
public class Folks.Individual : Object,
Alias,
Avatar,
- Capabilities,
Favourite,
Groups,
Presence
{
- private HashTable<string, bool> _groups;
- private GLib.List<Persona> _personas;
- private HashTable<PersonaStore, HashSet<Persona>> stores;
private bool _is_favourite;
+ private string _alias;
+ private HashTable<string, bool> _groups;
+ /* These two data structures should store exactly the same set of Personas:
+ * the Personas contained in this Individual. The HashSet is used for fast
+ * lookups, whereas the List is used for iteration.
+ * The Individual's references to its Personas are kept by the HashSet;
+ * since the List contains the same set of Personas, it doesn't need an
+ * extra reference (and due to bgo#624249, this is a good thing). */
+ private GLib.List<unowned Persona> _persona_list;
+ private HashSet<Persona> _persona_set;
+ /* Mapping from PersonaStore -> number of Personas from that store contained
+ * in this Individual. There shouldn't be any entries with a number < 1.
+ * This is used for working out when to disconnect from store signals. */
+ private HashMap<PersonaStore, uint> stores;
- /* XXX: should setting this push it down into the Persona (to foward along to
- * the actual store if possible?) */
/**
- * {@inheritDoc}
+ * The trust level of the Individual.
+ *
+ * This specifies how far the Individual can be trusted to be who it claims
+ * to be. See the descriptions for the elements of {@link TrustLevel}.
+ *
+ * Clients should ''not'' allow linking of Individuals who have a trust level
+ * of {@link TrustLevel.NONE}.
+ *
+ * @since 0.1.15
*/
- public string alias { get; set; }
+ public TrustLevel trust_level { get; private set; }
/**
* {@inheritDoc}
/**
* {@inheritDoc}
*/
- public CapabilitiesFlags capabilities { get; private set; }
-
- /**
- * {@inheritDoc}
- */
public Folks.PresenceType presence_type { get; private set; }
/**
*
* At this point, the Individual is invalid, so any client referencing it
* should unreference it and remove it from their UI.
+ *
+ * @param replacement_individual the individual which has replaced this one
+ * due to linking, or `null` if this individual was removed for another reason
*/
- public signal void removed ();
+ public signal void removed (Individual? replacement_individual);
+
+ /**
+ * {@inheritDoc}
+ */
+ public string alias
+ {
+ get { return this._alias; }
+
+ set
+ {
+ if (this._alias == value)
+ return;
+
+ this._alias = value;
+
+ /* First, try to write it to only the writeable Personas… */
+ bool alias_changed = false;
+ this._persona_list.foreach ((p) =>
+ {
+ if (p is Alias && ((Persona) p).store.is_writeable == true)
+ {
+ ((Alias) p).alias = value;
+ alias_changed = true;
+ }
+ });
+
+ /* …but if there are no writeable Personas, we have to fall back to
+ * writing it to every Persona. */
+ if (alias_changed == false)
+ {
+ this._persona_list.foreach ((p) =>
+ {
+ if (p is Alias)
+ ((Alias) p).alias = value;
+ });
+ }
+ }
+ }
/**
* Whether this Individual is a user-defined favourite.
*
* This property is `true` if any of this Individual's {@link Persona}s are
* favourites).
- *
- * When set, the value is propagated to all of this Individual's
- * {@link Persona}s.
*/
public bool is_favourite
{
get { return this._is_favourite; }
- /* Propagate the new favourite status to every Persona, but only if it's
- * changed. */
set
{
if (this._is_favourite == value)
return;
+ debug ("Setting '%s' favourite status to %s", this.id,
+ value ? "TRUE" : "FALSE");
+
this._is_favourite = value;
- this._personas.foreach ((p) =>
+ this._persona_list.foreach ((p) =>
{
if (p is Favourite)
- ((Favourite) p).is_favourite = value;
+ {
+ SignalHandler.block_by_func (p,
+ (void*) this.notify_is_favourite_cb, this);
+ ((Favourite) p).is_favourite = value;
+ SignalHandler.unblock_by_func (p,
+ (void*) this.notify_is_favourite_cb, this);
+ }
});
}
}
{
get { return this._groups; }
- /* Propagate the list of new groups to every Persona in the individual
- * which implements the Groups interface */
set
{
- this._personas.foreach ((p) =>
+ this._groups = value;
+ this._persona_list.foreach ((p) =>
{
- if (p is Groups)
+ if (p is Groups && ((Persona) p).store.is_writeable == true)
((Groups) p).groups = value;
});
}
*
* Changing the set of personas may cause updates to the aggregated properties
* provided by the Individual, resulting in property notifications for them.
+ *
+ * Changing the set of personas will not cause permanent linking/unlinking of
+ * the added/removed personas to/from this Individual. To do that, call
+ * {@link IndividualAggregator.link_personas} or
+ * {@link IndividualAggregator.unlink_individual}, which will ensure the link
+ * changes are written to the appropriate backend.
*/
public GLib.List<Persona> personas
{
- get { return this._personas; }
-
- set
- {
- /* Disconnect from all our previous personas */
- this._personas.foreach ((p) =>
- {
- var persona = (Persona) p;
- var groups = (p is Groups) ? (Groups) p : null;
-
- persona.notify["avatar"].disconnect (this.notify_avatar_cb);
- persona.notify["presence-message"].disconnect (
- this.notify_presence_cb);
- persona.notify["presence-type"].disconnect (
- this.notify_presence_cb);
- persona.notify["is-favourite"].disconnect (
- this.notify_is_favourite_cb);
- groups.group_changed.disconnect (this.persona_group_changed_cb);
- });
-
- this._personas = new GLib.List<Persona> ();
- value.foreach ((l) =>
- {
- this._personas.prepend ((Persona) l);
- });
- this._personas.reverse ();
-
- /* If all the personas have been removed, remove the individual */
- if (this._personas.length () < 1)
- {
- this.removed ();
- return;
- }
-
- /* TODO: base this upon our ID in permanent storage, once we have that
- */
- if (this.id == null && this._personas.data != null)
- this.id = this._personas.data.iid;
+ get { return this._persona_list; }
+ set { this._set_personas (value, null); }
+ }
- /* Connect to all the new personas */
- this._personas.foreach ((p) =>
- {
- var persona = (Persona) p;
- var groups = (p is Groups) ? (Groups) p : null;
-
- persona.notify["avatar"].connect (this.notify_avatar_cb);
- persona.notify["presence-message"].connect (
- this.notify_presence_cb);
- persona.notify["presence-type"].connect (this.notify_presence_cb);
- persona.notify["is-favourite"].connect (
- this.notify_is_favourite_cb);
- groups.group_changed.connect (this.persona_group_changed_cb);
- });
+ /**
+ * Emitted when one or more {@link Persona}s are added to or removed from
+ * the Individual.
+ *
+ * @param added a list of {@link Persona}s which have been added
+ * @param removed a list of {@link Persona}s which have been removed
+ *
+ * @since 0.1.15
+ */
+ public signal void personas_changed (GLib.List<Persona>? added,
+ GLib.List<Persona>? removed);
- /* Update our aggregated fields and notify the changes */
- this.update_fields ();
- }
+ private void notify_alias_cb (Object obj, ParamSpec ps)
+ {
+ this.update_alias ();
}
private void notify_avatar_cb (Object obj, ParamSpec ps)
private void persona_group_changed_cb (string group, bool is_member)
{
- this.change_group (group, is_member);
this.update_groups ();
}
* @param group a freeform group identifier
* @param is_member whether the Individual should be a member of the group
*/
- public void change_group (string group, bool is_member)
+ public async void change_group (string group, bool is_member)
{
- this._personas.foreach ((p) =>
+ this._persona_list.foreach ((p) =>
{
if (p is Groups)
- ((Groups) p).change_group (group, is_member);
+ ((Groups) p).change_group.begin (group, is_member);
});
/* don't notify, since it hasn't happened in the persona backing stores
*/
public Individual (GLib.List<Persona>? personas)
{
- Object (personas: personas);
-
- this.stores = new HashTable<PersonaStore, HashSet<Persona>> (direct_hash,
- direct_equal);
- this.stores_update ();
+ this._persona_set = new HashSet<Persona> (null, null);
+ this.stores = new HashMap<PersonaStore, uint> (null, null);
+ this.personas = personas;
}
- private void stores_update ()
+ private void store_removed_cb (PersonaStore store)
{
- this._personas.foreach ((p) =>
+ GLib.List<Persona> removed_personas = null;
+ Iterator<Persona> iter = this._persona_set.iterator ();
+ while (iter.next ())
{
- var persona = (Persona) p;
- var store_is_new = false;
- var persona_set = this.stores.lookup (persona.store);
- if (persona_set == null)
- {
- persona_set = new HashSet<Persona> (direct_hash, direct_equal);
- store_is_new = true;
- }
-
- persona_set.add (persona);
+ Persona persona = iter.get ();
- if (store_is_new)
- {
- this.stores.insert (persona.store, persona_set);
+ removed_personas.prepend (persona);
+ this._persona_list.remove (persona);
+ iter.remove ();
+ }
- persona.store.removed.connect (this.store_removed_cb);
- persona.store.personas_removed.connect (
- this.store_personas_removed_cb);
- }
- });
- }
+ if (removed_personas != null)
+ this.personas_changed (null, removed_personas);
- private void store_removed_cb (PersonaStore store)
- {
- var persona_set = this.stores.lookup (store);
- if (persona_set != null)
- {
- foreach (var persona in persona_set)
- {
- this._personas.remove (persona);
- }
- }
if (store != null)
- this.stores.remove (store);
+ this.stores.unset (store);
- if (this._personas.length () < 1 || this.stores.size () < 1)
+ if (this._persona_set.size < 1)
{
- this.removed ();
+ this.removed (null);
return;
}
this.update_fields ();
}
- private void store_personas_removed_cb (PersonaStore store,
- GLib.List<Persona> personas)
+ private void store_personas_changed_cb (PersonaStore store,
+ GLib.List<Persona>? added,
+ GLib.List<Persona>? removed,
+ string? message,
+ Persona? actor,
+ Groups.ChangeReason reason)
{
- var persona_set = this.stores.lookup (store);
- personas.foreach ((data) =>
+ GLib.List<Persona> removed_personas = null;
+ removed.foreach ((data) =>
{
- var p = (Persona) data;
+ unowned Persona p = (Persona) data;
- persona_set.remove (p);
- this._personas.remove (p);
+ if (this._persona_set.remove (p))
+ {
+ removed_personas.prepend (p);
+ this._persona_list.remove (p);
+ }
});
- if (this._personas.length () < 1)
+ if (removed_personas != null)
+ this.personas_changed (null, removed_personas);
+
+ if (this._persona_set.size < 1)
{
- this.removed ();
+ this.removed (null);
return;
}
private void update_fields ()
{
- /* Gather the first occurrence of each field. We assume that there is
- * at least one persona in the list, since the Individual should've been
- * destroyed before now otherwise. */
- string alias = null;
- var caps = CapabilitiesFlags.NONE;
- this._personas.foreach ((p) =>
- {
- if (p is Alias)
- {
- var a = (Alias) p;
-
- if (alias == null || alias.strip () == "")
- alias = a.alias;
- }
-
- if (p is Capabilities)
- caps |= ((Capabilities) p).capabilities;
- });
-
- if (alias == null)
- {
- /* We have to pick a UID, since none of the personas have an alias
- * available. Pick the UID from the first persona in the list. */
- alias = this._personas.data.uid;
- warning ("No aliases available for individual; using UID instead: %s",
- alias);
- }
-
- /* only notify if the value has changed */
- if (this.alias != alias)
- this.alias = alias;
-
- if (this.capabilities != caps)
- this.capabilities = caps;
-
this.update_groups ();
this.update_presence ();
this.update_is_favourite ();
this.update_avatar ();
+ this.update_alias ();
+ this.update_trust_level ();
}
private void update_groups ()
* "groups-changed" on the store (with the set of personas), to allow the
* back-end to optimize it (like Telepathy will for MembersChanged for the
* groups channel list) */
- this._personas.foreach ((p) =>
+ this._persona_list.foreach ((p) =>
{
if (p is Groups)
{
- var persona = (Groups) p;
+ unowned Groups persona = (Groups) p;
persona.groups.foreach ((k, v) =>
{
var presence_type = Folks.PresenceType.UNSET;
/* Choose the most available presence from our personas */
- this._personas.foreach ((p) =>
+ this._persona_list.foreach ((p) =>
{
if (p is Presence)
{
- var presence = (Presence) p;
+ unowned Presence presence = (Presence) p;
if (Presence.typecmp (presence.presence_type, presence_type) > 0)
{
{
bool favourite = false;
- this._personas.foreach ((p) =>
+ debug ("Running update_is_favourite() on '%s'", this.id);
+
+ this._persona_list.foreach ((p) =>
{
if (favourite == false && p is Favourite)
{
}
});
- /* Only notify if the value has changed */
+ /* Only notify if the value has changed. We have to set the private member
+ * and notify manually, or we'd end up propagating the new favourite
+ * status back down to all our Personas. */
if (this._is_favourite != favourite)
- this._is_favourite = favourite;
+ {
+ this._is_favourite = favourite;
+ this.notify_property ("is-favourite");
+ }
+ }
+
+ private void update_alias ()
+ {
+ string alias = null;
+ bool alias_is_display_id = false;
+
+ /* Search for an alias from a writeable Persona, and use it as our first
+ * choice if it's non-empty, since that's where the user-set alias is
+ * stored. */
+ foreach (Persona p in this._persona_list)
+ {
+ if (p is Alias && p.store.is_writeable == true)
+ {
+ unowned Alias a = (Alias) p;
+
+ if (a.alias != null && a.alias.strip () != "")
+ {
+ alias = a.alias;
+ break;
+ }
+ }
+ }
+
+ /* Since we can't find a non-empty alias from a writeable backend, try
+ * the aliases from other personas. Use a non-empty alias which isn't
+ * equal to the persona's display ID as our preference. If we can't find
+ * one of those, fall back to one which is equal to the display ID. */
+ if (alias == null)
+ {
+ foreach (Persona p in this._persona_list)
+ {
+ if (p is Alias)
+ {
+ unowned Alias a = (Alias) p;
+
+ if (a.alias == null || a.alias.strip () == "")
+ continue;
+
+ if (alias == null || alias_is_display_id == true)
+ {
+ /* We prefer to not have an alias which is the same as the
+ * Persona's display-id, since having such an alias
+ * implies that it's the default. However, we prefer using
+ * such an alias to using the Persona's UID, which is our
+ * ultimate fallback (below). */
+ alias = a.alias;
+
+ if (a.alias == p.display_id)
+ alias_is_display_id = true;
+ else if (alias != null)
+ break;
+ }
+ }
+ }
+ }
+
+ if (alias == null)
+ {
+ /* We have to pick a display ID, since none of the personas have an
+ * alias available. Pick the display ID from the first persona in the
+ * list. */
+ alias = this._persona_list.data.display_id;
+ debug ("No aliases available for individual; using display ID " +
+ "instead: %s", alias);
+ }
+
+ /* Only notify if the value has changed. We have to set the private member
+ * and notify manually, or we'd end up propagating the new alias back
+ * down to all our Personas, even if it's a fallback display ID or
+ * something else undesirable. */
+ if (this._alias != alias)
+ {
+ this._alias = alias;
+ this.notify_property ("alias");
+ }
}
private void update_avatar ()
{
File avatar = null;
- this._personas.foreach ((p) =>
+ this._persona_list.foreach ((p) =>
{
if (avatar == null && p is Avatar)
{
this.avatar = avatar;
}
- /**
- * Get a bitmask of the capabilities of this Individual.
- *
- * The capabilities is the union of the sets of capabilities of all the
- * {@link Persona}s in the Individual.
- *
- * @return bitmask of the Individual's capabilities
- */
- public CapabilitiesFlags get_capabilities ()
+ private void update_trust_level ()
{
- return this.capabilities;
+ TrustLevel trust_level = TrustLevel.PERSONAS;
+
+ foreach (Persona p in this._persona_list)
+ {
+ if (p.store.trust_level == PersonaStoreTrust.NONE)
+ trust_level = TrustLevel.NONE;
+ }
+
+ /* Only notify if the value has changed */
+ if (this.trust_level != trust_level)
+ this.trust_level = trust_level;
}
/*
Presence p = this;
return p.is_online ();
}
+
+ private void connect_to_persona (Persona persona)
+ {
+ persona.notify["alias"].connect (this.notify_alias_cb);
+ persona.notify["avatar"].connect (this.notify_avatar_cb);
+ persona.notify["presence-message"].connect (this.notify_presence_cb);
+ persona.notify["presence-type"].connect (this.notify_presence_cb);
+ persona.notify["is-favourite"].connect (this.notify_is_favourite_cb);
+
+ if (persona is Groups)
+ {
+ ((Groups) persona).group_changed.connect (
+ this.persona_group_changed_cb);
+ }
+ }
+
+ private void disconnect_from_persona (Persona persona)
+ {
+ persona.notify["alias"].disconnect (this.notify_alias_cb);
+ persona.notify["avatar"].disconnect (this.notify_avatar_cb);
+ persona.notify["presence-message"].disconnect (
+ this.notify_presence_cb);
+ persona.notify["presence-type"].disconnect (this.notify_presence_cb);
+ persona.notify["is-favourite"].disconnect (
+ this.notify_is_favourite_cb);
+
+ if (persona is Groups)
+ {
+ ((Groups) persona).group_changed.disconnect (
+ this.persona_group_changed_cb);
+ }
+ }
+
+ private void _set_personas (GLib.List<Persona>? persona_list,
+ Individual? replacement_individual)
+ {
+ HashSet<Persona> persona_set = new HashSet<Persona> (null, null);
+ GLib.List<Persona> added = null;
+ GLib.List<Persona> removed = null;
+
+ /* Determine which Personas have been added */
+ foreach (Persona p in persona_list)
+ {
+ if (!this._persona_set.contains (p))
+ {
+ added.prepend (p);
+
+ this._persona_set.add (p);
+ this.connect_to_persona (p);
+
+ /* Increment the Persona count for this PersonaStore */
+ unowned PersonaStore store = p.store;
+ uint num_from_store = this.stores.get (store);
+ if (num_from_store == 0)
+ {
+ this.stores.set (store, num_from_store + 1);
+ }
+ else
+ {
+ this.stores.set (store, 1);
+
+ store.removed.connect (this.store_removed_cb);
+ store.personas_changed.connect (
+ this.store_personas_changed_cb);
+ }
+ }
+
+ persona_set.add (p);
+ }
+
+ /* Determine which Personas have been removed */
+ foreach (Persona p in this._persona_list)
+ {
+ if (!persona_set.contains (p))
+ {
+ removed.prepend (p);
+
+ /* Decrement the Persona count for this PersonaStore */
+ unowned PersonaStore store = p.store;
+ uint num_from_store = this.stores.get (store);
+ if (num_from_store > 1)
+ {
+ this.stores.set (store, num_from_store - 1);
+ }
+ else
+ {
+ store.removed.disconnect (this.store_removed_cb);
+ store.personas_changed.disconnect (
+ this.store_personas_changed_cb);
+
+ this.stores.unset (store);
+ }
+
+ this.disconnect_from_persona (p);
+ this._persona_set.remove (p);
+ }
+ }
+
+ /* Update the Persona list. We just copy the list given to us to save
+ * repeated insertions/removals and also to ensure we retain the ordering
+ * of the Personas we were given. */
+ this._persona_list = persona_list.copy ();
+
+ this.personas_changed (added, removed);
+
+ /* If all the Personas have been removed, remove the Individual */
+ if (this._persona_set.size < 1)
+ {
+ this.removed (replacement_individual);
+ return;
+ }
+
+ /* TODO: Base this upon our ID in permanent storage, once we have that. */
+ if (this.id == null && this._persona_list.data != null)
+ this.id = this._persona_list.data.uid;
+
+ /* Update our aggregated fields and notify the changes */
+ this.update_fields ();
+ }
+
+ internal void replace (Individual replacement_individual)
+ {
+ this._set_personas (null, replacement_individual);
+ }
}