2 * Copyright (C) 2010 Collabora Ltd.
4 * This library is free software: you can redistribute it and/or modify
5 * it under the terms of the GNU Lesser General Public License as published by
6 * the Free Software Foundation, either version 2.1 of the License, or
7 * (at your option) any later version.
9 * This library is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU Lesser General Public License for more details.
14 * You should have received a copy of the GNU Lesser General Public License
15 * along with this library. If not, see <http://www.gnu.org/licenses/>.
18 * Travis Reitter <travis.reitter@collabora.co.uk>
25 * Trust level for an {@link Individual} for use in the UI.
29 public enum Folks.TrustLevel
32 * The {@link Individual}'s {@link Persona}s aren't trusted at all.
34 * This is the trust level for an {@link Individual} which contains one or
35 * more {@link Persona}s which cannot be guaranteed to be the same
36 * {@link Persona}s as were originally linked together.
38 * For example, an {@link Individual} containing a link-local XMPP
39 * {@link Persona} would have this trust level, since someone else could
40 * easily spoof the link-local XMPP {@link Persona}'s identity.
47 * The {@link Individual}'s {@link Persona}s are trusted.
49 * This trust level is for {@link Individual}s where it can be guaranteed
50 * that all the {@link Persona}s are the same ones as when they were
51 * originally linked together.
53 * Note that this doesn't guarantee that the user who behind each
54 * {@link Persona} is who they claim to be.
62 * A physical person, aggregated from the various {@link Persona}s the person
63 * might have, such as their different IM addresses or vCard entries.
65 public class Folks.Individual : Object,
72 private bool _is_favourite;
73 private string _alias;
74 private HashTable<string, bool> _groups;
75 /* These two data structures should store exactly the same set of Personas:
76 * the Personas contained in this Individual. The HashSet is used for fast
77 * lookups, whereas the List is used for iteration.
78 * The Individual's references to its Personas are kept by the HashSet;
79 * since the List contains the same set of Personas, it doesn't need an
80 * extra reference (and due to bgo#624249, this is a good thing). */
81 private GLib.List<unowned Persona> _persona_list;
82 private HashSet<Persona> _persona_set;
83 /* Mapping from PersonaStore -> number of Personas from that store contained
84 * in this Individual. There shouldn't be any entries with a number < 1.
85 * This is used for working out when to disconnect from store signals. */
86 private HashMap<PersonaStore, uint> stores;
89 * The trust level of the Individual.
91 * This specifies how far the Individual can be trusted to be who it claims
92 * to be. See the descriptions for the elements of {@link TrustLevel}.
94 * Clients should ''not'' allow linking of Individuals who have a trust level
95 * of {@link TrustLevel.NONE}.
99 public TrustLevel trust_level { get; private set; }
104 public File avatar { get; private set; }
109 public Folks.PresenceType presence_type { get; private set; }
114 public string presence_message { get; private set; }
117 * A unique identifier for the Individual.
119 * This uniquely identifies the Individual, and persists across
120 * {@link IndividualAggregator} instances.
122 * FIXME: Will this.id actually be the persistent ID for storage?
124 public string id { get; private set; }
127 * Emitted when the last of the Individual's {@link Persona}s has been
130 * At this point, the Individual is invalid, so any client referencing it
131 * should unreference it and remove it from their UI.
133 * @param replacement_individual the individual which has replaced this one
134 * due to linking, or `null` if this individual was removed for another reason
137 public signal void removed (Individual? replacement_individual);
144 get { return this._alias; }
148 if (this._alias == value)
153 debug ("Setting alias of individual '%s' to '%s'…", this.id, value);
155 /* First, try to write it to only the writeable Personas… */
156 bool alias_changed = false;
157 this._persona_list.foreach ((p) =>
159 if (p is Aliasable && ((Persona) p).store.is_writeable == true)
161 debug (" written to writeable persona '%s'",
163 ((Aliasable) p).alias = value;
164 alias_changed = true;
168 /* …but if there are no writeable Personas, we have to fall back to
169 * writing it to every Persona. */
170 if (alias_changed == false)
172 this._persona_list.foreach ((p) =>
176 debug (" written to non-writeable persona '%s'",
178 ((Aliasable) p).alias = value;
186 * Whether this Individual is a user-defined favourite.
188 * This property is `true` if any of this Individual's {@link Persona}s are
191 public bool is_favourite
193 get { return this._is_favourite; }
197 if (this._is_favourite == value)
200 debug ("Setting '%s' favourite status to %s", this.id,
201 value ? "TRUE" : "FALSE");
203 this._is_favourite = value;
204 this._persona_list.foreach ((p) =>
208 SignalHandler.block_by_func (p,
209 (void*) this.notify_is_favourite_cb, this);
210 ((Favourite) p).is_favourite = value;
211 SignalHandler.unblock_by_func (p,
212 (void*) this.notify_is_favourite_cb, this);
221 public HashTable<string, bool> groups
223 get { return this._groups; }
227 this._groups = value;
228 this._persona_list.foreach ((p) =>
230 if (p is Groupable && ((Persona) p).store.is_writeable == true)
231 ((Groupable) p).groups = value;
237 * The set of {@link Persona}s encapsulated by this Individual.
239 * Changing the set of personas may cause updates to the aggregated properties
240 * provided by the Individual, resulting in property notifications for them.
242 * Changing the set of personas will not cause permanent linking/unlinking of
243 * the added/removed personas to/from this Individual. To do that, call
244 * {@link IndividualAggregator.link_personas} or
245 * {@link IndividualAggregator.unlink_individual}, which will ensure the link
246 * changes are written to the appropriate backend.
248 public GLib.List<Persona> personas
250 get { return this._persona_list; }
251 set { this._set_personas (value, null); }
255 * Emitted when one or more {@link Persona}s are added to or removed from
258 * @param added a list of {@link Persona}s which have been added
259 * @param removed a list of {@link Persona}s which have been removed
263 public signal void personas_changed (GLib.List<Persona>? added,
264 GLib.List<Persona>? removed);
266 private void notify_alias_cb (Object obj, ParamSpec ps)
268 this.update_alias ();
271 private void notify_avatar_cb (Object obj, ParamSpec ps)
273 this.update_avatar ();
276 private void persona_group_changed_cb (string group, bool is_member)
278 this.update_groups ();
282 * Add or remove the Individual from the specified group.
284 * If `is_member` is `true`, the Individual will be added to the `group`. If
285 * it is `false`, they will be removed from the `group`.
287 * The group membership change will propagate to every {@link Persona} in
290 * @param group a freeform group identifier
291 * @param is_member whether the Individual should be a member of the group
294 public async void change_group (string group, bool is_member)
296 this._persona_list.foreach ((p) =>
299 ((Groupable) p).change_group.begin (group, is_member);
302 /* don't notify, since it hasn't happened in the persona backing stores
303 * yet; react to that directly */
306 private void notify_presence_cb (Object obj, ParamSpec ps)
308 this.update_presence ();
311 private void notify_is_favourite_cb (Object obj, ParamSpec ps)
313 this.update_is_favourite ();
317 * Create a new Individual.
319 * The Individual can optionally be seeded with the {@link Persona}s in
320 * `personas`. Otherwise, it will have to have personas added using the
321 * {@link Folks.Individual.personas} property after construction.
323 * @param personas a list of {@link Persona}s to initialise the
324 * {@link Individual} with, or `null`
325 * @return a new Individual
327 public Individual (GLib.List<Persona>? personas)
329 this._persona_set = new HashSet<Persona> (null, null);
330 this.stores = new HashMap<PersonaStore, uint> (null, null);
331 this.personas = personas;
334 private void store_removed_cb (PersonaStore store)
336 GLib.List<Persona> removed_personas = null;
337 Iterator<Persona> iter = this._persona_set.iterator ();
340 Persona persona = iter.get ();
342 removed_personas.prepend (persona);
343 this._persona_list.remove (persona);
347 if (removed_personas != null)
348 this.personas_changed (null, removed_personas);
351 this.stores.unset (store);
353 if (this._persona_set.size < 1)
359 this.update_fields ();
362 private void store_personas_changed_cb (PersonaStore store,
363 GLib.List<Persona>? added,
364 GLib.List<Persona>? removed,
367 Groupable.ChangeReason reason)
369 GLib.List<Persona> removed_personas = null;
370 removed.foreach ((data) =>
372 unowned Persona p = (Persona) data;
374 if (this._persona_set.remove (p))
376 removed_personas.prepend (p);
377 this._persona_list.remove (p);
381 if (removed_personas != null)
382 this.personas_changed (null, removed_personas);
384 if (this._persona_set.size < 1)
390 this.update_fields ();
393 private void update_fields ()
395 this.update_groups ();
396 this.update_presence ();
397 this.update_is_favourite ();
398 this.update_avatar ();
399 this.update_alias ();
400 this.update_trust_level ();
403 private void update_groups ()
405 var new_groups = new HashTable<string, bool> (str_hash, str_equal);
407 /* this._groups is null during initial construction */
408 if (this._groups == null)
409 this._groups = new HashTable<string, bool> (str_hash, str_equal);
411 /* FIXME: this should partition the personas by store (maybe we should
412 * keep that mapping in general in this class), and execute
413 * "groups-changed" on the store (with the set of personas), to allow the
414 * back-end to optimize it (like Telepathy will for MembersChanged for the
415 * groups channel list) */
416 this._persona_list.foreach ((p) =>
420 unowned Groupable persona = (Groupable) p;
422 persona.groups.foreach ((k, v) =>
424 new_groups.insert ((string) k, true);
429 new_groups.foreach ((k, v) =>
431 var group = (string) k;
432 if (this._groups.lookup (group) != true)
434 this._groups.insert (group, true);
435 this._groups.foreach ((k, v) =>
441 this.group_changed (group, true);
445 /* buffer the removals, so we don't remove while iterating */
446 var removes = new GLib.List<string> ();
447 this._groups.foreach ((k, v) =>
449 var group = (string) k;
450 if (new_groups.lookup (group) != true)
451 removes.prepend (group);
454 removes.foreach ((l) =>
456 var group = (string) l;
457 this._groups.remove (group);
458 this.group_changed (group, false);
462 private void update_presence ()
464 var presence_message = "";
465 var presence_type = Folks.PresenceType.UNSET;
467 /* Choose the most available presence from our personas */
468 this._persona_list.foreach ((p) =>
472 unowned Presence presence = (Presence) p;
474 if (Presence.typecmp (presence.presence_type, presence_type) > 0)
476 presence_type = presence.presence_type;
477 presence_message = presence.presence_message;
482 if (presence_message == null)
483 presence_message = "";
485 /* only notify if the value has changed */
486 if (this.presence_message != presence_message)
487 this.presence_message = presence_message;
489 if (this.presence_type != presence_type)
490 this.presence_type = presence_type;
493 private void update_is_favourite ()
495 bool favourite = false;
497 debug ("Running update_is_favourite() on '%s'", this.id);
499 this._persona_list.foreach ((p) =>
501 if (favourite == false && p is Favourite)
503 favourite = ((Favourite) p).is_favourite;
504 if (favourite == true)
509 /* Only notify if the value has changed. We have to set the private member
510 * and notify manually, or we'd end up propagating the new favourite
511 * status back down to all our Personas. */
512 if (this._is_favourite != favourite)
514 this._is_favourite = favourite;
515 this.notify_property ("is-favourite");
519 private void update_alias ()
522 bool alias_is_display_id = false;
524 debug ("Updating alias for individual '%s'", this.id);
526 /* Search for an alias from a writeable Persona, and use it as our first
527 * choice if it's non-empty, since that's where the user-set alias is
529 foreach (Persona p in this._persona_list)
531 if (p is Aliasable && p.store.is_writeable == true)
533 unowned Aliasable a = (Aliasable) p;
535 if (a.alias != null && a.alias.strip () != "")
543 debug (" got alias '%s' from writeable personas", alias);
545 /* Since we can't find a non-empty alias from a writeable backend, try
546 * the aliases from other personas. Use a non-empty alias which isn't
547 * equal to the persona's display ID as our preference. If we can't find
548 * one of those, fall back to one which is equal to the display ID. */
551 foreach (Persona p in this._persona_list)
555 unowned Aliasable a = (Aliasable) p;
557 if (a.alias == null || a.alias.strip () == "")
560 if (alias == null || alias_is_display_id == true)
562 /* We prefer to not have an alias which is the same as the
563 * Persona's display-id, since having such an alias
564 * implies that it's the default. However, we prefer using
565 * such an alias to using the Persona's UID, which is our
566 * ultimate fallback (below). */
569 if (a.alias == p.display_id)
570 alias_is_display_id = true;
571 else if (alias != null)
578 debug (" got alias '%s' from non-writeable personas", alias);
582 /* We have to pick a display ID, since none of the personas have an
583 * alias available. Pick the display ID from the first persona in the
585 alias = this._persona_list.data.display_id;
586 debug ("No aliases available for individual; using display ID " +
587 "instead: %s", alias);
590 /* Only notify if the value has changed. We have to set the private member
591 * and notify manually, or we'd end up propagating the new alias back
592 * down to all our Personas, even if it's a fallback display ID or
593 * something else undesirable. */
594 if (this._alias != alias)
596 debug ("Changing alias of individual '%s' from '%s' to '%s'.",
597 this.id, this._alias, alias);
599 this.notify_property ("alias");
603 private void update_avatar ()
607 this._persona_list.foreach ((p) =>
609 if (avatar == null && p is Avatar)
611 avatar = ((Avatar) p).avatar;
616 /* only notify if the value has changed */
617 if (this.avatar != avatar)
618 this.avatar = avatar;
621 private void update_trust_level ()
623 TrustLevel trust_level = TrustLevel.PERSONAS;
625 foreach (Persona p in this._persona_list)
627 if (p.store.trust_level == PersonaStoreTrust.NONE)
628 trust_level = TrustLevel.NONE;
631 /* Only notify if the value has changed */
632 if (this.trust_level != trust_level)
633 this.trust_level = trust_level;
637 * GLib/C convenience functions (for built-in casting, etc.)
641 * Get the Individual's alias.
643 * The alias is a user-chosen name for the Individual; how the user wants that
644 * Individual to be represented in UIs.
646 * @return the Individual's alias
648 public unowned string get_alias ()
654 * Get a mapping of group ID to whether the Individual is a member.
656 * Freeform group IDs are mapped to a boolean which is `true` if the
657 * Individual is a member of the group, and `false` otherwise.
659 * @return a mapping of group ID to membership status
661 public HashTable<string, bool> get_groups ()
668 * Get the Individual's current presence message.
670 * The presence message returned is from the same {@link Persona} which
671 * provided the presence type returned by
672 * {@link Individual.get_presence_type}.
674 * If none of the {@link Persona}s in the Individual have a presence message
675 * set, an empty string is returned.
677 * @return the Individual's presence message
679 public unowned string get_presence_message ()
681 return this.presence_message;
685 * Get the Individual's current presence type.
687 * The presence type returned is from the same {@link Persona} which provided
688 * the presence message returned by {@link Individual.get_presence_message}.
690 * If none of the {@link Persona}s in the Individual have a presence type set,
691 * {@link PresenceType.UNSET} is returned.
693 * @return the Individual's presence type
695 public Folks.PresenceType get_presence_type ()
697 return this.presence_type;
701 * Whether the Individual is online.
703 * This will be `true` if any of the Individual's {@link Persona}s have a
704 * presence type higher than {@link PresenceType.OFFLINE}, as determined by
705 * {@link Presence.typecmp}.
707 * @return `true` if the Individual is online, `false` otherwise
709 public bool is_online ()
712 return p.is_online ();
715 private void connect_to_persona (Persona persona)
717 persona.notify["alias"].connect (this.notify_alias_cb);
718 persona.notify["avatar"].connect (this.notify_avatar_cb);
719 persona.notify["presence-message"].connect (this.notify_presence_cb);
720 persona.notify["presence-type"].connect (this.notify_presence_cb);
721 persona.notify["is-favourite"].connect (this.notify_is_favourite_cb);
723 if (persona is Groupable)
725 ((Groupable) persona).group_changed.connect (
726 this.persona_group_changed_cb);
730 private void disconnect_from_persona (Persona persona)
732 persona.notify["alias"].disconnect (this.notify_alias_cb);
733 persona.notify["avatar"].disconnect (this.notify_avatar_cb);
734 persona.notify["presence-message"].disconnect (
735 this.notify_presence_cb);
736 persona.notify["presence-type"].disconnect (this.notify_presence_cb);
737 persona.notify["is-favourite"].disconnect (
738 this.notify_is_favourite_cb);
740 if (persona is Groupable)
742 ((Groupable) persona).group_changed.disconnect (
743 this.persona_group_changed_cb);
747 private void _set_personas (GLib.List<Persona>? persona_list,
748 Individual? replacement_individual)
750 HashSet<Persona> persona_set = new HashSet<Persona> (null, null);
751 GLib.List<Persona> added = null;
752 GLib.List<Persona> removed = null;
754 /* Determine which Personas have been added */
755 foreach (Persona p in persona_list)
757 if (!this._persona_set.contains (p))
761 this._persona_set.add (p);
762 this.connect_to_persona (p);
764 /* Increment the Persona count for this PersonaStore */
765 unowned PersonaStore store = p.store;
766 uint num_from_store = this.stores.get (store);
767 if (num_from_store == 0)
769 this.stores.set (store, num_from_store + 1);
773 this.stores.set (store, 1);
775 store.removed.connect (this.store_removed_cb);
776 store.personas_changed.connect (
777 this.store_personas_changed_cb);
784 /* Determine which Personas have been removed */
785 foreach (Persona p in this._persona_list)
787 if (!persona_set.contains (p))
791 /* Decrement the Persona count for this PersonaStore */
792 unowned PersonaStore store = p.store;
793 uint num_from_store = this.stores.get (store);
794 if (num_from_store > 1)
796 this.stores.set (store, num_from_store - 1);
800 store.removed.disconnect (this.store_removed_cb);
801 store.personas_changed.disconnect (
802 this.store_personas_changed_cb);
804 this.stores.unset (store);
807 this.disconnect_from_persona (p);
808 this._persona_set.remove (p);
812 /* Update the Persona list. We just copy the list given to us to save
813 * repeated insertions/removals and also to ensure we retain the ordering
814 * of the Personas we were given. */
815 this._persona_list = persona_list.copy ();
817 this.personas_changed (added, removed);
819 /* If all the Personas have been removed, remove the Individual */
820 if (this._persona_set.size < 1)
822 this.removed (replacement_individual);
826 /* TODO: Base this upon our ID in permanent storage, once we have that. */
827 if (this.id == null && this._persona_list.data != null)
828 this.id = this._persona_list.data.uid;
830 /* Update our aggregated fields and notify the changes */
831 this.update_fields ();
834 internal void replace (Individual replacement_individual)
836 this._set_personas (null, replacement_individual);