Bug 652660 — Make Individual.id more stable and well-defined
[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     AliasDetails,
67     AvatarDetails,
68     BirthdayDetails,
69     EmailDetails,
70     FavouriteDetails,
71     GenderDetails,
72     GroupDetails,
73     ImDetails,
74     LocalIdDetails,
75     NameDetails,
76     NoteDetails,
77     PresenceDetails,
78     PhoneDetails,
79     PostalAddressDetails,
80     RoleDetails,
81     UrlDetails,
82     WebServiceDetails
83 {
84   private bool _is_favourite;
85   private string _alias;
86   private HashSet<string> _groups;
87   /* Stores the Personas contained in this Individual. */
88   private HashSet<Persona> _persona_set;
89   /* Read-only view of the above set */
90   private Set<Persona> _persona_set_ro;
91   /* Mapping from PersonaStore -> number of Personas from that store contained
92    * in this Individual. There shouldn't be any entries with a number < 1.
93    * This is used for working out when to disconnect from store signals. */
94   private HashMap<PersonaStore, uint> _stores;
95   /* The number of Personas in this Individual which have
96    * Persona.is_user == true. Iff this is > 0, Individual.is_user == true. */
97   private uint _persona_user_count = 0;
98   private HashMultiMap<string, string> _im_addresses;
99   private HashMultiMap<string, string> _web_service_addresses;
100
101   /**
102    * The trust level of the Individual.
103    *
104    * This specifies how far the Individual can be trusted to be who it claims
105    * to be. See the descriptions for the elements of {@link TrustLevel}.
106    *
107    * Clients should ''not'' allow linking of Individuals who have a trust level
108    * of {@link TrustLevel.NONE}.
109    *
110    * @since 0.1.15
111    */
112   public TrustLevel trust_level { get; private set; }
113
114   /**
115    * {@inheritDoc}
116    */
117   public File avatar { get; private set; }
118
119   /**
120    * {@inheritDoc}
121    */
122   public Folks.PresenceType presence_type { get; private set; }
123
124   /**
125    * {@inheritDoc}
126    *
127    * @since 0.5.UNRELEASED
128    */
129   public string presence_status { get; private set; }
130
131   /**
132    * {@inheritDoc}
133    */
134   public string presence_message { get; private set; }
135
136   /**
137    * Whether the Individual is the user.
138    *
139    * Iff the Individual represents the user (the person who owns the
140    * account in the backend for each {@link Persona} in the Individual)
141    * this is `true`.
142    *
143    * It is //not// guaranteed that every {@link Persona} in the Individual has
144    * its {@link Persona.is_user} set to the same value as the Individual. For
145    * example, the user could own two Telepathy accounts, and have added the
146    * other account as a contact in each account. The accounts will expose a
147    * {@link Persona} for the user (which will have {@link Persona.is_user} set
148    * to `true`) //and// a {@link Persona} for the contact for the other account
149    * (which will have {@link Persona.is_user} set to `false`).
150    *
151    * It is guaranteed that iff this property is set to `true` on an Individual,
152    * there will be at least one {@link Persona} in the Individual with its
153    * {@link Persona.is_user} set to `true`.
154    *
155    * It is guaranteed that there will only ever be one Individual with this
156    * property set to `true`.
157    *
158    * @since 0.3.0
159    */
160   public bool is_user { get; private set; }
161
162   /**
163    * A unique identifier for the Individual.
164    *
165    * This uniquely identifies the Individual, and persists across
166    * {@link IndividualAggregator} instances. It may not persist across linking
167    * the Individual with other Individuals.
168    *
169    * This is an opaque string and has no structure.
170    *
171    * If an identifier is required which will be used for a long-lived link
172    * between different stored data, it may be more desirable to use the
173    * {@link Persona.uid} of the most relevant {@link Persona} in the Individual
174    * instead. For example, if storing references to Individuals who are tagged
175    * in a photo, it may be safer to store the UID of the Persona whose backend
176    * provided the photo (e.g. Facebook).
177    */
178   public string id { get; private set; }
179
180   /**
181    * Emitted when the last of the Individual's {@link Persona}s has been
182    * removed.
183    *
184    * At this point, the Individual is invalid, so any client referencing it
185    * should unreference it and remove it from their UI.
186    *
187    * @param replacement_individual the individual which has replaced this one
188    * due to linking, or `null` if this individual was removed for another reason
189    * @since 0.1.13
190    */
191   public signal void removed (Individual? replacement_individual);
192
193   /**
194    * {@inheritDoc}
195    */
196   public string alias
197     {
198       get { return this._alias; }
199
200       set
201         {
202           if (this._alias == value)
203             return;
204
205           this._alias = value;
206
207           debug ("Setting alias of individual '%s' to '%s'…", this.id, value);
208
209           /* First, try to write it to only the writeable Personas… */
210           var alias_changed = false;
211           foreach (var p in this._persona_set)
212             {
213               if (p is AliasDetails &&
214                   ((Persona) p).store.is_writeable == true)
215                 {
216                   debug ("    written to writeable persona '%s'",
217                       ((Persona) p).uid);
218                   ((AliasDetails) p).alias = value;
219                   alias_changed = true;
220                 }
221             }
222
223           /* …but if there are no writeable Personas, we have to fall back to
224            * writing it to every Persona. */
225           if (alias_changed == false)
226             {
227               foreach (var p in this._persona_set)
228                 {
229                   if (p is AliasDetails)
230                     {
231                       debug ("    written to non-writeable persona '%s'",
232                           ((Persona) p).uid);
233                       ((AliasDetails) p).alias = value;
234                     }
235                 }
236             }
237         }
238     }
239
240   /**
241    * {@inheritDoc}
242    */
243   public StructuredName structured_name { get; private set; }
244
245   /**
246    * {@inheritDoc}
247    */
248   public string full_name { get; private set; }
249
250   private string _nickname;
251   /**
252    * {@inheritDoc}
253    */
254   public string nickname { get { return this._nickname; } }
255
256   private Gender _gender;
257   /**
258    * {@inheritDoc}
259    */
260   public Gender gender
261     {
262       get { return this._gender; }
263       private set
264         {
265           this._gender = value;
266           this.notify_property ("gender");
267         }
268     }
269
270   private HashSet<FieldDetails> _urls;
271   /**
272    * {@inheritDoc}
273    */
274   public Set<FieldDetails> urls
275     {
276       get { return this._urls; }
277       private set
278         {
279           this._urls = new HashSet<FieldDetails> ();
280           foreach (var ps in value)
281             this._urls.add (ps);
282         }
283     }
284
285   private HashSet<FieldDetails> _phone_numbers;
286
287   /**
288    * {@inheritDoc}
289    */
290   public Set<FieldDetails> phone_numbers
291     {
292       get { return this._phone_numbers; }
293       private set
294         {
295           this._phone_numbers = new HashSet<FieldDetails> ();
296           foreach (var fd in value)
297             this._phone_numbers.add (fd);
298         }
299     }
300
301   private HashSet<FieldDetails> _email_addresses;
302   /**
303    * {@inheritDoc}
304    */
305   public Set<FieldDetails> email_addresses
306     {
307       get { return this._email_addresses; }
308       private set
309         {
310           this._email_addresses = new HashSet<FieldDetails> ();
311           foreach (var fd in value)
312             this._email_addresses.add (fd);
313         }
314     }
315
316   private HashSet<Role> _roles;
317
318   /**
319    * {@inheritDoc}
320    */
321   public Set<Role> roles
322     {
323       get { return this._roles; }
324       private set
325         {
326           this._roles = new HashSet<Role> ();
327           foreach (var role in value)
328             this._roles.add (role);
329           this.notify_property ("roles");
330         }
331     }
332
333   private HashSet<string> _local_ids;
334
335   /**
336    * {@inheritDoc}
337    */
338   public Set<string> local_ids
339     {
340       get { return this._local_ids; }
341       private set
342         {
343           this._local_ids = new HashSet<string> ();
344           foreach (var id in value)
345             this._local_ids.add (id);
346           this.notify_property ("local-ids");
347         }
348     }
349
350   public DateTime birthday { get; set; }
351
352   public string calendar_event_id { get; set; }
353
354   private HashSet<Note> _notes;
355
356   /**
357    * {@inheritDoc}
358    */
359   public Set<Note> notes
360     {
361       get { return this._notes; }
362       private set
363         {
364           this._notes = new HashSet<Note> ();
365           foreach (var note in value)
366             this._notes.add (note);
367           this.notify_property ("notes");
368         }
369     }
370
371   private HashSet<PostalAddress> _postal_addresses;
372   /**
373    * {@inheritDoc}
374    */
375   public Set<PostalAddress> postal_addresses
376     {
377       get { return this._postal_addresses; }
378       private set
379         {
380           this._postal_addresses = new HashSet<PostalAddress> ();
381           foreach (PostalAddress pa in value)
382             this._postal_addresses.add (pa);
383         }
384     }
385
386   /**
387    * Whether this Individual is a user-defined favourite.
388    *
389    * This property is `true` if any of this Individual's {@link Persona}s are
390    * favourites).
391    */
392   public bool is_favourite
393     {
394       get { return this._is_favourite; }
395
396       set
397         {
398           if (this._is_favourite == value)
399             return;
400
401           debug ("Setting '%s' favourite status to %s", this.id,
402               value ? "TRUE" : "FALSE");
403
404           this._is_favourite = value;
405           foreach (var p in this._persona_set)
406             {
407               if (p is FavouriteDetails)
408                 {
409                   SignalHandler.block_by_func (p,
410                       (void*) this._notify_is_favourite_cb, this);
411                   ((FavouriteDetails) p).is_favourite = value;
412                   SignalHandler.unblock_by_func (p,
413                       (void*) this._notify_is_favourite_cb, this);
414                 }
415             }
416         }
417     }
418
419   /**
420    * {@inheritDoc}
421    */
422   public Set<string> groups
423     {
424       get { return this._groups; }
425
426       set
427         {
428           foreach (var p in this._persona_set)
429             {
430               if (p is GroupDetails && ((Persona) p).store.is_writeable == true)
431                 ((GroupDetails) p).groups = value;
432             }
433           this._update_groups ();
434         }
435     }
436
437   /**
438    * {@inheritDoc}
439    */
440   public MultiMap<string, string> im_addresses
441     {
442       get { return this._im_addresses; }
443       private set {}
444     }
445
446   /**
447    * {@inheritDoc}
448    */
449   public MultiMap<string, string> web_service_addresses
450     {
451       get { return this._web_service_addresses; }
452       private set {}
453     }
454
455   /**
456    * The set of {@link Persona}s encapsulated by this Individual.
457    *
458    * No order is specified over the set of personas, as such an order may be
459    * different across each of the properties implemented by the personas (e.g.
460    * should they be ordered by presence, name, star sign, etc.?).
461    *
462    * Changing the set of personas may cause updates to the aggregated properties
463    * provided by the Individual, resulting in property notifications for them.
464    *
465    * Changing the set of personas will not cause permanent linking/unlinking of
466    * the added/removed personas to/from this Individual. To do that, call
467    * {@link IndividualAggregator.link_personas} or
468    * {@link IndividualAggregator.unlink_individual}, which will ensure the link
469    * changes are written to the appropriate backend.
470    *
471    * @since 0.5.1
472    */
473   public Set<Persona> personas
474     {
475       get { return this._persona_set_ro; }
476       set { this._set_personas (value, null); }
477     }
478
479   /**
480    * Emitted when one or more {@link Persona}s are added to or removed from
481    * the Individual. As the parameters are (unordered) sets, the orders of their
482    * elements are undefined.
483    *
484    * @param added a set of {@link Persona}s which have been added
485    * @param removed a set of {@link Persona}s which have been removed
486    *
487    * @since 0.5.1
488    */
489   public signal void personas_changed (Set<Persona> added,
490       Set<Persona> removed);
491
492   private void _notify_alias_cb (Object obj, ParamSpec ps)
493     {
494       this._update_alias ();
495     }
496
497   private void _notify_avatar_cb (Object obj, ParamSpec ps)
498     {
499       this._update_avatar ();
500     }
501
502   private void _notify_full_name_cb ()
503     {
504       this._update_full_name ();
505     }
506
507   private void _notify_structured_name_cb ()
508     {
509       this._update_structured_name ();
510     }
511
512   private void _notify_nickname_cb ()
513     {
514       this._update_nickname ();
515     }
516
517   private void _persona_group_changed_cb (string group, bool is_member)
518     {
519       this._update_groups ();
520     }
521
522   private void _notify_gender_cb ()
523     {
524       this._update_gender ();
525     }
526
527   private void _notify_urls_cb ()
528     {
529       this._update_urls ();
530     }
531
532   private void _notify_phone_numbers_cb ()
533     {
534       this._update_phone_numbers ();
535     }
536
537   private void _notify_postal_addresses_cb ()
538     {
539       this._update_postal_addresses ();
540     }
541
542   private void _notify_email_addresses_cb ()
543     {
544       this._update_email_addresses ();
545     }
546
547   private void _notify_roles_cb ()
548     {
549       this._update_roles ();
550     }
551
552   private void _notify_birthday_cb ()
553     {
554       this._update_birthday ();
555     }
556
557   private void _notify_notes_cb ()
558     {
559       this._update_notes ();
560     }
561
562   private void _notify_local_ids_cb ()
563     {
564       this._update_local_ids ();
565     }
566
567   /**
568    * Add or remove the Individual from the specified group.
569    *
570    * If `is_member` is `true`, the Individual will be added to the `group`. If
571    * it is `false`, they will be removed from the `group`.
572    *
573    * The group membership change will propagate to every {@link Persona} in
574    * the Individual.
575    *
576    * @param group a freeform group identifier
577    * @param is_member whether the Individual should be a member of the group
578    * @since 0.1.11
579    */
580   public async void change_group (string group, bool is_member)
581     {
582       foreach (var p in this._persona_set)
583         {
584           if (p is GroupDetails)
585             ((GroupDetails) p).change_group.begin (group, is_member);
586         }
587
588       /* don't notify, since it hasn't happened in the persona backing stores
589        * yet; react to that directly */
590     }
591
592   private void _notify_presence_cb (Object obj, ParamSpec ps)
593     {
594       this._update_presence ();
595     }
596
597   private void _notify_im_addresses_cb (Object obj, ParamSpec ps)
598     {
599       this._update_im_addresses ();
600     }
601
602   private void _notify_web_service_addresses_cb (Object obj, ParamSpec ps)
603     {
604       this._update_web_service_addresses ();
605     }
606
607   private void _notify_is_favourite_cb (Object obj, ParamSpec ps)
608     {
609       this._update_is_favourite ();
610     }
611
612   /**
613    * Create a new Individual.
614    *
615    * The Individual can optionally be seeded with the {@link Persona}s in
616    * `personas`. Otherwise, it will have to have personas added using the
617    * {@link Folks.Individual.personas} property after construction.
618    *
619    * @param personas a list of {@link Persona}s to initialise the
620    * {@link Individual} with, or `null`
621    * @return a new Individual
622    *
623    * @since 0.5.1
624    */
625   public Individual (Set<Persona>? personas)
626     {
627       this._im_addresses = new HashMultiMap<string, string> ();
628       this._web_service_addresses = new HashMultiMap<string, string> ();
629       this._persona_set =
630           new HashSet<Persona> (direct_hash, direct_equal);
631       this._persona_set_ro = this._persona_set.read_only_view;
632       this._stores = new HashMap<PersonaStore, uint> (null, null);
633       this._gender = Gender.UNSPECIFIED;
634       this._urls = new HashSet<FieldDetails> ();
635       this._phone_numbers = new HashSet<FieldDetails> ();
636       this._email_addresses = new HashSet<FieldDetails> ();
637       this._roles = new HashSet<Role>
638           ((GLib.HashFunc) Role.hash, (GLib.EqualFunc) Role.equal);
639       this._local_ids = new HashSet<string> ();
640       this._postal_addresses = new HashSet<PostalAddress> ();
641       this._notes = new HashSet<Note>
642           ((GLib.HashFunc) Note.hash, (GLib.EqualFunc) Note.equal);
643
644       this.personas = personas;
645     }
646
647   /* Emit the personas-changed signal, turning null parameters into empty sets
648    * and ensuring that the signal is emitted with read-only views of the sets
649    * so that signal handlers can't modify the sets. */
650   private void _emit_personas_changed (Set<Persona>? added,
651       Set<Persona>? removed)
652     {
653       var _added = added;
654       var _removed = removed;
655
656       if ((added == null || added.size == 0) &&
657           (removed == null || removed.size == 0))
658         {
659           /* Emitting it with no added or removed personas is pointless */
660           return;
661         }
662       else if (added == null)
663         {
664           _added = new HashSet<Persona> ();
665         }
666       else if (removed == null)
667         {
668           _removed = new HashSet<Persona> ();
669         }
670
671       this.personas_changed (_added.read_only_view, _removed.read_only_view);
672     }
673
674   private void _store_removed_cb (PersonaStore store)
675     {
676       var removed_personas = new HashSet<Persona> ();
677       var iter = this._persona_set.iterator ();
678       while (iter.next ())
679         {
680           var persona = iter.get ();
681
682           removed_personas.add (persona);
683           iter.remove ();
684         }
685
686       if (removed_personas != null)
687         this._emit_personas_changed (null, removed_personas);
688
689       if (store != null)
690         this._stores.unset (store);
691
692       if (this._persona_set.size < 1)
693         {
694           this.removed (null);
695           return;
696         }
697
698       this._update_fields ();
699     }
700
701   private void _store_personas_changed_cb (PersonaStore store,
702       Set<Persona> added,
703       Set<Persona> removed,
704       string? message,
705       Persona? actor,
706       GroupDetails.ChangeReason reason)
707     {
708       var removed_personas = new HashSet<Persona> ();
709       foreach (var p in removed)
710         {
711           if (this._persona_set.remove (p))
712             {
713               removed_personas.add (p);
714             }
715         }
716
717       if (removed_personas != null)
718         this._emit_personas_changed (null, removed_personas);
719
720       if (this._persona_set.size < 1)
721         {
722           this.removed (null);
723           return;
724         }
725
726       this._update_fields ();
727     }
728
729   private void _update_fields ()
730     {
731       this._update_groups ();
732       this._update_presence ();
733       this._update_is_favourite ();
734       this._update_avatar ();
735       this._update_alias ();
736       this._update_trust_level ();
737       this._update_im_addresses ();
738       this._update_web_service_addresses ();
739       this._update_structured_name ();
740       this._update_full_name ();
741       this._update_nickname ();
742       this._update_gender ();
743       this._update_urls ();
744       this._update_phone_numbers ();
745       this._update_email_addresses ();
746       this._update_roles ();
747       this._update_birthday ();
748       this._update_notes ();
749       this._update_postal_addresses ();
750       this._update_local_ids ();
751     }
752
753   private void _update_groups ()
754     {
755       var new_groups = new HashSet<string> ();
756
757       /* this._groups is null during initial construction */
758       if (this._groups == null)
759         this._groups = new HashSet<string> ();
760
761       /* FIXME: this should partition the personas by store (maybe we should
762        * keep that mapping in general in this class), and execute
763        * "groups-changed" on the store (with the set of personas), to allow the
764        * back-end to optimize it (like Telepathy will for MembersChanged for the
765        * groups channel list) */
766       foreach (var p in this._persona_set)
767         {
768           if (p is GroupDetails)
769             {
770               var persona = (GroupDetails) p;
771
772               foreach (var group in persona.groups)
773                 {
774                   new_groups.add (group);
775                 }
776             }
777         }
778
779       foreach (var group in new_groups)
780         {
781           if (!this._groups.contains (group))
782             {
783               this._groups.add (group);
784               foreach (var g in this._groups)
785                 {
786                   debug ("   %s", g);
787                 }
788
789               this.group_changed (group, true);
790             }
791         }
792
793       /* buffer the removals, so we don't remove while iterating */
794       var removes = new GLib.List<string> ();
795       foreach (var group in this._groups)
796         {
797           if (!new_groups.contains (group))
798             removes.prepend (group);
799         }
800
801       removes.foreach ((l) =>
802         {
803           unowned string group = (string) l;
804           this._groups.remove (group);
805           this.group_changed (group, false);
806         });
807     }
808
809   private void _update_presence ()
810     {
811       var presence_message = "";
812       var presence_status = "";
813       var presence_type = Folks.PresenceType.UNSET;
814
815       /* Choose the most available presence from our personas */
816       foreach (var p in this._persona_set)
817         {
818           if (p is PresenceDetails)
819             {
820               unowned PresenceDetails presence = (PresenceDetails) p;
821
822               if (PresenceDetails.typecmp (presence.presence_type,
823                   presence_type) > 0)
824                 {
825                   presence_type = presence.presence_type;
826                   presence_message = presence.presence_message;
827                   presence_status = presence.presence_status;
828                 }
829             }
830         }
831
832       if (presence_message == null)
833         presence_message = "";
834       if (presence_status == null)
835         presence_status = "";
836
837       /* only notify if the value has changed */
838       if (this.presence_message != presence_message)
839         this.presence_message = presence_message;
840
841       if (this.presence_type != presence_type)
842         this.presence_type = presence_type;
843
844       if (this.presence_status != presence_status)
845         this.presence_status = presence_status;
846     }
847
848   private void _update_is_favourite ()
849     {
850       var favourite = false;
851
852       debug ("Running _update_is_favourite() on '%s'", this.id);
853
854       foreach (var p in this._persona_set)
855         {
856           if (favourite == false && p is FavouriteDetails)
857             {
858               favourite = ((FavouriteDetails) p).is_favourite;
859               if (favourite == true)
860                 break;
861             }
862         }
863
864       /* Only notify if the value has changed. We have to set the private member
865        * and notify manually, or we'd end up propagating the new favourite
866        * status back down to all our Personas. */
867       if (this._is_favourite != favourite)
868         {
869           this._is_favourite = favourite;
870           this.notify_property ("is-favourite");
871         }
872     }
873
874   private void _update_alias ()
875     {
876       string alias = null;
877       var alias_is_display_id = false;
878
879       debug ("Updating alias for individual '%s'", this.id);
880
881       /* Search for an alias from a writeable Persona, and use it as our first
882        * choice if it's non-empty, since that's where the user-set alias is
883        * stored. */
884       foreach (var p in this._persona_set)
885         {
886           if (p is AliasDetails && p.store.is_writeable == true)
887             {
888               var a = (AliasDetails) p;
889
890               if (a.alias != null && a.alias.strip () != "")
891                 {
892                   alias = a.alias;
893                   break;
894                 }
895             }
896         }
897
898       debug ("    got alias '%s' from writeable personas", alias);
899
900       /* Since we can't find a non-empty alias from a writeable backend, try
901        * the aliases from other personas. Use a non-empty alias which isn't
902        * equal to the persona's display ID as our preference. If we can't find
903        * one of those, fall back to one which is equal to the display ID. */
904       if (alias == null)
905         {
906           foreach (var p in this._persona_set)
907             {
908               if (p is AliasDetails)
909                 {
910                   var a = (AliasDetails) p;
911
912                   if (a.alias == null || a.alias.strip () == "")
913                     continue;
914
915                   if (alias == null || alias_is_display_id == true)
916                     {
917                       /* We prefer to not have an alias which is the same as the
918                        * Persona's display-id, since having such an alias
919                        * implies that it's the default. However, we prefer using
920                        * such an alias to using the Persona's UID, which is our
921                        * ultimate fallback (below). */
922                       alias = a.alias;
923
924                       if (a.alias == p.display_id)
925                         alias_is_display_id = true;
926                       else if (alias != null)
927                         break;
928                     }
929                 }
930             }
931         }
932
933       debug ("    got alias '%s' from non-writeable personas", alias);
934
935       if (alias == null)
936         {
937           /* We have to pick a display ID, since none of the personas have an
938            * alias available. Pick the display ID from the first persona in the
939            * list. */
940           foreach (var persona in this._persona_set)
941             {
942               alias = persona.display_id;
943               debug ("No aliases available for individual; using display ID " +
944                   "instead: %s", alias);
945               break;
946             }
947         }
948
949       /* Only notify if the value has changed. We have to set the private member
950        * and notify manually, or we'd end up propagating the new alias back
951        * down to all our Personas, even if it's a fallback display ID or
952        * something else undesirable. */
953       if (this._alias != alias)
954         {
955           debug ("Changing alias of individual '%s' from '%s' to '%s'.",
956               this.id, this._alias, alias);
957           this._alias = alias;
958           this.notify_property ("alias");
959         }
960     }
961
962   private void _update_avatar ()
963     {
964       File avatar = null;
965
966       foreach (var p in this._persona_set)
967         {
968           if (p is AvatarDetails)
969             {
970               avatar = ((AvatarDetails) p).avatar;
971               if (avatar != null)
972                 break;
973             }
974         }
975
976       /* only notify if the value has changed */
977       if (this.avatar != avatar)
978         this.avatar = avatar;
979     }
980
981   private void _update_trust_level ()
982     {
983       var trust_level = TrustLevel.PERSONAS;
984
985       foreach (var p in this._persona_set)
986         {
987           if (p.is_user == false &&
988               p.store.trust_level == PersonaStoreTrust.NONE)
989             trust_level = TrustLevel.NONE;
990         }
991
992       /* Only notify if the value has changed */
993       if (this.trust_level != trust_level)
994         this.trust_level = trust_level;
995     }
996
997   private void _update_im_addresses ()
998     {
999       /* populate the IM addresses as the union of our Personas' addresses */
1000       this._im_addresses.clear ();
1001
1002       foreach (var persona in this._persona_set)
1003         {
1004           if (persona is ImDetails)
1005             {
1006               var im_details = (ImDetails) persona;
1007               foreach (var cur_protocol in im_details.im_addresses.get_keys ())
1008                 {
1009                   var cur_addresses =
1010                       im_details.im_addresses.get (cur_protocol);
1011
1012                   foreach (var address in cur_addresses)
1013                     {
1014                       this._im_addresses.set (cur_protocol, address);
1015                     }
1016                 }
1017             }
1018         }
1019       this.notify_property ("im-addresses");
1020     }
1021
1022   private void _update_web_service_addresses ()
1023     {
1024       /* populate the web service addresses as the union of our Personas' addresses */
1025       this._web_service_addresses.clear ();
1026
1027       foreach (var persona in this.personas)
1028         {
1029           if (persona is WebServiceDetails)
1030             {
1031               var web_service_details = (WebServiceDetails) persona;
1032               foreach (var cur_web_service in
1033                   web_service_details.web_service_addresses.get_keys ())
1034                 {
1035                   var cur_addresses =
1036                       web_service_details.web_service_addresses.get (
1037                           cur_web_service);
1038
1039                   foreach (var address in cur_addresses)
1040                     {
1041                       this._web_service_addresses.set (cur_web_service,
1042                           address);
1043                     }
1044                 }
1045             }
1046         }
1047       this.notify_property ("web-service-addresses");
1048     }
1049
1050   private void _connect_to_persona (Persona persona)
1051     {
1052       persona.notify["alias"].connect (this._notify_alias_cb);
1053       persona.notify["avatar"].connect (this._notify_avatar_cb);
1054       persona.notify["presence-message"].connect (this._notify_presence_cb);
1055       persona.notify["presence-type"].connect (this._notify_presence_cb);
1056       persona.notify["im-addresses"].connect (this._notify_im_addresses_cb);
1057       persona.notify["web-service-addresses"].connect
1058               (this._notify_web_service_addresses_cb);
1059       persona.notify["is-favourite"].connect (this._notify_is_favourite_cb);
1060       persona.notify["structured-name"].connect (
1061           this._notify_structured_name_cb);
1062       persona.notify["full-name"].connect (this._notify_full_name_cb);
1063       persona.notify["nickname"].connect (this._notify_nickname_cb);
1064       persona.notify["gender"].connect (this._notify_gender_cb);
1065       persona.notify["urls"].connect (this._notify_urls_cb);
1066       persona.notify["phone-numbers"].connect (this._notify_phone_numbers_cb);
1067       persona.notify["email-addresses"].connect (
1068           this._notify_email_addresses_cb);
1069       persona.notify["roles"].connect (this._notify_roles_cb);
1070       persona.notify["birthday"].connect (this._notify_birthday_cb);
1071       persona.notify["notes"].connect (this._notify_notes_cb);
1072       persona.notify["postal-addresses"].connect
1073           (this._notify_postal_addresses_cb);
1074       persona.notify["local-ids"].connect
1075           (this._notify_local_ids_cb);
1076
1077
1078       if (persona is GroupDetails)
1079         {
1080           ((GroupDetails) persona).group_changed.connect (
1081               this._persona_group_changed_cb);
1082         }
1083     }
1084
1085   private void _update_structured_name ()
1086     {
1087       bool name_found = false;
1088
1089       foreach (var persona in this._persona_set)
1090         {
1091           var name_details = persona as NameDetails;
1092           if (name_details != null)
1093             {
1094               var new_value = name_details.structured_name;
1095               if (new_value != null && !new_value.is_empty ())
1096                 {
1097                   name_found = true;
1098                   if (this.structured_name == null ||
1099                       !this.structured_name.equal (new_value))
1100                     {
1101                       this.structured_name = new_value;
1102                       return;
1103                     }
1104                 }
1105             }
1106         }
1107
1108       if (name_found == false)
1109         this.structured_name = null;
1110     }
1111
1112   private void _update_full_name ()
1113     {
1114       string? new_full_name = null;
1115
1116       foreach (var persona in this._persona_set)
1117         {
1118           var name_details = persona as NameDetails;
1119           if (name_details != null)
1120             {
1121               var new_value = name_details.full_name;
1122               if (new_value != null && new_value != "")
1123                 {
1124                   new_full_name = new_value;
1125                   break;
1126                 }
1127             }
1128         }
1129
1130       if (new_full_name != this.full_name)
1131         this.full_name = new_full_name;
1132     }
1133
1134   private void _update_nickname ()
1135     {
1136       string? new_nickname = null;
1137
1138       foreach (var persona in this._persona_set)
1139         {
1140           var name_details = persona as NameDetails;
1141           if (name_details != null)
1142             {
1143               var new_value = name_details.nickname;
1144               if (new_value != null && new_value != "")
1145                 {
1146                   new_nickname = new_value;
1147                   break;
1148                 }
1149             }
1150         }
1151
1152       if (new_nickname != this._nickname)
1153         {
1154           this._nickname = new_nickname;
1155           this.notify_property ("nickname");
1156         }
1157     }
1158
1159   private void _disconnect_from_persona (Persona persona)
1160     {
1161       persona.notify["alias"].disconnect (this._notify_alias_cb);
1162       persona.notify["avatar"].disconnect (this._notify_avatar_cb);
1163       persona.notify["presence-message"].disconnect (
1164           this._notify_presence_cb);
1165       persona.notify["presence-type"].disconnect (this._notify_presence_cb);
1166       persona.notify["im-addresses"].disconnect (
1167           this._notify_im_addresses_cb);
1168       persona.notify["web-service-addresses"].disconnect (
1169           this._notify_web_service_addresses_cb);
1170       persona.notify["is-favourite"].disconnect (
1171           this._notify_is_favourite_cb);
1172       persona.notify["structured-name"].disconnect (
1173           this._notify_structured_name_cb);
1174       persona.notify["full-name"].disconnect (this._notify_full_name_cb);
1175       persona.notify["nickname"].disconnect (this._notify_nickname_cb);
1176       persona.notify["gender"].disconnect (this._notify_gender_cb);
1177       persona.notify["urls"].disconnect (this._notify_urls_cb);
1178       persona.notify["phone-numbers"].disconnect (
1179           this._notify_phone_numbers_cb);
1180       persona.notify["email-addresses"].disconnect (
1181           this._notify_email_addresses_cb);
1182       persona.notify["roles"].disconnect (this._notify_roles_cb);
1183       persona.notify["birthday"].disconnect (this._notify_birthday_cb);
1184       persona.notify["notes"].disconnect (this._notify_notes_cb);
1185       persona.notify["postal-addresses"].disconnect
1186           (this._notify_postal_addresses_cb);
1187       persona.notify["local-ids"].disconnect (this._notify_local_ids_cb);
1188
1189
1190       if (persona is GroupDetails)
1191         {
1192           ((GroupDetails) persona).group_changed.disconnect (
1193               this._persona_group_changed_cb);
1194         }
1195     }
1196
1197   private void _update_gender ()
1198     {
1199       Gender new_gender = Gender.UNSPECIFIED;
1200
1201       foreach (var persona in this._persona_set)
1202         {
1203           var gender_details = persona as GenderDetails;
1204           if (gender_details != null)
1205             {
1206               var new_value = gender_details.gender;
1207               if (new_value != Gender.UNSPECIFIED)
1208                 {
1209                   new_gender = new_value;
1210                   break;
1211                 }
1212             }
1213         }
1214
1215       if (new_gender != this.gender)
1216         this.gender = new_gender;
1217     }
1218
1219   private void _update_urls ()
1220     {
1221       /* Populate the URLs as the union of our Personas' URLs.
1222        * If the same URL exists multiple times we merge the parameters. */
1223       var urls_set = new HashMap<unowned string, unowned FieldDetails> ();
1224
1225       this._urls.clear ();
1226
1227       foreach (var persona in this._persona_set)
1228         {
1229           var url_details = persona as UrlDetails;
1230           if (url_details != null)
1231             {
1232               foreach (var ps in url_details.urls)
1233                 {
1234                   if (ps.value == null)
1235                     continue;
1236
1237                   var existing = urls_set.get (ps.value);
1238                   if (existing != null)
1239                     existing.extend_parameters (ps.parameters);
1240                   else
1241                     {
1242                       var new_ps = new FieldDetails (ps.value);
1243                       new_ps.extend_parameters (ps.parameters);
1244                       urls_set.set (ps.value, new_ps);
1245                       this._urls.add (new_ps);
1246                     }
1247                 }
1248             }
1249         }
1250
1251       this.notify_property ("urls");
1252     }
1253
1254   private void _update_phone_numbers ()
1255     {
1256       /* Populate the phone numbers as the union of our Personas' numbers
1257        * If the same number exists multiple times we merge the parameters. */
1258       /* FIXME: We should handle phone numbers better, just string comparison
1259          doesn't work. */
1260       var phone_numbers_set =
1261           new HashMap<unowned string, unowned FieldDetails> ();
1262
1263       this._phone_numbers.clear ();
1264
1265       foreach (var persona in this._persona_set)
1266         {
1267           var phone_details = persona as PhoneDetails;
1268           if (phone_details != null)
1269             {
1270               foreach (var fd in phone_details.phone_numbers)
1271                 {
1272                   if (fd.value == null)
1273                     continue;
1274
1275                   var existing = phone_numbers_set.get (fd.value);
1276                   if (existing != null)
1277                     existing.extend_parameters (fd.parameters);
1278                   else
1279                     {
1280                       var new_fd = new FieldDetails (fd.value);
1281                       new_fd.extend_parameters (fd.parameters);
1282                       phone_numbers_set.set (fd.value, new_fd);
1283                       this._phone_numbers.add (new_fd);
1284                     }
1285                 }
1286             }
1287         }
1288
1289       this.notify_property ("phone-numbers");
1290     }
1291
1292   private void _update_email_addresses ()
1293     {
1294       /* Populate the email addresses as the union of our Personas' addresses.
1295        * If the same address exists multiple times we merge the parameters. */
1296       var emails_set = new HashMap<unowned string, unowned FieldDetails> ();
1297
1298       this._email_addresses.clear ();
1299
1300       foreach (var persona in this._persona_set)
1301         {
1302           var email_details = persona as EmailDetails;
1303           if (email_details != null)
1304             {
1305               foreach (var fd in email_details.email_addresses)
1306                 {
1307                   if (fd.value == null)
1308                     continue;
1309
1310                   var existing = emails_set.get (fd.value);
1311                   if (existing != null)
1312                     existing.extend_parameters (fd.parameters);
1313                   else
1314                     {
1315                       var new_fd = new FieldDetails (fd.value);
1316                       new_fd.extend_parameters (fd.parameters);
1317                       emails_set.set (fd.value, new_fd);
1318                       this._email_addresses.add (new_fd);
1319                     }
1320                 }
1321             }
1322         }
1323
1324       this.notify_property ("email-addresses");
1325     }
1326
1327   private void _update_roles ()
1328     {
1329       this._roles.clear ();
1330
1331       foreach (var persona in this._persona_set)
1332         {
1333           var role_details = persona as RoleDetails;
1334           if (role_details != null)
1335             {
1336               foreach (var r in role_details.roles)
1337                 {
1338                   this._roles.add (r);
1339                 }
1340             }
1341         }
1342
1343       this.notify_property ("roles");
1344     }
1345
1346   private void _update_local_ids ()
1347     {
1348       this._local_ids.clear ();
1349
1350       foreach (var persona in this._persona_set)
1351         {
1352           var local_ids_details = persona as LocalIdDetails;
1353           if (local_ids_details != null)
1354             {
1355               foreach (var id in local_ids_details.local_ids)
1356                 {
1357                   this._local_ids.add (id);
1358                 }
1359             }
1360         }
1361
1362       this.notify_property ("local-ids");
1363     }
1364
1365   private void _update_postal_addresses ()
1366     {
1367       this._postal_addresses.clear ();
1368
1369       /* FIXME: Detect duplicates somehow? */
1370       foreach (var persona in this._persona_set)
1371         {
1372           var address_details = persona as PostalAddressDetails;
1373           if (address_details != null)
1374             {
1375               foreach (var pa in address_details.postal_addresses)
1376                 this._postal_addresses.add (pa);
1377             }
1378         }
1379
1380       this.notify_property ("postal-addresses");
1381     }
1382
1383   private void _update_birthday ()
1384     {
1385       unowned DateTime bday = null;
1386       unowned string calendar_event_id = "";
1387
1388       foreach (var persona in this._persona_set)
1389         {
1390           var bday_owner = persona as BirthdayDetails;
1391           if (bday_owner != null)
1392             {
1393               if (bday_owner.birthday != null)
1394                 {
1395                   if (this.birthday == null ||
1396                       bday_owner.birthday.compare (this.birthday) != 0)
1397                     {
1398                       bday = bday_owner.birthday;
1399                       calendar_event_id = bday_owner.calendar_event_id;
1400                       break;
1401                     }
1402                 }
1403             }
1404         }
1405
1406       if (this.birthday != null && bday == null)
1407         {
1408           this.birthday = null;
1409           this.calendar_event_id = null;
1410         }
1411       else if (bday != null)
1412         {
1413           this.birthday = bday;
1414           this.calendar_event_id = calendar_event_id;
1415         }
1416     }
1417
1418   private void _update_notes ()
1419     {
1420       this._notes.clear ();
1421
1422       foreach (var persona in this._persona_set)
1423         {
1424           var note_details = persona as NoteDetails;
1425           if (note_details != null)
1426             {
1427               foreach (var n in note_details.notes)
1428                 {
1429                   this._notes.add (n);
1430                 }
1431             }
1432         }
1433
1434       this.notify_property ("notes");
1435     }
1436
1437   private void _set_personas (Set<Persona>? personas,
1438       Individual? replacement_individual)
1439     {
1440       var added = new HashSet<Persona> ();
1441       var removed = new HashSet<Persona> ();
1442
1443       /* Determine which Personas have been added. If personas == null, we
1444        * assume it's an empty set. */
1445       if (personas != null)
1446         {
1447           foreach (var p in personas)
1448             {
1449               if (!this._persona_set.contains (p))
1450                 {
1451                   /* Keep track of how many Personas are users */
1452                   if (p.is_user)
1453                     this._persona_user_count++;
1454
1455                   added.add (p);
1456
1457                   this._persona_set.add (p);
1458                   this._connect_to_persona (p);
1459
1460                   /* Increment the Persona count for this PersonaStore */
1461                   var store = p.store;
1462                   var num_from_store = this._stores.get (store);
1463                   if (num_from_store == 0)
1464                     {
1465                       this._stores.set (store, num_from_store + 1);
1466                     }
1467                   else
1468                     {
1469                       this._stores.set (store, 1);
1470
1471                       store.removed.connect (this._store_removed_cb);
1472                       store.personas_changed.connect (
1473                           this._store_personas_changed_cb);
1474                     }
1475                 }
1476             }
1477         }
1478
1479       /* Determine which Personas have been removed */
1480       var iter = this._persona_set.iterator ();
1481       while (iter.next ())
1482         {
1483           var p = iter.get ();
1484
1485           if (personas == null || !personas.contains (p))
1486             {
1487               /* Keep track of how many Personas are users */
1488               if (p.is_user)
1489                 this._persona_user_count--;
1490
1491               removed.add (p);
1492
1493               /* Decrement the Persona count for this PersonaStore */
1494               var store = p.store;
1495               var num_from_store = this._stores.get (store);
1496               if (num_from_store > 1)
1497                 {
1498                   this._stores.set (store, num_from_store - 1);
1499                 }
1500               else
1501                 {
1502                   store.removed.disconnect (this._store_removed_cb);
1503                   store.personas_changed.disconnect (
1504                       this._store_personas_changed_cb);
1505
1506                   this._stores.unset (store);
1507                 }
1508
1509               this._disconnect_from_persona (p);
1510               iter.remove ();
1511             }
1512         }
1513
1514       this._emit_personas_changed (added, removed);
1515
1516       /* Update this.is_user */
1517       var new_is_user = (this._persona_user_count > 0) ? true : false;
1518       if (new_is_user != this.is_user)
1519         this.is_user = new_is_user;
1520
1521       /* If all the Personas have been removed, remove the Individual */
1522       if (this._persona_set.size < 1)
1523         {
1524           this.removed (replacement_individual);
1525           return;
1526         }
1527
1528       /* Update the ID. We choose the most interesting Persona in the
1529        * Individual and hash their UID. This is guaranteed to be globally
1530        * unique, and may not change (for one of the two Individuals) if we link
1531        * two Individuals together, which is nice though we can't rely on this
1532        * behaviour.
1533        *
1534        * This method of constructing an ID ensures that it'll be unique and
1535        * stable for a given Individual once the IndividualAggregator reaches
1536        * a quiescent state after startup. It guarantees that the ID will be
1537        * the same every time folks is used, until the Individual is linked
1538        * or unlinked to another Individual.
1539        *
1540        * We choose the most interesting Persona by ranking all the Personas
1541        * in the Individual by:
1542        *  1. store.is-writeable
1543        *  2. store.trust-level
1544        *  3. store.id (alphabetically)
1545        *
1546        * Note that this heuristic shouldn't be changed without careful thought,
1547        * since stored references to IDs may be broken by the change.
1548        */
1549       if (this._persona_set.size > 0)
1550         {
1551           Persona? chosen_persona = null;
1552
1553           foreach (var persona in this._persona_set)
1554             {
1555               if (chosen_persona == null ||
1556                   (chosen_persona.store.is_writeable == false &&
1557                       persona.store.is_writeable == true) ||
1558                   (chosen_persona.store.is_writeable ==
1559                           persona.store.is_writeable &&
1560                       chosen_persona.store.trust_level >
1561                           persona.store.trust_level) ||
1562                   (chosen_persona.store.is_writeable ==
1563                           persona.store.is_writeable &&
1564                       chosen_persona.store.trust_level ==
1565                           persona.store.trust_level &&
1566                       chosen_persona.store.id > persona.store.id)
1567                  )
1568                {
1569                  chosen_persona = persona;
1570                }
1571             }
1572
1573           // Hash the chosen persona's UID
1574           this.id = Checksum.compute_for_string (ChecksumType.SHA1,
1575               chosen_persona.uid);
1576         }
1577
1578       /* Update our aggregated fields and notify the changes */
1579       this._update_fields ();
1580     }
1581
1582   internal void replace (Individual replacement_individual)
1583     {
1584       this._set_personas (null, replacement_individual);
1585     }
1586 }