Don't set Individual.alias when updating the alias from Personas
[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.15
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.15
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.15
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.15
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
153           /* First, try to write it to only the writeable Personas… */
154           bool alias_changed = false;
155           this._persona_list.foreach ((p) =>
156             {
157               if (p is Alias && ((Persona) p).store.is_writeable == true)
158                 {
159                   ((Alias) p).alias = value;
160                   alias_changed = true;
161                 }
162             });
163
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)
167             {
168               this._persona_list.foreach ((p) =>
169                 {
170                   if (p is Alias)
171                     ((Alias) p).alias = value;
172                 });
173             }
174         }
175     }
176
177   /**
178    * Whether this Individual is a user-defined favourite.
179    *
180    * This property is `true` if any of this Individual's {@link Persona}s are
181    * favourites).
182    */
183   public bool is_favourite
184     {
185       get { return this._is_favourite; }
186
187       set
188         {
189           if (this._is_favourite == value)
190             return;
191
192           debug ("Setting '%s' favourite status to %s", this.id,
193               value ? "TRUE" : "FALSE");
194
195           this._is_favourite = value;
196           this._persona_list.foreach ((p) =>
197             {
198               if (p is Favourite)
199                 {
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);
205                 }
206             });
207         }
208     }
209
210   /**
211    * {@inheritDoc}
212    */
213   public HashTable<string, bool> groups
214     {
215       get { return this._groups; }
216
217       set
218         {
219           this._groups = value;
220           this._persona_list.foreach ((p) =>
221             {
222               if (p is Groups && ((Persona) p).store.is_writeable == true)
223                 ((Groups) p).groups = value;
224             });
225         }
226     }
227
228   /**
229    * The set of {@link Persona}s encapsulated by this Individual.
230    *
231    * Changing the set of personas may cause updates to the aggregated properties
232    * provided by the Individual, resulting in property notifications for them.
233    *
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.
239    */
240   public GLib.List<Persona> personas
241     {
242       get { return this._persona_list; }
243       set { this._set_personas (value, null); }
244     }
245
246   /**
247    * Emitted when one or more {@link Persona}s are added to or removed from
248    * the Individual.
249    *
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
252    *
253    * @since 0.1.15
254    */
255   public signal void personas_changed (GLib.List<Persona>? added,
256       GLib.List<Persona>? removed);
257
258   private void notify_alias_cb (Object obj, ParamSpec ps)
259     {
260       this.update_alias ();
261     }
262
263   private void notify_avatar_cb (Object obj, ParamSpec ps)
264     {
265       this.update_avatar ();
266     }
267
268   private void persona_group_changed_cb (string group, bool is_member)
269     {
270       this.update_groups ();
271     }
272
273   /**
274    * Add or remove the Individual from the specified group.
275    *
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`.
278    *
279    * The group membership change will propagate to every {@link Persona} in
280    * the Individual.
281    *
282    * @param group a freeform group identifier
283    * @param is_member whether the Individual should be a member of the group
284    */
285   public async void change_group (string group, bool is_member)
286     {
287       this._persona_list.foreach ((p) =>
288         {
289           if (p is Groups)
290             ((Groups) p).change_group.begin (group, is_member);
291         });
292
293       /* don't notify, since it hasn't happened in the persona backing stores
294        * yet; react to that directly */
295     }
296
297   private void notify_presence_cb (Object obj, ParamSpec ps)
298     {
299       this.update_presence ();
300     }
301
302   private void notify_is_favourite_cb (Object obj, ParamSpec ps)
303     {
304       this.update_is_favourite ();
305     }
306
307   /**
308    * Create a new Individual.
309    *
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.
313    *
314    * @return a new Individual
315    */
316   public Individual (GLib.List<Persona>? personas)
317     {
318       this._persona_set = new HashSet<Persona> (null, null);
319       this.stores = new HashMap<PersonaStore, uint> (null, null);
320       this.personas = personas;
321     }
322
323   private void store_removed_cb (PersonaStore store)
324     {
325       GLib.List<Persona> removed_personas = null;
326       Iterator<Persona> iter = this._persona_set.iterator ();
327       while (iter.next ())
328         {
329           Persona persona = iter.get ();
330
331           removed_personas.prepend (persona);
332           this._persona_list.remove (persona);
333           iter.remove ();
334         }
335
336       if (removed_personas != null)
337         this.personas_changed (null, removed_personas);
338
339       if (store != null)
340         this.stores.unset (store);
341
342       if (this._persona_set.size < 1)
343         {
344           this.removed (null);
345           return;
346         }
347
348       this.update_fields ();
349     }
350
351   private void store_personas_changed_cb (PersonaStore store,
352       GLib.List<Persona>? added,
353       GLib.List<Persona>? removed,
354       string? message,
355       Persona? actor,
356       Groups.ChangeReason reason)
357     {
358       GLib.List<Persona> removed_personas = null;
359       removed.foreach ((data) =>
360         {
361           unowned Persona p = (Persona) data;
362
363           if (this._persona_set.remove (p))
364             {
365               removed_personas.prepend (p);
366               this._persona_list.remove (p);
367             }
368         });
369
370       if (removed_personas != null)
371         this.personas_changed (null, removed_personas);
372
373       if (this._persona_set.size < 1)
374         {
375           this.removed (null);
376           return;
377         }
378
379       this.update_fields ();
380     }
381
382   private void update_fields ()
383     {
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 ();
390     }
391
392   private void update_groups ()
393     {
394       var new_groups = new HashTable<string, bool> (str_hash, str_equal);
395
396       /* this._groups is null during initial construction */
397       if (this._groups == null)
398         this._groups = new HashTable<string, bool> (str_hash, str_equal);
399
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) =>
406         {
407           if (p is Groups)
408             {
409               unowned Groups persona = (Groups) p;
410
411               persona.groups.foreach ((k, v) =>
412                 {
413                   new_groups.insert ((string) k, true);
414                 });
415             }
416         });
417
418       new_groups.foreach ((k, v) =>
419         {
420           var group = (string) k;
421           if (this._groups.lookup (group) != true)
422             {
423               this._groups.insert (group, true);
424               this._groups.foreach ((k, v) =>
425                 {
426                   var g = (string) k;
427                   debug ("   %s", g);
428                 });
429
430               this.group_changed (group, true);
431             }
432         });
433
434       /* buffer the removals, so we don't remove while iterating */
435       var removes = new GLib.List<string> ();
436       this._groups.foreach ((k, v) =>
437         {
438           var group = (string) k;
439           if (new_groups.lookup (group) != true)
440             removes.prepend (group);
441         });
442
443       removes.foreach ((l) =>
444         {
445           var group = (string) l;
446           this._groups.remove (group);
447           this.group_changed (group, false);
448         });
449     }
450
451   private void update_presence ()
452     {
453       var presence_message = "";
454       var presence_type = Folks.PresenceType.UNSET;
455
456       /* Choose the most available presence from our personas */
457       this._persona_list.foreach ((p) =>
458         {
459           if (p is Presence)
460             {
461               unowned Presence presence = (Presence) p;
462
463               if (Presence.typecmp (presence.presence_type, presence_type) > 0)
464                 {
465                   presence_type = presence.presence_type;
466                   presence_message = presence.presence_message;
467                 }
468             }
469         });
470
471       if (presence_message == null)
472         presence_message = "";
473
474       /* only notify if the value has changed */
475       if (this.presence_message != presence_message)
476         this.presence_message = presence_message;
477
478       if (this.presence_type != presence_type)
479         this.presence_type = presence_type;
480     }
481
482   private void update_is_favourite ()
483     {
484       bool favourite = false;
485
486       debug ("Running update_is_favourite() on '%s'", this.id);
487
488       this._persona_list.foreach ((p) =>
489         {
490           if (favourite == false && p is Favourite)
491             {
492               favourite = ((Favourite) p).is_favourite;
493               if (favourite == true)
494                 return;
495             }
496         });
497
498       /* Only notify if the value has changed */
499       if (this.is_favourite != favourite)
500         this.is_favourite = favourite;
501     }
502
503   private void update_alias ()
504     {
505       string alias = null;
506       bool alias_is_display_id = false;
507
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
510        * stored. */
511       foreach (Persona p in this._persona_list)
512         {
513           if (p is Alias && p.store.is_writeable == true)
514             {
515               unowned Alias a = (Alias) p;
516
517               if (a.alias != null && a.alias.strip () != "")
518                 {
519                   alias = a.alias;
520                   break;
521                 }
522             }
523         }
524
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. */
529       if (alias == null)
530         {
531           foreach (Persona p in this._persona_list)
532             {
533               if (p is Alias)
534                 {
535                   unowned Alias a = (Alias) p;
536
537                   if (a.alias == null || a.alias.strip () == "")
538                     continue;
539
540                   if (alias == null || alias_is_display_id == true)
541                     {
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). */
547                       alias = a.alias;
548
549                       if (a.alias == p.display_id)
550                         alias_is_display_id = true;
551                       else if (alias != null)
552                         break;
553                     }
554                 }
555             }
556         }
557
558       if (alias == null)
559         {
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
562            * list. */
563           alias = this._persona_list.data.display_id;
564           debug ("No aliases available for individual; using display ID " +
565               "instead: %s", alias);
566         }
567
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)
573         {
574           this._alias = alias;
575           this.notify_property ("alias");
576         }
577     }
578
579   private void update_avatar ()
580     {
581       File avatar = null;
582
583       this._persona_list.foreach ((p) =>
584         {
585           if (avatar == null && p is Avatar)
586             {
587               avatar = ((Avatar) p).avatar;
588               return;
589             }
590         });
591
592       /* only notify if the value has changed */
593       if (this.avatar != avatar)
594         this.avatar = avatar;
595     }
596
597   private void update_trust_level ()
598     {
599       TrustLevel trust_level = TrustLevel.PERSONAS;
600
601       foreach (Persona p in this._persona_list)
602         {
603           if (p.store.trust_level == PersonaStoreTrust.NONE)
604             trust_level = TrustLevel.NONE;
605         }
606
607       /* Only notify if the value has changed */
608       if (this.trust_level != trust_level)
609         this.trust_level = trust_level;
610     }
611
612   /*
613    * GLib/C convenience functions (for built-in casting, etc.)
614    */
615
616   /**
617    * Get the Individual's alias.
618    *
619    * The alias is a user-chosen name for the Individual; how the user wants that
620    * Individual to be represented in UIs.
621    *
622    * @return the Individual's alias
623    */
624   public unowned string get_alias ()
625     {
626       return this.alias;
627     }
628
629   /**
630    * Get a mapping of group ID to whether the Individual is a member.
631    *
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.
634    *
635    * @return a mapping of group ID to membership status
636    */
637   public HashTable<string, bool> get_groups ()
638     {
639       Groups g = this;
640       return g.groups;
641     }
642
643   /**
644    * Get the Individual's current presence message.
645    *
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}.
649    *
650    * If none of the {@link Persona}s in the Individual have a presence message
651    * set, an empty string is returned.
652    *
653    * @return the Individual's presence message
654    */
655   public unowned string get_presence_message ()
656     {
657       return this.presence_message;
658     }
659
660   /**
661    * Get the Individual's current presence type.
662    *
663    * The presence type returned is from the same {@link Persona} which provided
664    * the presence message returned by {@link Individual.get_presence_message}.
665    *
666    * If none of the {@link Persona}s in the Individual have a presence type set,
667    * {@link PresenceType.UNSET} is returned.
668    *
669    * @return the Individual's presence type
670    */
671   public Folks.PresenceType get_presence_type ()
672     {
673       return this.presence_type;
674     }
675
676   /**
677    * Whether the Individual is online.
678    *
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}.
682    *
683    * @return `true` if the Individual is online, `false` otherwise
684    */
685   public bool is_online ()
686     {
687       Presence p = this;
688       return p.is_online ();
689     }
690
691   private void connect_to_persona (Persona persona)
692     {
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);
698
699       if (persona is Groups)
700         {
701           ((Groups) persona).group_changed.connect (
702               this.persona_group_changed_cb);
703         }
704     }
705
706   private void disconnect_from_persona (Persona persona)
707     {
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);
715
716       if (persona is Groups)
717         {
718           ((Groups) persona).group_changed.disconnect (
719               this.persona_group_changed_cb);
720         }
721     }
722
723   private void _set_personas (GLib.List<Persona>? persona_list,
724       Individual? replacement_individual)
725     {
726       HashSet<Persona> persona_set = new HashSet<Persona> (null, null);
727       GLib.List<Persona> added = null;
728       GLib.List<Persona> removed = null;
729
730       /* Determine which Personas have been added */
731       foreach (Persona p in persona_list)
732         {
733           if (!this._persona_set.contains (p))
734             {
735               added.prepend (p);
736
737               this._persona_set.add (p);
738               this.connect_to_persona (p);
739
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)
744                 {
745                   this.stores.set (store, num_from_store + 1);
746                 }
747               else
748                 {
749                   this.stores.set (store, 1);
750
751                   store.removed.connect (this.store_removed_cb);
752                   store.personas_changed.connect (
753                       this.store_personas_changed_cb);
754                 }
755             }
756
757           persona_set.add (p);
758         }
759
760       /* Determine which Personas have been removed */
761       foreach (Persona p in this._persona_list)
762         {
763           if (!persona_set.contains (p))
764             {
765               removed.prepend (p);
766
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)
771                 {
772                   this.stores.set (store, num_from_store - 1);
773                 }
774               else
775                 {
776                   store.removed.disconnect (this.store_removed_cb);
777                   store.personas_changed.disconnect (
778                       this.store_personas_changed_cb);
779
780                   this.stores.unset (store);
781                 }
782
783               this.disconnect_from_persona (p);
784               this._persona_set.remove (p);
785             }
786         }
787
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 ();
792
793       this.personas_changed (added, removed);
794
795       /* If all the Personas have been removed, remove the Individual */
796       if (this._persona_set.size < 1)
797         {
798           this.removed (replacement_individual);
799             return;
800         }
801
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;
805
806       /* Update our aggregated fields and notify the changes */
807       this.update_fields ();
808     }
809
810   internal void replace (Individual replacement_individual)
811     {
812       this._set_personas (null, replacement_individual);
813     }
814 }