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)
153 /* First, try to write it to only the writeable Personas… */
154 bool alias_changed = false;
155 this._persona_list.foreach ((p) =>
157 if (p is Alias && ((Persona) p).store.is_writeable == true)
159 ((Alias) p).alias = value;
160 alias_changed = true;
164 /* …but if there are no writeable Personas, we have to fall back to
165 * writing it to every Persona. */
166 if (alias_changed == false)
168 this._persona_list.foreach ((p) =>
171 ((Alias) p).alias = value;
178 * Whether this Individual is a user-defined favourite.
180 * This property is `true` if any of this Individual's {@link Persona}s are
183 public bool is_favourite
185 get { return this._is_favourite; }
189 if (this._is_favourite == value)
192 debug ("Setting '%s' favourite status to %s", this.id,
193 value ? "TRUE" : "FALSE");
195 this._is_favourite = value;
196 this._persona_list.foreach ((p) =>
200 SignalHandler.block_by_func (p,
201 (void*) this.notify_is_favourite_cb, this);
202 ((Favourite) p).is_favourite = value;
203 SignalHandler.unblock_by_func (p,
204 (void*) this.notify_is_favourite_cb, this);
213 public HashTable<string, bool> groups
215 get { return this._groups; }
219 this._groups = value;
220 this._persona_list.foreach ((p) =>
222 if (p is Groups && ((Persona) p).store.is_writeable == true)
223 ((Groups) p).groups = value;
229 * The set of {@link Persona}s encapsulated by this Individual.
231 * Changing the set of personas may cause updates to the aggregated properties
232 * provided by the Individual, resulting in property notifications for them.
234 * Changing the set of personas will not cause permanent linking/unlinking of
235 * the added/removed personas to/from this Individual. To do that, call
236 * {@link IndividualAggregator.link_personas} or
237 * {@link IndividualAggregator.unlink_individual}, which will ensure the link
238 * changes are written to the appropriate backend.
240 public GLib.List<Persona> personas
242 get { return this._persona_list; }
243 set { this._set_personas (value, null); }
247 * Emitted when one or more {@link Persona}s are added to or removed from
250 * @param added a list of {@link Persona}s which have been added
251 * @param removed a list of {@link Persona}s which have been removed
255 public signal void personas_changed (GLib.List<Persona>? added,
256 GLib.List<Persona>? removed);
258 private void notify_alias_cb (Object obj, ParamSpec ps)
260 this.update_alias ();
263 private void notify_avatar_cb (Object obj, ParamSpec ps)
265 this.update_avatar ();
268 private void persona_group_changed_cb (string group, bool is_member)
270 this.update_groups ();
274 * Add or remove the Individual from the specified group.
276 * If `is_member` is `true`, the Individual will be added to the `group`. If
277 * it is `false`, they will be removed from the `group`.
279 * The group membership change will propagate to every {@link Persona} in
282 * @param group a freeform group identifier
283 * @param is_member whether the Individual should be a member of the group
285 public async void change_group (string group, bool is_member)
287 this._persona_list.foreach ((p) =>
290 ((Groups) p).change_group.begin (group, is_member);
293 /* don't notify, since it hasn't happened in the persona backing stores
294 * yet; react to that directly */
297 private void notify_presence_cb (Object obj, ParamSpec ps)
299 this.update_presence ();
302 private void notify_is_favourite_cb (Object obj, ParamSpec ps)
304 this.update_is_favourite ();
308 * Create a new Individual.
310 * The Individual can optionally be seeded with the {@link Persona}s in
311 * `personas`. Otherwise, it will have to have personas added using the
312 * {@link Folks.Individual.personas} property after construction.
314 * @return a new Individual
316 public Individual (GLib.List<Persona>? personas)
318 this._persona_set = new HashSet<Persona> (null, null);
319 this.stores = new HashMap<PersonaStore, uint> (null, null);
320 this.personas = personas;
323 private void store_removed_cb (PersonaStore store)
325 GLib.List<Persona> removed_personas = null;
326 Iterator<Persona> iter = this._persona_set.iterator ();
329 Persona persona = iter.get ();
331 removed_personas.prepend (persona);
332 this._persona_list.remove (persona);
336 if (removed_personas != null)
337 this.personas_changed (null, removed_personas);
340 this.stores.unset (store);
342 if (this._persona_set.size < 1)
348 this.update_fields ();
351 private void store_personas_changed_cb (PersonaStore store,
352 GLib.List<Persona>? added,
353 GLib.List<Persona>? removed,
356 Groups.ChangeReason reason)
358 GLib.List<Persona> removed_personas = null;
359 removed.foreach ((data) =>
361 unowned Persona p = (Persona) data;
363 if (this._persona_set.remove (p))
365 removed_personas.prepend (p);
366 this._persona_list.remove (p);
370 if (removed_personas != null)
371 this.personas_changed (null, removed_personas);
373 if (this._persona_set.size < 1)
379 this.update_fields ();
382 private void update_fields ()
384 this.update_groups ();
385 this.update_presence ();
386 this.update_is_favourite ();
387 this.update_avatar ();
388 this.update_alias ();
389 this.update_trust_level ();
392 private void update_groups ()
394 var new_groups = new HashTable<string, bool> (str_hash, str_equal);
396 /* this._groups is null during initial construction */
397 if (this._groups == null)
398 this._groups = new HashTable<string, bool> (str_hash, str_equal);
400 /* FIXME: this should partition the personas by store (maybe we should
401 * keep that mapping in general in this class), and execute
402 * "groups-changed" on the store (with the set of personas), to allow the
403 * back-end to optimize it (like Telepathy will for MembersChanged for the
404 * groups channel list) */
405 this._persona_list.foreach ((p) =>
409 unowned Groups persona = (Groups) p;
411 persona.groups.foreach ((k, v) =>
413 new_groups.insert ((string) k, true);
418 new_groups.foreach ((k, v) =>
420 var group = (string) k;
421 if (this._groups.lookup (group) != true)
423 this._groups.insert (group, true);
424 this._groups.foreach ((k, v) =>
430 this.group_changed (group, true);
434 /* buffer the removals, so we don't remove while iterating */
435 var removes = new GLib.List<string> ();
436 this._groups.foreach ((k, v) =>
438 var group = (string) k;
439 if (new_groups.lookup (group) != true)
440 removes.prepend (group);
443 removes.foreach ((l) =>
445 var group = (string) l;
446 this._groups.remove (group);
447 this.group_changed (group, false);
451 private void update_presence ()
453 var presence_message = "";
454 var presence_type = Folks.PresenceType.UNSET;
456 /* Choose the most available presence from our personas */
457 this._persona_list.foreach ((p) =>
461 unowned Presence presence = (Presence) p;
463 if (Presence.typecmp (presence.presence_type, presence_type) > 0)
465 presence_type = presence.presence_type;
466 presence_message = presence.presence_message;
471 if (presence_message == null)
472 presence_message = "";
474 /* only notify if the value has changed */
475 if (this.presence_message != presence_message)
476 this.presence_message = presence_message;
478 if (this.presence_type != presence_type)
479 this.presence_type = presence_type;
482 private void update_is_favourite ()
484 bool favourite = false;
486 debug ("Running update_is_favourite() on '%s'", this.id);
488 this._persona_list.foreach ((p) =>
490 if (favourite == false && p is Favourite)
492 favourite = ((Favourite) p).is_favourite;
493 if (favourite == true)
498 /* Only notify if the value has changed */
499 if (this.is_favourite != favourite)
500 this.is_favourite = favourite;
503 private void update_alias ()
506 bool alias_is_display_id = false;
508 /* Search for an alias from a writeable Persona, and use it as our first
509 * choice if it's non-empty, since that's where the user-set alias is
511 foreach (Persona p in this._persona_list)
513 if (p is Alias && p.store.is_writeable == true)
515 unowned Alias a = (Alias) p;
517 if (a.alias != null && a.alias.strip () != "")
525 /* Since we can't find a non-empty alias from a writeable backend, try
526 * the aliases from other personas. Use a non-empty alias which isn't
527 * equal to the persona's display ID as our preference. If we can't find
528 * one of those, fall back to one which is equal to the display ID. */
531 foreach (Persona p in this._persona_list)
535 unowned Alias a = (Alias) p;
537 if (a.alias == null || a.alias.strip () == "")
540 if (alias == null || alias_is_display_id == true)
542 /* We prefer to not have an alias which is the same as the
543 * Persona's display-id, since having such an alias
544 * implies that it's the default. However, we prefer using
545 * such an alias to using the Persona's UID, which is our
546 * ultimate fallback (below). */
549 if (a.alias == p.display_id)
550 alias_is_display_id = true;
551 else if (alias != null)
560 /* We have to pick a display ID, since none of the personas have an
561 * alias available. Pick the display ID from the first persona in the
563 alias = this._persona_list.data.display_id;
564 debug ("No aliases available for individual; using display ID " +
565 "instead: %s", alias);
568 /* Only notify if the value has changed. We have to set the private member
569 * and notify manually, or we'd end up propagating the new alias back
570 * down to all our Personas, even if it's a fallback display ID or
571 * something else undesirable. */
572 if (this._alias != alias)
575 this.notify_property ("alias");
579 private void update_avatar ()
583 this._persona_list.foreach ((p) =>
585 if (avatar == null && p is Avatar)
587 avatar = ((Avatar) p).avatar;
592 /* only notify if the value has changed */
593 if (this.avatar != avatar)
594 this.avatar = avatar;
597 private void update_trust_level ()
599 TrustLevel trust_level = TrustLevel.PERSONAS;
601 foreach (Persona p in this._persona_list)
603 if (p.store.trust_level == PersonaStoreTrust.NONE)
604 trust_level = TrustLevel.NONE;
607 /* Only notify if the value has changed */
608 if (this.trust_level != trust_level)
609 this.trust_level = trust_level;
613 * GLib/C convenience functions (for built-in casting, etc.)
617 * Get the Individual's alias.
619 * The alias is a user-chosen name for the Individual; how the user wants that
620 * Individual to be represented in UIs.
622 * @return the Individual's alias
624 public unowned string get_alias ()
630 * Get a mapping of group ID to whether the Individual is a member.
632 * Freeform group IDs are mapped to a boolean which is `true` if the
633 * Individual is a member of the group, and `false` otherwise.
635 * @return a mapping of group ID to membership status
637 public HashTable<string, bool> get_groups ()
644 * Get the Individual's current presence message.
646 * The presence message returned is from the same {@link Persona} which
647 * provided the presence type returned by
648 * {@link Individual.get_presence_type}.
650 * If none of the {@link Persona}s in the Individual have a presence message
651 * set, an empty string is returned.
653 * @return the Individual's presence message
655 public unowned string get_presence_message ()
657 return this.presence_message;
661 * Get the Individual's current presence type.
663 * The presence type returned is from the same {@link Persona} which provided
664 * the presence message returned by {@link Individual.get_presence_message}.
666 * If none of the {@link Persona}s in the Individual have a presence type set,
667 * {@link PresenceType.UNSET} is returned.
669 * @return the Individual's presence type
671 public Folks.PresenceType get_presence_type ()
673 return this.presence_type;
677 * Whether the Individual is online.
679 * This will be `true` if any of the Individual's {@link Persona}s have a
680 * presence type higher than {@link PresenceType.OFFLINE}, as determined by
681 * {@link Presence.typecmp}.
683 * @return `true` if the Individual is online, `false` otherwise
685 public bool is_online ()
688 return p.is_online ();
691 private void connect_to_persona (Persona persona)
693 persona.notify["alias"].connect (this.notify_alias_cb);
694 persona.notify["avatar"].connect (this.notify_avatar_cb);
695 persona.notify["presence-message"].connect (this.notify_presence_cb);
696 persona.notify["presence-type"].connect (this.notify_presence_cb);
697 persona.notify["is-favourite"].connect (this.notify_is_favourite_cb);
699 if (persona is Groups)
701 ((Groups) persona).group_changed.connect (
702 this.persona_group_changed_cb);
706 private void disconnect_from_persona (Persona persona)
708 persona.notify["alias"].disconnect (this.notify_alias_cb);
709 persona.notify["avatar"].disconnect (this.notify_avatar_cb);
710 persona.notify["presence-message"].disconnect (
711 this.notify_presence_cb);
712 persona.notify["presence-type"].disconnect (this.notify_presence_cb);
713 persona.notify["is-favourite"].disconnect (
714 this.notify_is_favourite_cb);
716 if (persona is Groups)
718 ((Groups) persona).group_changed.disconnect (
719 this.persona_group_changed_cb);
723 private void _set_personas (GLib.List<Persona>? persona_list,
724 Individual? replacement_individual)
726 HashSet<Persona> persona_set = new HashSet<Persona> (null, null);
727 GLib.List<Persona> added = null;
728 GLib.List<Persona> removed = null;
730 /* Determine which Personas have been added */
731 foreach (Persona p in persona_list)
733 if (!this._persona_set.contains (p))
737 this._persona_set.add (p);
738 this.connect_to_persona (p);
740 /* Increment the Persona count for this PersonaStore */
741 unowned PersonaStore store = p.store;
742 uint num_from_store = this.stores.get (store);
743 if (num_from_store == 0)
745 this.stores.set (store, num_from_store + 1);
749 this.stores.set (store, 1);
751 store.removed.connect (this.store_removed_cb);
752 store.personas_changed.connect (
753 this.store_personas_changed_cb);
760 /* Determine which Personas have been removed */
761 foreach (Persona p in this._persona_list)
763 if (!persona_set.contains (p))
767 /* Decrement the Persona count for this PersonaStore */
768 unowned PersonaStore store = p.store;
769 uint num_from_store = this.stores.get (store);
770 if (num_from_store > 1)
772 this.stores.set (store, num_from_store - 1);
776 store.removed.disconnect (this.store_removed_cb);
777 store.personas_changed.disconnect (
778 this.store_personas_changed_cb);
780 this.stores.unset (store);
783 this.disconnect_from_persona (p);
784 this._persona_set.remove (p);
788 /* Update the Persona list. We just copy the list given to us to save
789 * repeated insertions/removals and also to ensure we retain the ordering
790 * of the Personas we were given. */
791 this._persona_list = persona_list.copy ();
793 this.personas_changed (added, removed);
795 /* If all the Personas have been removed, remove the Individual */
796 if (this._persona_set.size < 1)
798 this.removed (replacement_individual);
802 /* TODO: Base this upon our ID in permanent storage, once we have that. */
803 if (this.id == null && this._persona_list.data != null)
804 this.id = this._persona_list.data.uid;
806 /* Update our aggregated fields and notify the changes */
807 this.update_fields ();
810 internal void replace (Individual replacement_individual)
812 this._set_personas (null, replacement_individual);