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>
26 * Trust level for an {@link Individual} for use in the UI.
30 public enum Folks.TrustLevel
33 * The {@link Individual}'s {@link Persona}s aren't trusted at all.
35 * This is the trust level for an {@link Individual} which contains one or
36 * more {@link Persona}s which cannot be guaranteed to be the same
37 * {@link Persona}s as were originally linked together.
39 * For example, an {@link Individual} containing a link-local XMPP
40 * {@link Persona} would have this trust level, since someone else could
41 * easily spoof the link-local XMPP {@link Persona}'s identity.
48 * The {@link Individual}'s {@link Persona}s are trusted.
50 * This trust level is for {@link Individual}s where it can be guaranteed
51 * that all the {@link Persona}s are the same ones as when they were
52 * originally linked together.
54 * Note that this doesn't guarantee that the user who behind each
55 * {@link Persona} is who they claim to be.
63 * A physical person, aggregated from the various {@link Persona}s the person
64 * might have, such as their different IM addresses or vCard entries.
66 public class Folks.Individual : Object,
73 private bool _is_favourite;
74 private string _alias;
75 private HashTable<string, bool> _groups;
76 /* These two data structures should store exactly the same set of Personas:
77 * the Personas contained in this Individual. The HashSet is used for fast
78 * lookups, whereas the List is used for iteration.
79 * The Individual's references to its Personas are kept by the HashSet;
80 * since the List contains the same set of Personas, it doesn't need an
81 * extra reference (and due to bgo#624249, this is a good thing). */
82 private GLib.List<unowned Persona> _persona_list;
83 private HashSet<Persona> _persona_set;
84 /* Mapping from PersonaStore -> number of Personas from that store contained
85 * in this Individual. There shouldn't be any entries with a number < 1.
86 * This is used for working out when to disconnect from store signals. */
87 private HashMap<PersonaStore, uint> stores;
90 * The trust level of the Individual.
92 * This specifies how far the Individual can be trusted to be who it claims
93 * to be. See the descriptions for the elements of {@link TrustLevel}.
95 * Clients should ''not'' allow linking of Individuals who have a trust level
96 * of {@link TrustLevel.NONE}.
100 public TrustLevel trust_level { get; private set; }
105 public File avatar { get; private set; }
110 public Folks.PresenceType presence_type { get; private set; }
115 public string presence_message { get; private set; }
118 * A unique identifier for the Individual.
120 * This uniquely identifies the Individual, and persists across
121 * {@link IndividualAggregator} instances.
123 * FIXME: Will this.id actually be the persistent ID for storage?
125 public string id { get; private set; }
128 * Emitted when the last of the Individual's {@link Persona}s has been
131 * At this point, the Individual is invalid, so any client referencing it
132 * should unreference it and remove it from their UI.
134 * @param replacement_individual the individual which has replaced this one
135 * 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)
152 this._persona_list.foreach ((p) =>
154 if (p is Alias && ((Persona) p).store.is_writeable == true)
155 ((Alias) p).alias = value;
161 * Whether this Individual is a user-defined favourite.
163 * This property is `true` if any of this Individual's {@link Persona}s are
166 public bool is_favourite
168 get { return this._is_favourite; }
172 if (this._is_favourite == value)
175 this._is_favourite = value;
176 this._persona_list.foreach ((p) =>
178 if (p is Favourite && ((Persona) p).store.is_writeable == true)
179 ((Favourite) p).is_favourite = value;
187 public HashTable<string, bool> groups
189 get { return this._groups; }
193 this._persona_list.foreach ((p) =>
195 if (p is Groups && ((Persona) p).store.is_writeable == true)
196 ((Groups) p).groups = value;
202 * The set of {@link Persona}s encapsulated by this Individual.
204 * Changing the set of personas may cause updates to the aggregated properties
205 * provided by the Individual, resulting in property notifications for them.
207 * Changing the set of personas will not cause permanent linking/unlinking of
208 * the added/removed personas to/from this Individual. To do that, call
209 * {@link IndividualAggregator.link_personas} or
210 * {@link IndividualAggregator.unlink_individual}, which will ensure the link
211 * changes are written to the appropriate backend.
213 public GLib.List<Persona> personas
215 get { return this._persona_list; }
216 set { this._set_personas (value, null); }
220 * Emitted when one or more {@link Persona}s are added to or removed from
223 * @param added a list of {@link Persona}s which have been added
224 * @param removed a list of {@link Persona}s which have been removed
228 public signal void personas_changed (GLib.List<Persona>? added,
229 GLib.List<Persona>? removed);
231 private void notify_groups_cb (Object obj, ParamSpec ps)
233 this.update_groups ();
236 private void notify_alias_cb (Object obj, ParamSpec ps)
238 this.update_alias ();
241 private void notify_avatar_cb (Object obj, ParamSpec ps)
243 this.update_avatar ();
246 private void persona_group_changed_cb (string group, bool is_member)
248 this.change_group.begin (group, is_member);
249 this.update_groups ();
253 * Add or remove the Individual from the specified group.
255 * If `is_member` is `true`, the Individual will be added to the `group`. If
256 * it is `false`, they will be removed from the `group`.
258 * The group membership change will propagate to every {@link Persona} in
261 * @param group a freeform group identifier
262 * @param is_member whether the Individual should be a member of the group
264 public async void change_group (string group, bool is_member)
266 this._persona_list.foreach ((p) =>
269 ((Groups) p).change_group.begin (group, is_member);
272 /* don't notify, since it hasn't happened in the persona backing stores
273 * yet; react to that directly */
276 private void notify_presence_cb (Object obj, ParamSpec ps)
278 this.update_presence ();
281 private void notify_is_favourite_cb (Object obj, ParamSpec ps)
283 this.update_is_favourite ();
287 * Create a new Individual.
289 * The Individual can optionally be seeded with the {@link Persona}s in
290 * `personas`. Otherwise, it will have to have personas added using the
291 * {@link Folks.Individual.personas} property after construction.
293 * @return a new Individual
295 public Individual (GLib.List<Persona>? personas)
297 this._persona_set = new HashSet<Persona> (null, null);
298 this.stores = new HashMap<PersonaStore, uint> (null, null);
299 this.personas = personas;
302 private void store_removed_cb (PersonaStore store)
304 GLib.List<Persona> removed_personas = null;
305 Iterator<Persona> iter = this._persona_set.iterator ();
308 Persona persona = iter.get ();
310 removed_personas.prepend (persona);
311 this._persona_list.remove (persona);
315 if (removed_personas != null)
316 this.personas_changed (null, removed_personas);
319 this.stores.remove (store);
321 if (this._persona_set.size < 1)
327 this.update_fields ();
330 private void store_personas_changed_cb (PersonaStore store,
331 GLib.List<Persona>? added,
332 GLib.List<Persona>? removed,
335 Groups.ChangeReason reason)
337 GLib.List<Persona> removed_personas = null;
338 removed.foreach ((data) =>
340 unowned Persona p = (Persona) data;
342 if (this._persona_set.remove (p))
344 removed_personas.prepend (p);
345 this._persona_list.remove (p);
349 if (removed_personas != null)
350 this.personas_changed (null, removed_personas);
352 if (this._persona_set.size < 1)
358 this.update_fields ();
361 private void update_fields ()
363 this.update_groups ();
364 this.update_presence ();
365 this.update_is_favourite ();
366 this.update_avatar ();
367 this.update_alias ();
368 this.update_trust_level ();
371 private void update_groups ()
373 var new_groups = new HashTable<string, bool> (str_hash, str_equal);
375 /* this._groups is null during initial construction */
376 if (this._groups == null)
377 this._groups = new HashTable<string, bool> (str_hash, str_equal);
379 /* FIXME: this should partition the personas by store (maybe we should
380 * keep that mapping in general in this class), and execute
381 * "groups-changed" on the store (with the set of personas), to allow the
382 * back-end to optimize it (like Telepathy will for MembersChanged for the
383 * groups channel list) */
384 this._persona_list.foreach ((p) =>
388 unowned Groups persona = (Groups) p;
390 persona.groups.foreach ((k, v) =>
392 new_groups.insert ((string) k, true);
397 new_groups.foreach ((k, v) =>
399 var group = (string) k;
400 if (this._groups.lookup (group) != true)
402 this._groups.insert (group, true);
403 this._groups.foreach ((k, v) =>
409 this.group_changed (group, true);
413 /* buffer the removals, so we don't remove while iterating */
414 var removes = new GLib.List<string> ();
415 this._groups.foreach ((k, v) =>
417 var group = (string) k;
418 if (new_groups.lookup (group) != true)
419 removes.prepend (group);
422 removes.foreach ((l) =>
424 var group = (string) l;
425 this._groups.remove (group);
426 this.group_changed (group, false);
430 private void update_presence ()
432 var presence_message = "";
433 var presence_type = Folks.PresenceType.UNSET;
435 /* Choose the most available presence from our personas */
436 this._persona_list.foreach ((p) =>
440 unowned Presence presence = (Presence) p;
442 if (Presence.typecmp (presence.presence_type, presence_type) > 0)
444 presence_type = presence.presence_type;
445 presence_message = presence.presence_message;
450 if (presence_message == null)
451 presence_message = "";
453 /* only notify if the value has changed */
454 if (this.presence_message != presence_message)
455 this.presence_message = presence_message;
457 if (this.presence_type != presence_type)
458 this.presence_type = presence_type;
461 private void update_is_favourite ()
463 bool favourite = false;
465 this._persona_list.foreach ((p) =>
467 if (favourite == false && p is Favourite)
469 favourite = ((Favourite) p).is_favourite;
470 if (favourite == true)
475 /* Only notify if the value has changed */
476 if (this.is_favourite != favourite)
477 this.is_favourite = favourite;
480 private void update_alias ()
483 bool alias_is_display_id = false;
485 foreach (Persona p in this._persona_list)
489 unowned Alias a = (Alias) p;
491 if (a.alias == null || a.alias.strip () == "")
494 if (alias == null || alias_is_display_id == true)
496 /* We prefer to not have an alias which is the same as the
497 * Persona's display-id, since having such an alias implies
498 * that it's the default. However, we prefer using such an
499 * alias to using the Persona's UID, which is our ultimate
500 * fallback (below). */
503 if (a.alias == p.display_id)
504 alias_is_display_id = true;
505 else if (alias != null)
513 /* We have to pick a UID, since none of the personas have an alias
514 * available. Pick the UID from the first persona in the list. */
515 alias = this._persona_list.data.uid;
516 debug ("No aliases available for individual; using UID instead: %s",
520 /* only notify if the value has changed */
521 if (this.alias != alias)
525 private void update_avatar ()
529 this._persona_list.foreach ((p) =>
531 if (avatar == null && p is Avatar)
533 avatar = ((Avatar) p).avatar;
538 /* only notify if the value has changed */
539 if (this.avatar != avatar)
540 this.avatar = avatar;
543 private void update_trust_level ()
545 TrustLevel trust_level = TrustLevel.PERSONAS;
547 foreach (Persona p in this._persona_list)
549 if (p.store.trust_level == PersonaStoreTrust.NONE)
550 trust_level = TrustLevel.NONE;
553 /* Only notify if the value has changed */
554 if (this.trust_level != trust_level)
555 this.trust_level = trust_level;
559 * GLib/C convenience functions (for built-in casting, etc.)
563 * Get the Individual's alias.
565 * The alias is a user-chosen name for the Individual; how the user wants that
566 * Individual to be represented in UIs.
568 * @return the Individual's alias
570 public unowned string get_alias ()
576 * Get a mapping of group ID to whether the Individual is a member.
578 * Freeform group IDs are mapped to a boolean which is `true` if the
579 * Individual is a member of the group, and `false` otherwise.
581 * @return a mapping of group ID to membership status
583 public HashTable<string, bool> get_groups ()
590 * Get the Individual's current presence message.
592 * The presence message returned is from the same {@link Persona} which
593 * provided the presence type returned by
594 * {@link Individual.get_presence_type}.
596 * If none of the {@link Persona}s in the Individual have a presence message
597 * set, an empty string is returned.
599 * @return the Individual's presence message
601 public unowned string get_presence_message ()
603 return this.presence_message;
607 * Get the Individual's current presence type.
609 * The presence type returned is from the same {@link Persona} which provided
610 * the presence message returned by {@link Individual.get_presence_message}.
612 * If none of the {@link Persona}s in the Individual have a presence type set,
613 * {@link PresenceType.UNSET} is returned.
615 * @return the Individual's presence type
617 public Folks.PresenceType get_presence_type ()
619 return this.presence_type;
623 * Whether the Individual is online.
625 * This will be `true` if any of the Individual's {@link Persona}s have a
626 * presence type higher than {@link PresenceType.OFFLINE}, as determined by
627 * {@link Presence.typecmp}.
629 * @return `true` if the Individual is online, `false` otherwise
631 public bool is_online ()
634 return p.is_online ();
637 private void connect_to_persona (Persona persona)
639 persona.notify["alias"].connect (this.notify_alias_cb);
640 persona.notify["avatar"].connect (this.notify_avatar_cb);
641 persona.notify["presence-message"].connect (this.notify_presence_cb);
642 persona.notify["presence-type"].connect (this.notify_presence_cb);
643 persona.notify["is-favourite"].connect (this.notify_is_favourite_cb);
644 persona.notify["groups"].connect (this.notify_groups_cb);
646 if (persona is Groups)
648 ((Groups) persona).group_changed.connect (
649 this.persona_group_changed_cb);
653 private void disconnect_from_persona (Persona persona)
655 persona.notify["alias"].disconnect (this.notify_alias_cb);
656 persona.notify["avatar"].disconnect (this.notify_avatar_cb);
657 persona.notify["presence-message"].disconnect (
658 this.notify_presence_cb);
659 persona.notify["presence-type"].disconnect (this.notify_presence_cb);
660 persona.notify["is-favourite"].disconnect (
661 this.notify_is_favourite_cb);
662 persona.notify["groups"].disconnect (this.notify_groups_cb);
664 if (persona is Groups)
666 ((Groups) persona).group_changed.disconnect (
667 this.persona_group_changed_cb);
671 private void _set_personas (GLib.List<Persona>? persona_list,
672 Individual? replacement_individual)
674 HashSet<Persona> persona_set = new HashSet<Persona> (null, null);
675 GLib.List<Persona> added = null;
676 GLib.List<Persona> removed = null;
678 /* Determine which Personas have been added */
679 foreach (Persona p in persona_list)
681 if (!this._persona_set.contains (p))
685 this._persona_set.add (p);
686 this.connect_to_persona (p);
688 /* Increment the Persona count for this PersonaStore */
689 unowned PersonaStore store = p.store;
690 uint num_from_store = this.stores.get (store);
691 if (num_from_store == 0)
693 this.stores.set (store, num_from_store + 1);
697 this.stores.set (store, 1);
699 store.removed.connect (this.store_removed_cb);
700 store.personas_changed.connect (
701 this.store_personas_changed_cb);
708 /* Determine which Personas have been removed */
709 foreach (Persona p in this._persona_list)
711 if (!persona_set.contains (p))
715 /* Decrement the Persona count for this PersonaStore */
716 unowned PersonaStore store = p.store;
717 uint num_from_store = this.stores.get (store);
718 if (num_from_store > 1)
720 this.stores.set (store, num_from_store - 1);
724 store.removed.disconnect (this.store_removed_cb);
725 store.personas_changed.disconnect (
726 this.store_personas_changed_cb);
728 this.stores.unset (store);
731 this.disconnect_from_persona (p);
732 this._persona_set.remove (p);
736 /* Update the Persona list. We just copy the list given to us to save
737 * repeated insertions/removals and also to ensure we retain the ordering
738 * of the Personas we were given. */
739 this._persona_list = persona_list.copy ();
741 this.personas_changed (added, removed);
743 /* If all the Personas have been removed, remove the Individual */
744 if (this._persona_set.size < 1)
746 this.removed (replacement_individual);
750 /* TODO: Base this upon our ID in permanent storage, once we have that. */
751 if (this.id == null && this._persona_list.data != null)
752 this.id = this._persona_list.data.uid;
754 /* Update our aggregated fields and notify the changes */
755 this.update_fields ();
758 internal void replace (Individual replacement_individual)
760 this._set_personas (null, replacement_individual);