Bug 627968 — Add Individual.trust_level property
[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 using Folks;
24
25 /**
26  * Trust level for an {@link Individual} for use in the UI.
27  *
28  * @since 0.1.16
29  */
30 public enum Folks.TrustLevel
31 {
32   /**
33    * The {@link Individual}'s {@link Persona}s aren't trusted at all.
34    *
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.
38    *
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.
42    *
43    * @since 0.1.16
44    */
45   NONE,
46
47   /**
48    * The {@link Individual}'s {@link Persona}s are trusted.
49    *
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.
53    *
54    * Note that this doesn't guarantee that the user who behind each
55    * {@link Persona} is who they claim to be.
56    *
57    * @since 0.1.16
58    */
59   PERSONAS
60 }
61
62 /**
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.
65  */
66 public class Folks.Individual : Object,
67     Alias,
68     Avatar,
69     Favourite,
70     Groups,
71     Presence
72 {
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;
88
89   /**
90    * The trust level of the Individual.
91    *
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}.
94    *
95    * Clients should ''not'' allow linking of Individuals who have a trust level
96    * of {@link TrustLevel.NONE}.
97    *
98    * @since 0.1.16
99    */
100   public TrustLevel trust_level { get; private set; }
101
102   /**
103    * {@inheritDoc}
104    */
105   public File avatar { get; private set; }
106
107   /**
108    * {@inheritDoc}
109    */
110   public Folks.PresenceType presence_type { get; private set; }
111
112   /**
113    * {@inheritDoc}
114    */
115   public string presence_message { get; private set; }
116
117   /**
118    * A unique identifier for the Individual.
119    *
120    * This uniquely identifies the Individual, and persists across
121    * {@link IndividualAggregator} instances.
122    *
123    * FIXME: Will this.id actually be the persistent ID for storage?
124    */
125   public string id { get; private set; }
126
127   /**
128    * Emitted when the last of the Individual's {@link Persona}s has been
129    * removed.
130    *
131    * At this point, the Individual is invalid, so any client referencing it
132    * should unreference it and remove it from their UI.
133    *
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
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           this._persona_list.foreach ((p) =>
153             {
154               if (p is Alias && ((Persona) p).store.is_writeable == true)
155                 ((Alias) p).alias = value;
156             });
157         }
158     }
159
160   /**
161    * Whether this Individual is a user-defined favourite.
162    *
163    * This property is `true` if any of this Individual's {@link Persona}s are
164    * favourites).
165    */
166   public bool is_favourite
167     {
168       get { return this._is_favourite; }
169
170       set
171         {
172           if (this._is_favourite == value)
173             return;
174
175           this._is_favourite = value;
176           this._persona_list.foreach ((p) =>
177             {
178               if (p is Favourite && ((Persona) p).store.is_writeable == true)
179                 ((Favourite) p).is_favourite = value;
180             });
181         }
182     }
183
184   /**
185    * {@inheritDoc}
186    */
187   public HashTable<string, bool> groups
188     {
189       get { return this._groups; }
190
191       set
192         {
193           this._persona_list.foreach ((p) =>
194             {
195               if (p is Groups && ((Persona) p).store.is_writeable == true)
196                 ((Groups) p).groups = value;
197             });
198         }
199     }
200
201   /**
202    * The set of {@link Persona}s encapsulated by this Individual.
203    *
204    * Changing the set of personas may cause updates to the aggregated properties
205    * provided by the Individual, resulting in property notifications for them.
206    *
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.
212    */
213   public GLib.List<Persona> personas
214     {
215       get { return this._persona_list; }
216       set { this._set_personas (value, null); }
217     }
218
219   /**
220    * Emitted when one or more {@link Persona}s are added to or removed from
221    * the Individual.
222    *
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
225    *
226    * @since 0.1.16
227    */
228   public signal void personas_changed (GLib.List<Persona>? added,
229       GLib.List<Persona>? removed);
230
231   private void notify_groups_cb (Object obj, ParamSpec ps)
232     {
233       this.update_groups ();
234     }
235
236   private void notify_alias_cb (Object obj, ParamSpec ps)
237     {
238       this.update_alias ();
239     }
240
241   private void notify_avatar_cb (Object obj, ParamSpec ps)
242     {
243       this.update_avatar ();
244     }
245
246   private void persona_group_changed_cb (string group, bool is_member)
247     {
248       this.change_group.begin (group, is_member);
249       this.update_groups ();
250     }
251
252   /**
253    * Add or remove the Individual from the specified group.
254    *
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`.
257    *
258    * The group membership change will propagate to every {@link Persona} in
259    * the Individual.
260    *
261    * @param group a freeform group identifier
262    * @param is_member whether the Individual should be a member of the group
263    */
264   public async void change_group (string group, bool is_member)
265     {
266       this._persona_list.foreach ((p) =>
267         {
268           if (p is Groups)
269             ((Groups) p).change_group.begin (group, is_member);
270         });
271
272       /* don't notify, since it hasn't happened in the persona backing stores
273        * yet; react to that directly */
274     }
275
276   private void notify_presence_cb (Object obj, ParamSpec ps)
277     {
278       this.update_presence ();
279     }
280
281   private void notify_is_favourite_cb (Object obj, ParamSpec ps)
282     {
283       this.update_is_favourite ();
284     }
285
286   /**
287    * Create a new Individual.
288    *
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.
292    *
293    * @return a new Individual
294    */
295   public Individual (GLib.List<Persona>? personas)
296     {
297       this._persona_set = new HashSet<Persona> (null, null);
298       this.stores = new HashMap<PersonaStore, uint> (null, null);
299       this.personas = personas;
300     }
301
302   private void store_removed_cb (PersonaStore store)
303     {
304       GLib.List<Persona> removed_personas = null;
305       Iterator<Persona> iter = this._persona_set.iterator ();
306       while (iter.next ())
307         {
308           Persona persona = iter.get ();
309
310           removed_personas.prepend (persona);
311           this._persona_list.remove (persona);
312           iter.remove ();
313         }
314
315       if (removed_personas != null)
316         this.personas_changed (null, removed_personas);
317
318       if (store != null)
319         this.stores.remove (store);
320
321       if (this._persona_set.size < 1)
322         {
323           this.removed (null);
324           return;
325         }
326
327       this.update_fields ();
328     }
329
330   private void store_personas_changed_cb (PersonaStore store,
331       GLib.List<Persona>? added,
332       GLib.List<Persona>? removed,
333       string? message,
334       Persona? actor,
335       Groups.ChangeReason reason)
336     {
337       GLib.List<Persona> removed_personas = null;
338       removed.foreach ((data) =>
339         {
340           unowned Persona p = (Persona) data;
341
342           if (this._persona_set.remove (p))
343             {
344               removed_personas.prepend (p);
345               this._persona_list.remove (p);
346             }
347         });
348
349       if (removed_personas != null)
350         this.personas_changed (null, removed_personas);
351
352       if (this._persona_set.size < 1)
353         {
354           this.removed (null);
355           return;
356         }
357
358       this.update_fields ();
359     }
360
361   private void update_fields ()
362     {
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 ();
369     }
370
371   private void update_groups ()
372     {
373       var new_groups = new HashTable<string, bool> (str_hash, str_equal);
374
375       /* this._groups is null during initial construction */
376       if (this._groups == null)
377         this._groups = new HashTable<string, bool> (str_hash, str_equal);
378
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) =>
385         {
386           if (p is Groups)
387             {
388               unowned Groups persona = (Groups) p;
389
390               persona.groups.foreach ((k, v) =>
391                 {
392                   new_groups.insert ((string) k, true);
393                 });
394             }
395         });
396
397       new_groups.foreach ((k, v) =>
398         {
399           var group = (string) k;
400           if (this._groups.lookup (group) != true)
401             {
402               this._groups.insert (group, true);
403               this._groups.foreach ((k, v) =>
404                 {
405                   var g = (string) k;
406                   debug ("   %s", g);
407                 });
408
409               this.group_changed (group, true);
410             }
411         });
412
413       /* buffer the removals, so we don't remove while iterating */
414       var removes = new GLib.List<string> ();
415       this._groups.foreach ((k, v) =>
416         {
417           var group = (string) k;
418           if (new_groups.lookup (group) != true)
419             removes.prepend (group);
420         });
421
422       removes.foreach ((l) =>
423         {
424           var group = (string) l;
425           this._groups.remove (group);
426           this.group_changed (group, false);
427         });
428     }
429
430   private void update_presence ()
431     {
432       var presence_message = "";
433       var presence_type = Folks.PresenceType.UNSET;
434
435       /* Choose the most available presence from our personas */
436       this._persona_list.foreach ((p) =>
437         {
438           if (p is Presence)
439             {
440               unowned Presence presence = (Presence) p;
441
442               if (Presence.typecmp (presence.presence_type, presence_type) > 0)
443                 {
444                   presence_type = presence.presence_type;
445                   presence_message = presence.presence_message;
446                 }
447             }
448         });
449
450       if (presence_message == null)
451         presence_message = "";
452
453       /* only notify if the value has changed */
454       if (this.presence_message != presence_message)
455         this.presence_message = presence_message;
456
457       if (this.presence_type != presence_type)
458         this.presence_type = presence_type;
459     }
460
461   private void update_is_favourite ()
462     {
463       bool favourite = false;
464
465       this._persona_list.foreach ((p) =>
466         {
467           if (favourite == false && p is Favourite)
468             {
469               favourite = ((Favourite) p).is_favourite;
470               if (favourite == true)
471                 return;
472             }
473         });
474
475       /* Only notify if the value has changed */
476       if (this.is_favourite != favourite)
477         this.is_favourite = favourite;
478     }
479
480   private void update_alias ()
481     {
482       string alias = null;
483       bool alias_is_display_id = false;
484
485       foreach (Persona p in this._persona_list)
486         {
487           if (p is Alias)
488             {
489               unowned Alias a = (Alias) p;
490
491               if (a.alias == null || a.alias.strip () == "")
492                 continue;
493
494               if (alias == null || alias_is_display_id == true)
495                 {
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). */
501                   alias = a.alias;
502
503                   if (a.alias == p.display_id)
504                     alias_is_display_id = true;
505                   else if (alias != null)
506                     break;
507                 }
508             }
509         }
510
511       if (alias == null)
512         {
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",
517                    alias);
518         }
519
520       /* only notify if the value has changed */
521       if (this.alias != alias)
522         this.alias = alias;
523     }
524
525   private void update_avatar ()
526     {
527       File avatar = null;
528
529       this._persona_list.foreach ((p) =>
530         {
531           if (avatar == null && p is Avatar)
532             {
533               avatar = ((Avatar) p).avatar;
534               return;
535             }
536         });
537
538       /* only notify if the value has changed */
539       if (this.avatar != avatar)
540         this.avatar = avatar;
541     }
542
543   private void update_trust_level ()
544     {
545       TrustLevel trust_level = TrustLevel.PERSONAS;
546
547       foreach (Persona p in this._persona_list)
548         {
549           if (p.store.trust_level == PersonaStoreTrust.NONE)
550             trust_level = TrustLevel.NONE;
551         }
552
553       /* Only notify if the value has changed */
554       if (this.trust_level != trust_level)
555         this.trust_level = trust_level;
556     }
557
558   /*
559    * GLib/C convenience functions (for built-in casting, etc.)
560    */
561
562   /**
563    * Get the Individual's alias.
564    *
565    * The alias is a user-chosen name for the Individual; how the user wants that
566    * Individual to be represented in UIs.
567    *
568    * @return the Individual's alias
569    */
570   public unowned string get_alias ()
571     {
572       return this.alias;
573     }
574
575   /**
576    * Get a mapping of group ID to whether the Individual is a member.
577    *
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.
580    *
581    * @return a mapping of group ID to membership status
582    */
583   public HashTable<string, bool> get_groups ()
584     {
585       Groups g = this;
586       return g.groups;
587     }
588
589   /**
590    * Get the Individual's current presence message.
591    *
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}.
595    *
596    * If none of the {@link Persona}s in the Individual have a presence message
597    * set, an empty string is returned.
598    *
599    * @return the Individual's presence message
600    */
601   public unowned string get_presence_message ()
602     {
603       return this.presence_message;
604     }
605
606   /**
607    * Get the Individual's current presence type.
608    *
609    * The presence type returned is from the same {@link Persona} which provided
610    * the presence message returned by {@link Individual.get_presence_message}.
611    *
612    * If none of the {@link Persona}s in the Individual have a presence type set,
613    * {@link PresenceType.UNSET} is returned.
614    *
615    * @return the Individual's presence type
616    */
617   public Folks.PresenceType get_presence_type ()
618     {
619       return this.presence_type;
620     }
621
622   /**
623    * Whether the Individual is online.
624    *
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}.
628    *
629    * @return `true` if the Individual is online, `false` otherwise
630    */
631   public bool is_online ()
632     {
633       Presence p = this;
634       return p.is_online ();
635     }
636
637   private void connect_to_persona (Persona persona)
638     {
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);
645
646       if (persona is Groups)
647         {
648           ((Groups) persona).group_changed.connect (
649               this.persona_group_changed_cb);
650         }
651     }
652
653   private void disconnect_from_persona (Persona persona)
654     {
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);
663
664       if (persona is Groups)
665         {
666           ((Groups) persona).group_changed.disconnect (
667               this.persona_group_changed_cb);
668         }
669     }
670
671   private void _set_personas (GLib.List<Persona>? persona_list,
672       Individual? replacement_individual)
673     {
674       HashSet<Persona> persona_set = new HashSet<Persona> (null, null);
675       GLib.List<Persona> added = null;
676       GLib.List<Persona> removed = null;
677
678       /* Determine which Personas have been added */
679       foreach (Persona p in persona_list)
680         {
681           if (!this._persona_set.contains (p))
682             {
683               added.prepend (p);
684
685               this._persona_set.add (p);
686               this.connect_to_persona (p);
687
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)
692                 {
693                   this.stores.set (store, num_from_store + 1);
694                 }
695               else
696                 {
697                   this.stores.set (store, 1);
698
699                   store.removed.connect (this.store_removed_cb);
700                   store.personas_changed.connect (
701                       this.store_personas_changed_cb);
702                 }
703             }
704
705           persona_set.add (p);
706         }
707
708       /* Determine which Personas have been removed */
709       foreach (Persona p in this._persona_list)
710         {
711           if (!persona_set.contains (p))
712             {
713               removed.prepend (p);
714
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)
719                 {
720                   this.stores.set (store, num_from_store - 1);
721                 }
722               else
723                 {
724                   store.removed.disconnect (this.store_removed_cb);
725                   store.personas_changed.disconnect (
726                       this.store_personas_changed_cb);
727
728                   this.stores.unset (store);
729                 }
730
731               this.disconnect_from_persona (p);
732               this._persona_set.remove (p);
733             }
734         }
735
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 ();
740
741       this.personas_changed (added, removed);
742
743       /* If all the Personas have been removed, remove the Individual */
744       if (this._persona_set.size < 1)
745         {
746           this.removed (replacement_individual);
747             return;
748         }
749
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;
753
754       /* Update our aggregated fields and notify the changes */
755       this.update_fields ();
756     }
757
758   internal void replace (Individual replacement_individual)
759     {
760       this._set_personas (null, replacement_individual);
761     }
762 }