3f5b22d36d8e02ea6c669d2f28832c8ffaaacedf
[platform/upstream/folks.git] / folks / individual.vala
1 /*
2  * Copyright (C) 2010 Collabora Ltd.
3  *
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.
8  *
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.
13  *
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/>.
16  *
17  * Authors:
18  *       Travis Reitter <travis.reitter@collabora.co.uk>
19  */
20
21 using Gee;
22 using GLib;
23
24 /**
25  * Trust level for an {@link Individual} for use in the UI.
26  *
27  * @since 0.1.15
28  */
29 public enum Folks.TrustLevel
30 {
31   /**
32    * The {@link Individual}'s {@link Persona}s aren't trusted at all.
33    *
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.
37    *
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.
41    *
42    * @since 0.1.15
43    */
44   NONE,
45
46   /**
47    * The {@link Individual}'s {@link Persona}s are trusted.
48    *
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.
52    *
53    * Note that this doesn't guarantee that the user who behind each
54    * {@link Persona} is who they claim to be.
55    *
56    * @since 0.1.15
57    */
58   PERSONAS
59 }
60
61 /**
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.
64  */
65 public class Folks.Individual : Object,
66     Aliasable,
67     Avatar,
68     Favourite,
69     Groupable,
70     Presence
71 {
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;
87
88   /**
89    * The trust level of the Individual.
90    *
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}.
93    *
94    * Clients should ''not'' allow linking of Individuals who have a trust level
95    * of {@link TrustLevel.NONE}.
96    *
97    * @since 0.1.15
98    */
99   public TrustLevel trust_level { get; private set; }
100
101   /**
102    * {@inheritDoc}
103    */
104   public File avatar { get; private set; }
105
106   /**
107    * {@inheritDoc}
108    */
109   public Folks.PresenceType presence_type { get; private set; }
110
111   /**
112    * {@inheritDoc}
113    */
114   public string presence_message { get; private set; }
115
116   /**
117    * A unique identifier for the Individual.
118    *
119    * This uniquely identifies the Individual, and persists across
120    * {@link IndividualAggregator} instances.
121    *
122    * FIXME: Will this.id actually be the persistent ID for storage?
123    */
124   public string id { get; private set; }
125
126   /**
127    * Emitted when the last of the Individual's {@link Persona}s has been
128    * removed.
129    *
130    * At this point, the Individual is invalid, so any client referencing it
131    * should unreference it and remove it from their UI.
132    *
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
135    * @since 0.1.13
136    */
137   public signal void removed (Individual? replacement_individual);
138
139   /**
140    * {@inheritDoc}
141    */
142   public string alias
143     {
144       get { return this._alias; }
145
146       set
147         {
148           if (this._alias == value)
149             return;
150
151           this._alias = value;
152
153           debug ("Setting alias of individual '%s' to '%s'…", this.id, value);
154
155           /* First, try to write it to only the writeable Personas… */
156           bool alias_changed = false;
157           this._persona_list.foreach ((p) =>
158             {
159               if (p is Aliasable && ((Persona) p).store.is_writeable == true)
160                 {
161                   debug ("    written to writeable persona '%s'",
162                       ((Persona) p).uid);
163                   ((Aliasable) p).alias = value;
164                   alias_changed = true;
165                 }
166             });
167
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)
171             {
172               this._persona_list.foreach ((p) =>
173                 {
174                   if (p is Aliasable)
175                     {
176                       debug ("    written to non-writeable persona '%s'",
177                           ((Persona) p).uid);
178                       ((Aliasable) p).alias = value;
179                     }
180                 });
181             }
182         }
183     }
184
185   /**
186    * Whether this Individual is a user-defined favourite.
187    *
188    * This property is `true` if any of this Individual's {@link Persona}s are
189    * favourites).
190    */
191   public bool is_favourite
192     {
193       get { return this._is_favourite; }
194
195       set
196         {
197           if (this._is_favourite == value)
198             return;
199
200           debug ("Setting '%s' favourite status to %s", this.id,
201               value ? "TRUE" : "FALSE");
202
203           this._is_favourite = value;
204           this._persona_list.foreach ((p) =>
205             {
206               if (p is Favourite)
207                 {
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);
213                 }
214             });
215         }
216     }
217
218   /**
219    * {@inheritDoc}
220    */
221   public HashTable<string, bool> groups
222     {
223       get { return this._groups; }
224
225       set
226         {
227           this._groups = value;
228           this._persona_list.foreach ((p) =>
229             {
230               if (p is Groupable && ((Persona) p).store.is_writeable == true)
231                 ((Groupable) p).groups = value;
232             });
233         }
234     }
235
236   /**
237    * The set of {@link Persona}s encapsulated by this Individual.
238    *
239    * Changing the set of personas may cause updates to the aggregated properties
240    * provided by the Individual, resulting in property notifications for them.
241    *
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.
247    */
248   public GLib.List<Persona> personas
249     {
250       get { return this._persona_list; }
251       set { this._set_personas (value, null); }
252     }
253
254   /**
255    * Emitted when one or more {@link Persona}s are added to or removed from
256    * the Individual.
257    *
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
260    *
261    * @since 0.1.15
262    */
263   public signal void personas_changed (GLib.List<Persona>? added,
264       GLib.List<Persona>? removed);
265
266   private void notify_alias_cb (Object obj, ParamSpec ps)
267     {
268       this.update_alias ();
269     }
270
271   private void notify_avatar_cb (Object obj, ParamSpec ps)
272     {
273       this.update_avatar ();
274     }
275
276   private void persona_group_changed_cb (string group, bool is_member)
277     {
278       this.update_groups ();
279     }
280
281   /**
282    * Add or remove the Individual from the specified group.
283    *
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`.
286    *
287    * The group membership change will propagate to every {@link Persona} in
288    * the Individual.
289    *
290    * @param group a freeform group identifier
291    * @param is_member whether the Individual should be a member of the group
292    * @since 0.1.11
293    */
294   public async void change_group (string group, bool is_member)
295     {
296       this._persona_list.foreach ((p) =>
297         {
298           if (p is Groupable)
299             ((Groupable) p).change_group.begin (group, is_member);
300         });
301
302       /* don't notify, since it hasn't happened in the persona backing stores
303        * yet; react to that directly */
304     }
305
306   private void notify_presence_cb (Object obj, ParamSpec ps)
307     {
308       this.update_presence ();
309     }
310
311   private void notify_is_favourite_cb (Object obj, ParamSpec ps)
312     {
313       this.update_is_favourite ();
314     }
315
316   /**
317    * Create a new Individual.
318    *
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.
322    *
323    * @param personas a list of {@link Persona}s to initialise the
324    * {@link Individual} with, or `null`
325    * @return a new Individual
326    */
327   public Individual (GLib.List<Persona>? personas)
328     {
329       this._persona_set = new HashSet<Persona> (null, null);
330       this.stores = new HashMap<PersonaStore, uint> (null, null);
331       this.personas = personas;
332     }
333
334   private void store_removed_cb (PersonaStore store)
335     {
336       GLib.List<Persona> removed_personas = null;
337       Iterator<Persona> iter = this._persona_set.iterator ();
338       while (iter.next ())
339         {
340           Persona persona = iter.get ();
341
342           removed_personas.prepend (persona);
343           this._persona_list.remove (persona);
344           iter.remove ();
345         }
346
347       if (removed_personas != null)
348         this.personas_changed (null, removed_personas);
349
350       if (store != null)
351         this.stores.unset (store);
352
353       if (this._persona_set.size < 1)
354         {
355           this.removed (null);
356           return;
357         }
358
359       this.update_fields ();
360     }
361
362   private void store_personas_changed_cb (PersonaStore store,
363       GLib.List<Persona>? added,
364       GLib.List<Persona>? removed,
365       string? message,
366       Persona? actor,
367       Groupable.ChangeReason reason)
368     {
369       GLib.List<Persona> removed_personas = null;
370       removed.foreach ((data) =>
371         {
372           unowned Persona p = (Persona) data;
373
374           if (this._persona_set.remove (p))
375             {
376               removed_personas.prepend (p);
377               this._persona_list.remove (p);
378             }
379         });
380
381       if (removed_personas != null)
382         this.personas_changed (null, removed_personas);
383
384       if (this._persona_set.size < 1)
385         {
386           this.removed (null);
387           return;
388         }
389
390       this.update_fields ();
391     }
392
393   private void update_fields ()
394     {
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 ();
401     }
402
403   private void update_groups ()
404     {
405       var new_groups = new HashTable<string, bool> (str_hash, str_equal);
406
407       /* this._groups is null during initial construction */
408       if (this._groups == null)
409         this._groups = new HashTable<string, bool> (str_hash, str_equal);
410
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) =>
417         {
418           if (p is Groupable)
419             {
420               unowned Groupable persona = (Groupable) p;
421
422               persona.groups.foreach ((k, v) =>
423                 {
424                   new_groups.insert ((string) k, true);
425                 });
426             }
427         });
428
429       new_groups.foreach ((k, v) =>
430         {
431           var group = (string) k;
432           if (this._groups.lookup (group) != true)
433             {
434               this._groups.insert (group, true);
435               this._groups.foreach ((k, v) =>
436                 {
437                   var g = (string) k;
438                   debug ("   %s", g);
439                 });
440
441               this.group_changed (group, true);
442             }
443         });
444
445       /* buffer the removals, so we don't remove while iterating */
446       var removes = new GLib.List<string> ();
447       this._groups.foreach ((k, v) =>
448         {
449           var group = (string) k;
450           if (new_groups.lookup (group) != true)
451             removes.prepend (group);
452         });
453
454       removes.foreach ((l) =>
455         {
456           var group = (string) l;
457           this._groups.remove (group);
458           this.group_changed (group, false);
459         });
460     }
461
462   private void update_presence ()
463     {
464       var presence_message = "";
465       var presence_type = Folks.PresenceType.UNSET;
466
467       /* Choose the most available presence from our personas */
468       this._persona_list.foreach ((p) =>
469         {
470           if (p is Presence)
471             {
472               unowned Presence presence = (Presence) p;
473
474               if (Presence.typecmp (presence.presence_type, presence_type) > 0)
475                 {
476                   presence_type = presence.presence_type;
477                   presence_message = presence.presence_message;
478                 }
479             }
480         });
481
482       if (presence_message == null)
483         presence_message = "";
484
485       /* only notify if the value has changed */
486       if (this.presence_message != presence_message)
487         this.presence_message = presence_message;
488
489       if (this.presence_type != presence_type)
490         this.presence_type = presence_type;
491     }
492
493   private void update_is_favourite ()
494     {
495       bool favourite = false;
496
497       debug ("Running update_is_favourite() on '%s'", this.id);
498
499       this._persona_list.foreach ((p) =>
500         {
501           if (favourite == false && p is Favourite)
502             {
503               favourite = ((Favourite) p).is_favourite;
504               if (favourite == true)
505                 return;
506             }
507         });
508
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)
513         {
514           this._is_favourite = favourite;
515           this.notify_property ("is-favourite");
516         }
517     }
518
519   private void update_alias ()
520     {
521       string alias = null;
522       bool alias_is_display_id = false;
523
524       debug ("Updating alias for individual '%s'", this.id);
525
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
528        * stored. */
529       foreach (Persona p in this._persona_list)
530         {
531           if (p is Aliasable && p.store.is_writeable == true)
532             {
533               unowned Aliasable a = (Aliasable) p;
534
535               if (a.alias != null && a.alias.strip () != "")
536                 {
537                   alias = a.alias;
538                   break;
539                 }
540             }
541         }
542
543       debug ("    got alias '%s' from writeable personas", alias);
544
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. */
549       if (alias == null)
550         {
551           foreach (Persona p in this._persona_list)
552             {
553               if (p is Aliasable)
554                 {
555                   unowned Aliasable a = (Aliasable) p;
556
557                   if (a.alias == null || a.alias.strip () == "")
558                     continue;
559
560                   if (alias == null || alias_is_display_id == true)
561                     {
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). */
567                       alias = a.alias;
568
569                       if (a.alias == p.display_id)
570                         alias_is_display_id = true;
571                       else if (alias != null)
572                         break;
573                     }
574                 }
575             }
576         }
577
578       debug ("    got alias '%s' from non-writeable personas", alias);
579
580       if (alias == null)
581         {
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
584            * list. */
585           alias = this._persona_list.data.display_id;
586           debug ("No aliases available for individual; using display ID " +
587               "instead: %s", alias);
588         }
589
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)
595         {
596           debug ("Changing alias of individual '%s' from '%s' to '%s'.",
597               this.id, this._alias, alias);
598           this._alias = alias;
599           this.notify_property ("alias");
600         }
601     }
602
603   private void update_avatar ()
604     {
605       File avatar = null;
606
607       this._persona_list.foreach ((p) =>
608         {
609           if (avatar == null && p is Avatar)
610             {
611               avatar = ((Avatar) p).avatar;
612               return;
613             }
614         });
615
616       /* only notify if the value has changed */
617       if (this.avatar != avatar)
618         this.avatar = avatar;
619     }
620
621   private void update_trust_level ()
622     {
623       TrustLevel trust_level = TrustLevel.PERSONAS;
624
625       foreach (Persona p in this._persona_list)
626         {
627           if (p.store.trust_level == PersonaStoreTrust.NONE)
628             trust_level = TrustLevel.NONE;
629         }
630
631       /* Only notify if the value has changed */
632       if (this.trust_level != trust_level)
633         this.trust_level = trust_level;
634     }
635
636   /*
637    * GLib/C convenience functions (for built-in casting, etc.)
638    */
639
640   /**
641    * Get the Individual's alias.
642    *
643    * The alias is a user-chosen name for the Individual; how the user wants that
644    * Individual to be represented in UIs.
645    *
646    * @return the Individual's alias
647    */
648   public unowned string get_alias ()
649     {
650       return this.alias;
651     }
652
653   /**
654    * Get a mapping of group ID to whether the Individual is a member.
655    *
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.
658    *
659    * @return a mapping of group ID to membership status
660    */
661   public HashTable<string, bool> get_groups ()
662     {
663       Groupable g = this;
664       return g.groups;
665     }
666
667   /**
668    * Get the Individual's current presence message.
669    *
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}.
673    *
674    * If none of the {@link Persona}s in the Individual have a presence message
675    * set, an empty string is returned.
676    *
677    * @return the Individual's presence message
678    */
679   public unowned string get_presence_message ()
680     {
681       return this.presence_message;
682     }
683
684   /**
685    * Get the Individual's current presence type.
686    *
687    * The presence type returned is from the same {@link Persona} which provided
688    * the presence message returned by {@link Individual.get_presence_message}.
689    *
690    * If none of the {@link Persona}s in the Individual have a presence type set,
691    * {@link PresenceType.UNSET} is returned.
692    *
693    * @return the Individual's presence type
694    */
695   public Folks.PresenceType get_presence_type ()
696     {
697       return this.presence_type;
698     }
699
700   /**
701    * Whether the Individual is online.
702    *
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}.
706    *
707    * @return `true` if the Individual is online, `false` otherwise
708    */
709   public bool is_online ()
710     {
711       Presence p = this;
712       return p.is_online ();
713     }
714
715   private void connect_to_persona (Persona persona)
716     {
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);
722
723       if (persona is Groupable)
724         {
725           ((Groupable) persona).group_changed.connect (
726               this.persona_group_changed_cb);
727         }
728     }
729
730   private void disconnect_from_persona (Persona persona)
731     {
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);
739
740       if (persona is Groupable)
741         {
742           ((Groupable) persona).group_changed.disconnect (
743               this.persona_group_changed_cb);
744         }
745     }
746
747   private void _set_personas (GLib.List<Persona>? persona_list,
748       Individual? replacement_individual)
749     {
750       HashSet<Persona> persona_set = new HashSet<Persona> (null, null);
751       GLib.List<Persona> added = null;
752       GLib.List<Persona> removed = null;
753
754       /* Determine which Personas have been added */
755       foreach (Persona p in persona_list)
756         {
757           if (!this._persona_set.contains (p))
758             {
759               added.prepend (p);
760
761               this._persona_set.add (p);
762               this.connect_to_persona (p);
763
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)
768                 {
769                   this.stores.set (store, num_from_store + 1);
770                 }
771               else
772                 {
773                   this.stores.set (store, 1);
774
775                   store.removed.connect (this.store_removed_cb);
776                   store.personas_changed.connect (
777                       this.store_personas_changed_cb);
778                 }
779             }
780
781           persona_set.add (p);
782         }
783
784       /* Determine which Personas have been removed */
785       foreach (Persona p in this._persona_list)
786         {
787           if (!persona_set.contains (p))
788             {
789               removed.prepend (p);
790
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)
795                 {
796                   this.stores.set (store, num_from_store - 1);
797                 }
798               else
799                 {
800                   store.removed.disconnect (this.store_removed_cb);
801                   store.personas_changed.disconnect (
802                       this.store_personas_changed_cb);
803
804                   this.stores.unset (store);
805                 }
806
807               this.disconnect_from_persona (p);
808               this._persona_set.remove (p);
809             }
810         }
811
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 ();
816
817       this.personas_changed (added, removed);
818
819       /* If all the Personas have been removed, remove the Individual */
820       if (this._persona_set.size < 1)
821         {
822           this.removed (replacement_individual);
823             return;
824         }
825
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;
829
830       /* Update our aggregated fields and notify the changes */
831       this.update_fields ();
832     }
833
834   internal void replace (Individual replacement_individual)
835     {
836       this._set_personas (null, replacement_individual);
837     }
838 }