15fa9be2f2f1592aaeb0435950d9d2a98be7e34b
[platform/upstream/folks.git] / backends / telepathy / lib / tpf-persona.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 TelepathyGLib;
24 using Folks;
25 #if HAVE_ZEITGEIST
26 using Zeitgeist;
27 #endif
28
29 /**
30  * A persona subclass which represents a single instant messaging contact from
31  * Telepathy.
32  *
33  * There is a one-to-one correspondence between {@link Tpf.Persona}s and
34  * {@link TelepathyGLib.Contact}s, although at any time the
35  * {@link Tpf.Persona.contact} property of a persona may be ``null`` if the
36  * contact's Telepathy connection isn't available (e.g. due to being offline).
37  * In this case, the persona's properties persist from a local cache.
38  */
39 public class Tpf.Persona : Folks.Persona,
40     AliasDetails,
41     AvatarDetails,
42     BirthdayDetails,
43     EmailDetails,
44     FavouriteDetails,
45     GroupDetails,
46     InteractionDetails,
47     ImDetails,
48     NameDetails,
49     PhoneDetails,
50     PresenceDetails,
51     UrlDetails
52 {
53   private const string[] _linkable_properties = { "im-addresses" };
54   private string[] _writeable_properties = null;
55
56   /* Whether we've finished being constructed; this is used to prevent
57    * unnecessary trips to the Telepathy service to tell it about properties
58    * being set which are actually just being set from data it's just given us.
59    */
60   private bool _is_constructed = false;
61
62   /**
63    * Whether the Persona is in the user's contact list.
64    *
65    * This will be true for most {@link Folks.Persona}s, but may not be true for
66    * personas where {@link Folks.Persona.is_user} is true. If it's false in
67    * this case, it means that the persona has been retrieved from the Telepathy
68    * connection, but has not been added to the user's contact list.
69    *
70    * @since 0.3.5
71    */
72   public bool is_in_contact_list { get; set; }
73
74   private LoadableIcon? _avatar = null;
75
76   /**
77    * An avatar for the Persona.
78    *
79    * See {@link Folks.AvatarDetails.avatar}.
80    *
81    * @since 0.6.0
82    */
83   [CCode (notify = false)]
84   public LoadableIcon? avatar
85     {
86       get { return this._avatar; }
87       set { this.change_avatar.begin (value); } /* not writeable */
88     }
89
90   /**
91    * {@inheritDoc}
92    *
93    * @since 0.6.4
94    */
95   [CCode (notify = false)]
96   public StructuredName? structured_name
97     {
98       get { return null; }
99       set { this.change_structured_name.begin (value); } /* not writeable */
100     }
101
102   private string _full_name = ""; /* must never be null */
103
104   /**
105    * {@inheritDoc}
106    *
107    * @since 0.6.4
108    */
109   [CCode (notify = false)]
110   public string full_name
111     {
112       get { return this._full_name; }
113       set { this.change_full_name.begin (value); }
114     }
115
116   /**
117    * {@inheritDoc}
118    *
119    * @since 0.6.4
120    */
121   public async void change_full_name (string full_name) throws PropertyError
122     {
123       var tpf_store = this.store as Tpf.PersonaStore;
124
125       if (full_name == this._full_name)
126         return;
127
128       if (this._is_constructed)
129         {
130           try
131             {
132               yield tpf_store.change_user_full_name (this, full_name);
133             }
134           catch (PersonaStoreError.INVALID_ARGUMENT e1)
135             {
136               throw new PropertyError.NOT_WRITEABLE (e1.message);
137             }
138           catch (PersonaStoreError.STORE_OFFLINE e2)
139             {
140               throw new PropertyError.UNKNOWN_ERROR (e2.message);
141             }
142           catch (PersonaStoreError e3)
143             {
144               throw new PropertyError.UNKNOWN_ERROR (e3.message);
145             }
146         }
147
148       /* the change will be notified when we receive changes to
149        * contact.contact_info */
150     }
151
152   /**
153    * {@inheritDoc}
154    *
155    * @since 0.6.4
156    */
157   [CCode (notify = false)]
158   public string nickname
159     {
160       get { return ""; }
161       set { this.change_nickname.begin (value); } /* not writeable */
162     }
163
164   /**
165    * {@inheritDoc}
166    *
167    * ContactInfo has no equivalent field, so this is unsupported.
168    *
169    * @since 0.6.4
170    */
171   [CCode (notify = false)]
172   public string? calendar_event_id
173     {
174       get { return null; } /* unsupported */
175       set { this.change_calendar_event_id.begin (value); } /* not writeable */
176     }
177
178   private DateTime? _birthday = null;
179   /**
180    * {@inheritDoc}
181    *
182    * @since 0.6.4
183    */
184   [CCode (notify = false)]
185   public DateTime? birthday
186     {
187       get { return this._birthday; }
188       set { this.change_birthday.begin (value); }
189     }
190
191   /**
192    * {@inheritDoc}
193    *
194    * @since 0.6.4
195    */
196   public async void change_birthday (DateTime? birthday) throws PropertyError
197     {
198       var tpf_store = this.store as Tpf.PersonaStore;
199
200       if (birthday != null && this._birthday != null &&
201           birthday.equal (this._birthday))
202         {
203           return;
204         }
205
206       if (this._is_constructed)
207         {
208           try
209             {
210               yield tpf_store.change_user_birthday (this, birthday);
211             }
212           catch (PersonaStoreError.INVALID_ARGUMENT e1)
213             {
214               throw new PropertyError.NOT_WRITEABLE (e1.message);
215             }
216           catch (PersonaStoreError.STORE_OFFLINE e2)
217             {
218               throw new PropertyError.UNKNOWN_ERROR (e2.message);
219             }
220           catch (PersonaStoreError e3)
221             {
222               throw new PropertyError.UNKNOWN_ERROR (e3.message);
223             }
224         }
225
226       /* the change will be notified when we receive changes to
227        * contact.contact_info */
228     }
229
230   /**
231    * The Persona's presence type.
232    *
233    * See {@link Folks.PresenceDetails.presence_type}.
234    */
235   public Folks.PresenceType presence_type { get; set; }
236
237   /**
238    * The Persona's presence status.
239    *
240    * See {@link Folks.PresenceDetails.presence_status}.
241    *
242    * @since 0.6.0
243    */
244   public string presence_status { get; set; }
245
246   /**
247    * The Persona's presence message.
248    *
249    * See {@link Folks.PresenceDetails.presence_message}.
250    */
251   public string presence_message { get; set; }
252
253   /**
254    * The names of the Persona's linkable properties.
255    *
256    * See {@link Folks.Persona.linkable_properties}.
257    */
258   public override string[] linkable_properties
259     {
260       get { return Tpf.Persona._linkable_properties; }
261     }
262
263   /**
264    * {@inheritDoc}
265    *
266    * @since 0.6.0
267    */
268   public override string[] writeable_properties
269     {
270       get { return this._writeable_properties; }
271     }
272
273   private string _alias = ""; /* must never be null */
274
275   /**
276    * An alias for the Persona.
277    *
278    * See {@link Folks.AliasDetails.alias}.
279    */
280   [CCode (notify = false)]
281   public string alias
282     {
283       get { return this._alias; }
284       set { this.change_alias.begin (value); }
285     }
286
287   /**
288    * {@inheritDoc}
289    *
290    * @since 0.6.2
291    */
292   public async void change_alias (string alias) throws PropertyError
293     {
294       if (this._alias == alias)
295         {
296           return;
297         }
298
299       if (this._is_constructed)
300         {
301           yield ((Tpf.PersonaStore) this.store).change_alias (this, alias);
302         }
303
304       /* The change will be notified when we receive changes from the store. */
305     }
306
307   private bool _is_favourite = false;
308
309   /**
310    * Whether this Persona is a user-defined favourite.
311    *
312    * See {@link Folks.FavouriteDetails.is_favourite}.
313    */
314   [CCode (notify = false)]
315   public bool is_favourite
316     {
317       get { return this._is_favourite; }
318       set { this.change_is_favourite.begin (value); }
319     }
320
321   /**
322    * {@inheritDoc}
323    *
324    * @since 0.6.2
325    */
326   public async void change_is_favourite (bool is_favourite) throws PropertyError
327     {
328       if (this._is_favourite == is_favourite)
329         {
330           return;
331         }
332
333       if (this._is_constructed)
334         {
335           yield ((Tpf.PersonaStore) this.store).change_is_favourite (this,
336               is_favourite);
337         }
338
339       /* The change will be notified when we receive changes from the store. */
340     }
341
342   /* Note: Only ever called by Tpf.PersonaStore. */
343   internal void _set_is_favourite (bool is_favourite)
344     {
345       if (this._is_favourite == is_favourite)
346         {
347           return;
348         }
349
350       this._is_favourite = is_favourite;
351       this.notify_property ("is-favourite");
352
353       /* Mark the cache as needing to be updated. */
354       ((Tpf.PersonaStore) this.store)._set_cache_needs_update ();
355     }
356
357   private HashSet<EmailFieldDetails>? _email_addresses = null;
358   private Set<EmailFieldDetails>? _email_addresses_ro = null;
359
360   /**
361    * {@inheritDoc}
362    *
363    * @since 0.6.4
364    */
365   [CCode (notify = false)]
366   public Set<EmailFieldDetails> email_addresses
367     {
368       get
369         {
370           this._contact_notify_contact_info (true, false);
371           return this._email_addresses_ro;
372         }
373       set { this.change_email_addresses.begin (value); }
374     }
375
376   /**
377    * {@inheritDoc}
378    *
379    * @since 0.6.4
380    */
381   public async void change_email_addresses (
382       Set<EmailFieldDetails> email_addresses) throws PropertyError
383     {
384       yield this._change_details<EmailFieldDetails> (email_addresses,
385           this._email_addresses, "email");
386     }
387
388   /* NOTE: Other properties support lazy initialisation, but im-addresses
389    * doesn't as it's a linkable property, so always has to be loaded anyway. */
390   private HashMultiMap<string, ImFieldDetails> _im_addresses =
391       new HashMultiMap<string, ImFieldDetails> (null, null,
392           (Gee.HashDataFunc) AbstractFieldDetails<string>.hash_static,
393           (Gee.EqualDataFunc) AbstractFieldDetails<string>.equal_static);
394
395   /**
396    * A mapping of IM protocol to an (unordered) set of IM addresses.
397    *
398    * See {@link Folks.ImDetails.im_addresses}.
399    */
400   [CCode (notify = false)]
401   public MultiMap<string, ImFieldDetails> im_addresses
402     {
403       get { return this._im_addresses; }
404       set { this.change_im_addresses.begin (value); }
405     }
406
407   private uint _im_interaction_count = 0;
408
409   /**
410    * A counter for IM interactions (send/receive message) with the persona.
411    *
412    * See {@link Folks.InteractionDetails.im_interaction_count}
413    *
414    * @since 0.7.1
415    */
416   public uint im_interaction_count
417     {
418       get { return this._im_interaction_count; }
419     }
420
421   internal DateTime? _last_im_interaction_datetime = null;
422
423   /**
424    * The latest datetime for IM interactions (send/receive message) with the
425    * persona.
426    *
427    * See {@link Folks.InteractionDetails.last_im_interaction_datetime}
428    *
429    * @since 0.7.1
430    */
431   public DateTime? last_im_interaction_datetime
432     {
433       get { return this._last_im_interaction_datetime; }
434     }
435
436   private uint _call_interaction_count = 0;
437
438   /**
439    * A counter for call interactions (only successful calls) with the persona.
440    *
441    * See {@link Folks.InteractionDetails.call_interaction_count}
442    *
443    * @since 0.7.1
444    */
445   public uint call_interaction_count
446     {
447       get { return this._call_interaction_count; }
448     }
449
450   internal DateTime? _last_call_interaction_datetime = null;
451
452   /**
453    * The latest datetime for call interactions (only successful calls) with the
454    * persona.
455    *
456    * See {@link Folks.InteractionDetails.last_call_interaction_datetime}
457    *
458    * @since 0.7.1
459    */
460   public DateTime? last_call_interaction_datetime
461     {
462       get { return this._last_call_interaction_datetime; }
463     }
464
465   private HashSet<string> _groups = new HashSet<string> ();
466   private Set<string> _groups_ro;
467
468   /**
469    * A set group IDs for the groups the contact is a member of.
470    *
471    * See {@link Folks.GroupDetails.groups}.
472    */
473   [CCode (notify = false)]
474   public Set<string> groups
475     {
476       get { return this._groups_ro; }
477       set { this.change_groups.begin (value); }
478     }
479
480   /**
481    * Add or remove the Persona from the specified group.
482    *
483    * See {@link Folks.GroupDetails.change_group}.
484    *
485    * @throws Folks.PropertyError.UNKNOWN_ERROR if changing group membership
486    * failed
487    */
488   public async void change_group (string group, bool is_member)
489       throws GLib.Error
490     {
491       /* Ensure we have a strong ref to the contact for the duration of the
492        * operation. */
493       var contact = (Contact?) this._contact.get ();
494
495       if (contact == null)
496         {
497           /* The Tpf.Persona is being served out of the cache. */
498           throw new PropertyError.UNAVAILABLE (
499               _("Failed to change group membership: %s"),
500               /* Translators: "account" refers to an instant messaging
501                * account. */
502               _("Account is offline."));
503         }
504
505       try
506         {
507           if (is_member && !this._groups.contains (group))
508             {
509               yield contact.add_to_group_async (group);
510             }
511           else if (!is_member && this._groups.contains (group))
512             {
513               yield contact.remove_from_group_async (group);
514             }
515         }
516       catch (GLib.Error e)
517         {
518           /* Translators: the parameter is an error message. */
519           throw new PropertyError.UNKNOWN_ERROR (
520               _("Failed to change group membership: %s"), e.message);
521         }
522
523       /* The change will be notified when we receive changes from the store. */
524     }
525
526   /* Note: Only ever called as a result of signals from Telepathy. */
527   private void _contact_groups_changed (string[] added, string[] removed)
528     {
529       var changed = false;
530
531       foreach (var group in added)
532         {
533           if (this._groups.add (group) == true)
534             {
535               changed = true;
536               this.group_changed (group, true);
537             }
538         }
539
540       foreach (var group in removed)
541         {
542           if (this._groups.remove (group) == true)
543             {
544               changed = true;
545               this.group_changed (group, false);
546             }
547         }
548
549       /* Notify if anything changed. */
550       if (changed == true)
551         {
552           this.notify_property ("groups");
553
554           /* Mark the cache as needing to be updated. */
555           ((Tpf.PersonaStore) this.store)._set_cache_needs_update ();
556         }
557     }
558
559   /**
560    * {@inheritDoc}
561    *
562    * @since 0.6.2
563    */
564   public async void change_groups (Set<string> groups) throws PropertyError
565     {
566       var contact = (Contact?) this._contact.get ();
567
568       if (contact == null)
569         {
570           /* The Tpf.Persona is being served out of the cache. */
571           throw new PropertyError.UNAVAILABLE (
572               _("Failed to change group membership: %s"),
573               /* Translators: "account" refers to an instant messaging
574                * account. */
575               _("Account is offline."));
576         }
577
578       try
579         {
580           yield contact.set_contact_groups_async (groups.to_array ());
581         }
582       catch (GLib.Error e)
583         {
584           /* Translators: the parameter is an error message. */
585           throw new PropertyError.UNKNOWN_ERROR (
586               _("Failed to change group membership: %s"), e.message);
587         }
588
589       /* The change will be notified when we receive changes from the store. */
590     }
591
592   /* This has to be weak since, in general, we can't force any TpContacts to
593    * remain alive if we want to solve bgo#665376.
594    * As per bgo#680335, we have to use a WeakRef rather than a
595    * â€˜weak Contact?’ to avoid races when clearing the pointer. We still have
596    * to use a weak ref. notifier as well, though, in order to be able to emit
597    * a property change notification for ::contact.
598    *
599    * FIXME: Once bgo#554344 is fixed, _contact could be changed back to
600    * being a 'weak Contact?', assuming Vala implements weak references using
601    * GWeakRef. */
602   private GLib.WeakRef _contact = GLib.WeakRef (null);
603
604   private void _contact_weak_notify_cb (Object obj)
605     {
606       debug ("TpContact %p destroyed; setting ._contact = null in Persona %p",
607           obj, this);
608       /* _contact is cleared automatically as it's a WeakRef. */
609       this.notify_property ("contact");
610     }
611
612   /**
613    * The Telepathy contact represented by this persona.
614    *
615    * Note that this may be ``null`` if the {@link PersonaStore} providing this
616    * {@link Persona} isn't currently available (e.g. due to not being connected
617    * to the network). In this case, most other properties of the {@link Persona}
618    * are being retrieved from a cache and may not be current (though there's no
619    * way to tell this).
620    */
621   public Contact? contact
622     {
623       get
624         {
625           /* FIXME: This property should be changed to transfer its reference
626            * when the API is next broken. This is necessary because the
627            * TpfPersona doesn't hold a strong ref to the TpContact, so any
628            * pointer which is returned might be invalidated before reaching the
629            * caller. Probably not a problem in practice since folks won't be
630            * run multi-threaded. */
631           Contact? contact = (Contact?) this._contact.get ();
632           if (contact == null)
633             {
634               return null;
635             }
636
637           /* FIXME: I'm so very, very sorry. This is to cause Vala to forget
638            * we have a strong ref on 'contact' and not transfer it out. */
639           return (Contact) ((void*) contact);
640         }
641
642       construct
643         {
644           if (value != null)
645             {
646               value.weak_ref (this._contact_weak_notify_cb);
647             }
648
649           this._contact.set (value);
650         }
651     }
652
653   private HashSet<PhoneFieldDetails>? _phone_numbers = null;
654   private Set<PhoneFieldDetails>? _phone_numbers_ro = null;
655
656   /**
657    * {@inheritDoc}
658    *
659    * @since 0.6.4
660    */
661   [CCode (notify = false)]
662   public Set<PhoneFieldDetails> phone_numbers
663     {
664       get
665         {
666           this._contact_notify_contact_info (true, false);
667           return this._phone_numbers_ro;
668         }
669       set { this.change_phone_numbers.begin (value); }
670     }
671
672   /**
673    * {@inheritDoc}
674    *
675    * @since 0.6.4
676    */
677   public async void change_phone_numbers (
678       Set<PhoneFieldDetails> phone_numbers) throws PropertyError
679     {
680       yield this._change_details<PhoneFieldDetails> (phone_numbers,
681           this._phone_numbers, "tel");
682     }
683
684   private HashSet<UrlFieldDetails>? _urls = null;
685   private Set<UrlFieldDetails>? _urls_ro = null;
686
687   /**
688    * {@inheritDoc}
689    *
690    * @since 0.6.4
691    */
692   [CCode (notify = false)]
693   public Set<UrlFieldDetails> urls
694     {
695       get
696         {
697           this._contact_notify_contact_info (true, false);
698           return this._urls_ro;
699         }
700       set { this.change_urls.begin (value); }
701     }
702
703   /**
704    * {@inheritDoc}
705    *
706    * @since 0.6.4
707    */
708   public async void change_urls (Set<UrlFieldDetails> urls) throws PropertyError
709     {
710       yield this._change_details<UrlFieldDetails> (urls,
711           this._urls, "url");
712     }
713
714   private async void _change_details<T> (
715       Set<AbstractFieldDetails<string>> details,
716       Set<AbstractFieldDetails<string>>? member_set,
717       string field_name)
718         throws PropertyError
719     {
720       var tpf_store = this.store as Tpf.PersonaStore;
721
722       if (member_set != null &&
723           Folks.Internal.equal_sets<T> (details, member_set))
724         {
725           return;
726         }
727
728       if (this._is_constructed)
729         {
730           try
731             {
732               yield tpf_store._change_user_details (this, details, field_name);
733             }
734           catch (PersonaStoreError.INVALID_ARGUMENT e1)
735             {
736               throw new PropertyError.NOT_WRITEABLE (e1.message);
737             }
738           catch (PersonaStoreError.STORE_OFFLINE e2)
739             {
740               throw new PropertyError.UNKNOWN_ERROR (e2.message);
741             }
742           catch (PersonaStoreError e3)
743             {
744               throw new PropertyError.UNKNOWN_ERROR (e3.message);
745             }
746         }
747
748       /* the change will be notified when we receive changes to
749        * contact.contact_info */
750     }
751
752   /**
753    * Create a new persona.
754    *
755    * Create a new persona for the {@link PersonaStore} ``store``, representing
756    * the Telepathy contact given by ``contact``.
757    *
758    * @param contact the Telepathy contact being represented by the persona
759    * @param store the persona store to place the persona in
760    */
761   public Persona (Contact contact, PersonaStore store)
762     {
763       unowned string id = contact.get_identifier ();
764       var connection = contact.connection;
765       var account = connection.get_account ();
766       var uid = Folks.Persona.build_uid (store.type_id, store.id, id);
767
768       Object (contact: contact,
769               display_id: id,
770               /* FIXME: This IID format should be moved out to the ImDetails
771                * interface along with the code in
772                * Kf.Persona.linkable_property_to_links(), but that depends on
773                * bgo#624842 being fixed. */
774               iid: account.get_protocol () + ":" + id,
775               uid: uid,
776               store: store,
777               is_user: contact.handle == connection.self_handle);
778
779       debug ("Created new Tpf.Persona '%s' for service-specific UID '%s': %p",
780           uid, id, this);
781     }
782
783   construct
784     {
785       this._groups_ro = this._groups.read_only_view;
786
787       /* Contact can be null if we've been created from the cache. All the code
788        * below this point is for non-cached personas. */
789       var contact = (Contact?) this._contact.get ();
790
791       if (contact == null)
792         {
793           return;
794         }
795
796       /* Set our alias. */
797       this._alias = contact.get_alias ();
798
799       contact.notify["alias"].connect ((s, p) =>
800           {
801             var c = (Contact?) this._contact.get ();
802             assert (c != null); /* should never be called while cached */
803
804             /* Tp guarantees that aliases are always non-null. */
805             assert (c.alias != null);
806
807             if (this._alias != c.alias)
808               {
809                 this._alias = c.alias;
810                 this.notify_property ("alias");
811
812                 /* Mark the cache as needing to be updated. */
813                 ((Tpf.PersonaStore) this.store)._set_cache_needs_update ();
814               }
815           });
816
817       /* Set our single IM address */
818       var connection = contact.connection;
819       var account = connection.get_account ();
820
821       try
822         {
823           var im_addr = ImDetails.normalise_im_address (this.display_id,
824               account.get_protocol ());
825           var im_fd = new ImFieldDetails (im_addr);
826           this._im_addresses.set (account.get_protocol (), im_fd);
827         }
828       catch (ImDetailsError e)
829         {
830           /* This should never happen…but if it does, warn of it and continue */
831           warning (e.message);
832         }
833
834       contact.notify["avatar-file"].connect ((s, p) =>
835         {
836           this._contact_notify_avatar ();
837         });
838       this._contact_notify_avatar ();
839
840       contact.notify["presence-message"].connect ((s, p) =>
841         {
842           this._contact_notify_presence_message ();
843         });
844       contact.notify["presence-type"].connect ((s, p) =>
845         {
846           this._contact_notify_presence_type ();
847         });
848       contact.notify["presence-status"].connect ((s, p) =>
849         {
850           this._contact_notify_presence_status ();
851         });
852       this._contact_notify_presence_message ();
853       this._contact_notify_presence_type ();
854       this._contact_notify_presence_status ();
855
856       contact.notify["contact-info"].connect ((s, p) =>
857         {
858           this._contact_notify_contact_info (false);
859         });
860       this._contact_notify_contact_info (false);
861
862       contact.contact_groups_changed.connect ((added, removed) =>
863         {
864           this._contact_groups_changed (added, removed);
865         });
866       this._contact_groups_changed (contact.get_contact_groups (), {});
867
868       var tpf_store = this.store as Tpf.PersonaStore;
869
870       if (this.is_user)
871         {
872           tpf_store.notify["supported-fields"].connect ((s, p) =>
873             {
874               this._update_writeable_properties ();
875             });
876         }
877
878       tpf_store.notify["always-writeable-properties"].connect ((s, p) =>
879         {
880           this._update_writeable_properties ();
881         });
882
883       this._update_writeable_properties ();
884     }
885
886   /* Called after all construction-time properties have been set. */
887   public override void constructed ()
888     {
889       this._is_constructed = true;
890     }
891
892   private void _update_writeable_properties ()
893     {
894       var tpf_store = this.store as Tpf.PersonaStore;
895       this._writeable_properties = this.store.always_writeable_properties;
896
897       if (this.is_user)
898         {
899           if ("bday" in tpf_store.supported_fields)
900             this._writeable_properties += "birthday";
901           if ("email" in tpf_store.supported_fields)
902             this._writeable_properties += "email-addresses";
903           if ("fn" in tpf_store.supported_fields)
904             this._writeable_properties += "full-name";
905           if ("tel" in tpf_store.supported_fields)
906             this._writeable_properties += "phone-numbers";
907           if ("url" in tpf_store.supported_fields)
908             this._writeable_properties += "urls";
909         }
910     }
911
912   private void _contact_notify_contact_info (bool create_if_not_exists, bool emit_notification = true)
913     {
914       assert ((
915           (this._email_addresses == null) &&
916           (this._phone_numbers == null) &&
917           (this._urls == null)
918         ) || (
919           (this._email_addresses != null) &&
920           (this._phone_numbers != null) &&
921           (this._urls != null)
922         ));
923
924       /* See the comments in Folks.Individual about the lazy instantiation
925        * strategy for URIs, etc.
926        *
927        * It's necessary to notify for all three properties here, as this
928        * function is called identically for all of them. */
929       if (this._urls == null && create_if_not_exists == false)
930         {
931           if (emit_notification)
932             {
933               this.notify_property ("email-addresses");
934               this.notify_property ("phone-numbers");
935               this.notify_property ("urls");
936             }
937           return;
938         }
939       else if (this._urls == null)
940         {
941           this._urls = new HashSet<UrlFieldDetails> (
942               AbstractFieldDetails<string>.hash_static,
943               AbstractFieldDetails<string>.equal_static);
944           this._urls_ro = this._urls.read_only_view;
945
946           this._email_addresses = new HashSet<EmailFieldDetails> (
947               AbstractFieldDetails<string>.hash_static,
948               AbstractFieldDetails<string>.equal_static);
949           this._email_addresses_ro = this._email_addresses.read_only_view;
950
951           this._phone_numbers = new HashSet<PhoneFieldDetails> (
952               AbstractFieldDetails<string>.hash_static,
953               AbstractFieldDetails<string>.equal_static);
954           this._phone_numbers_ro = this._phone_numbers.read_only_view;
955         }
956
957       var contact = (Contact?) this._contact.get ();
958       if (contact == null)
959         {
960           /* If operating from the cache, bail out early. */
961           return;
962         }
963
964       var changed = false;
965       var new_birthday_str = "";
966       var new_full_name = "";
967       var new_email_addresses = new HashSet<EmailFieldDetails> (
968           AbstractFieldDetails<string>.hash_static,
969           AbstractFieldDetails<string>.equal_static);
970       var new_phone_numbers = new HashSet<PhoneFieldDetails> (
971            AbstractFieldDetails<string>.hash_static,
972            AbstractFieldDetails<string>.equal_static);
973       var new_urls = new HashSet<UrlFieldDetails> (
974            AbstractFieldDetails<string>.hash_static,
975            AbstractFieldDetails<string>.equal_static);
976
977       var contact_info = contact.get_contact_info ();
978       foreach (var info in contact_info)
979         {
980           if (info.field_name == "") {}
981           else if (info.field_name == "bday")
982             {
983               new_birthday_str = info.field_value[0] ?? "";
984             }
985           else if (info.field_name == "email")
986             {
987               foreach (var email_addr in info.field_value)
988                 {
989                   if (email_addr != "")
990                     {
991                       var parameters = this._afd_params_from_strv (info.parameters);
992                       var email_fd = new EmailFieldDetails (email_addr, parameters);
993                       new_email_addresses.add (email_fd);
994                     }
995                 }
996             }
997           else if (info.field_name == "fn")
998             {
999               new_full_name = info.field_value[0];
1000               if (new_full_name == null)
1001                 new_full_name = "";
1002             }
1003           else if (info.field_name == "tel")
1004             {
1005               foreach (var phone_num in info.field_value)
1006                 {
1007                   if (phone_num != "")
1008                     {
1009                       var parameters = this._afd_params_from_strv (info.parameters);
1010                       var phone_fd = new PhoneFieldDetails (phone_num, parameters);
1011                       new_phone_numbers.add (phone_fd);
1012                     }
1013                 }
1014             }
1015           else if (info.field_name == "url")
1016             {
1017               foreach (var url in info.field_value)
1018                 {
1019                   if (url != "")
1020                     {
1021                       var parameters = this._afd_params_from_strv (info.parameters);
1022                       var url_fd = new UrlFieldDetails (url, parameters);
1023                       new_urls.add (url_fd);
1024                     }
1025                 }
1026             }
1027         }
1028
1029       if (new_birthday_str != "")
1030         {
1031           var timeval = TimeVal ();
1032           if (timeval.from_iso8601 (new_birthday_str))
1033             {
1034               var d = new DateTime.from_timeval_utc (timeval);
1035               if (this._birthday == null ||
1036                   (this._birthday != null &&
1037                     !this._birthday.equal (d.to_utc ())))
1038                 {
1039                   this._birthday = d.to_utc ();
1040                   if (emit_notification)
1041                     {
1042                       this.notify_property ("birthday");
1043                     }
1044                   changed = true;
1045                 }
1046             }
1047           else
1048             {
1049               debug ("Failed to parse new birthday string '%s'",
1050                   new_birthday_str);
1051             }
1052         }
1053       else
1054         {
1055           if (this._birthday != null)
1056             {
1057               this._birthday = null;
1058               if (emit_notification)
1059                 {
1060                   this.notify_property ("birthday");
1061                 }
1062               changed = true;
1063             }
1064         }
1065
1066       if (!Folks.Internal.equal_sets<EmailFieldDetails> (new_email_addresses,
1067               this._email_addresses))
1068         {
1069           this._email_addresses = new_email_addresses;
1070           this._email_addresses_ro = new_email_addresses.read_only_view;
1071           if (emit_notification)
1072             {
1073               this.notify_property ("email-addresses");
1074             }
1075           changed = true;
1076         }
1077
1078       if (new_full_name != this._full_name)
1079         {
1080           this._full_name = new_full_name;
1081           this.notify_property ("full-name");
1082           changed = true;
1083         }
1084
1085       if (!Folks.Internal.equal_sets<PhoneFieldDetails> (new_phone_numbers,
1086               this._phone_numbers))
1087         {
1088           this._phone_numbers = new_phone_numbers;
1089           this._phone_numbers_ro = new_phone_numbers.read_only_view;
1090           if (emit_notification)
1091             {
1092               this.notify_property ("phone-numbers");
1093             }
1094           changed = true;
1095         }
1096
1097       if (!Folks.Internal.equal_sets<UrlFieldDetails> (new_urls, this._urls))
1098         {
1099           this._urls = new_urls;
1100           this._urls_ro = new_urls.read_only_view;
1101           this.notify_property ("urls");
1102           changed = true;
1103         }
1104
1105       if (changed == true)
1106         {
1107           /* Mark the cache as needing to be updated. */
1108           ((Tpf.PersonaStore) this.store)._set_cache_needs_update ();
1109         }
1110     }
1111
1112   private MultiMap<string, string> _afd_params_from_strv (string[] parameters)
1113     {
1114       var retval = new HashMultiMap<string, string> ();
1115
1116       foreach (var entry in parameters)
1117         {
1118           var tokens = entry.split ("=", 2);
1119           if (tokens.length == 2)
1120             {
1121               retval.set (tokens[0], tokens[1]);
1122             }
1123           else
1124             {
1125               warning ("Failed to parse vCard parameter from string '%s'",
1126                   entry);
1127             }
1128         }
1129
1130       return retval;
1131     }
1132
1133   /**
1134    * Create a new persona for the {@link PersonaStore} ``store``, representing
1135    * a cached contact for which we currently have no Telepathy contact.
1136    *
1137    * @param store The persona store to place the persona in.
1138    * @param uid The cached UID of the persona.
1139    * @param iid The cached IID of the persona.
1140    * @param im_address The cached IM address of the persona (excluding
1141    * protocol).
1142    * @param protocol The cached protocol of the persona.
1143    * @param groups The cached set of groups the persona is in.
1144    * @param is_favourite Whether the persona is a favourite.
1145    * @param alias The cached alias for the persona.
1146    * @param is_in_contact_list Whether the persona is in the user's contact
1147    * list.
1148    * @param is_user Whether the persona is the user.
1149    * @param avatar The icon for the persona's cached avatar, or ``null`` if they
1150    * have no avatar.
1151    * @param birthday The date/time of birth of the persona, or ``null`` if it's
1152    * unknown.
1153    * @param full_name The persona's full name, or the empty string if it's
1154    * unknown.
1155    * @param email_addresses A set of the persona's e-mail addresses, which may
1156    * be empty (but may not be ``null``).
1157    * @param phone_numbers A set of the persona's phone numbers, which may be
1158    * empty (but may not be ``null``).
1159    * @param urls A set of the persona's URLs, which may be empty (but may not be
1160    * ``null``).
1161    * @return A new {@link Tpf.Persona} representing the cached persona.
1162    *
1163    * @since 0.6.0
1164    */
1165   internal Persona.from_cache (PersonaStore store, string uid, string iid,
1166       string im_address, string protocol, HashSet<string> groups,
1167       bool is_favourite, string alias, bool is_in_contact_list, bool is_user,
1168       LoadableIcon? avatar, DateTime? birthday, string full_name,
1169       HashSet<EmailFieldDetails> email_addresses,
1170       HashSet<PhoneFieldDetails> phone_numbers, HashSet<UrlFieldDetails> urls)
1171     {
1172       Object (contact: null,
1173               display_id: im_address,
1174               iid: iid,
1175               uid: uid,
1176               store: store,
1177               is_user: is_user);
1178
1179       debug ("Created new Tpf.Persona '%s' from cache: %p", uid, this);
1180
1181       // IM addresses
1182       var im_fd = new ImFieldDetails (im_address);
1183       this._im_addresses.set (protocol, im_fd);
1184
1185       // Groups
1186       this._groups = groups;
1187       this._groups_ro = this._groups.read_only_view;
1188
1189       // E-mail addresses
1190       this._email_addresses = email_addresses;
1191       this._email_addresses_ro = this._email_addresses.read_only_view;
1192
1193       // Phone numbers
1194       this._phone_numbers = phone_numbers;
1195       this._phone_numbers_ro = this._phone_numbers.read_only_view;
1196
1197       // URLs
1198       this._urls = urls;
1199       this._urls_ro = this._urls.read_only_view;
1200
1201       // Other properties
1202       if (alias == null)
1203         {
1204           /* Deal with badly-behaved callers */
1205           alias = "";
1206         }
1207
1208       if (full_name == null)
1209         {
1210           /* Deal with badly-behaved callers */
1211           full_name = "";
1212         }
1213
1214       this._alias = alias;
1215       this._is_favourite = is_favourite;
1216       this.is_in_contact_list = is_in_contact_list;
1217       this._birthday = birthday;
1218       this._full_name = full_name;
1219
1220       // Avatars
1221       this._avatar = avatar;
1222       var avatar_file =
1223           (avatar != null) ? ((FileIcon) avatar).get_file () : null;
1224       ((Tpf.PersonaStore) store)._update_avatar_cache (iid, avatar_file);
1225
1226       // Make the persona appear offline
1227       this.presence_type = PresenceType.OFFLINE;
1228       this.presence_message = "";
1229       this.presence_status = "offline";
1230
1231       this._writeable_properties = {};
1232     }
1233
1234   ~Persona ()
1235     {
1236       debug ("Destroying Tpf.Persona '%s': %p", this.uid, this);
1237
1238       var contact = (Contact?) this._contact.get ();
1239       if (contact != null)
1240         {
1241           contact.weak_unref (this._contact_weak_notify_cb);
1242         }
1243     }
1244
1245   private void _contact_notify_presence_message ()
1246     {
1247       var contact = (Contact?) this._contact.get ();
1248       assert (contact != null); /* should never be called while cached */
1249       this.presence_message = contact.get_presence_message ();
1250     }
1251
1252   private void _contact_notify_presence_type ()
1253     {
1254       var contact = (Contact?) this._contact.get ();
1255       assert (contact != null); /* should never be called while cached */
1256       this.presence_type = Tpf.Persona._folks_presence_type_from_tp (
1257           contact.get_presence_type ());
1258     }
1259
1260   private void _contact_notify_presence_status ()
1261     {
1262       var contact = (Contact?) this._contact.get ();
1263       assert (contact != null); /* should never be called while cached */
1264       this.presence_status = contact.get_presence_status ();
1265     }
1266
1267   private static PresenceType _folks_presence_type_from_tp (
1268       TelepathyGLib.ConnectionPresenceType type)
1269     {
1270       switch (type)
1271         {
1272           case TelepathyGLib.ConnectionPresenceType.AVAILABLE:
1273             return PresenceType.AVAILABLE;
1274           case TelepathyGLib.ConnectionPresenceType.AWAY:
1275             return PresenceType.AWAY;
1276           case TelepathyGLib.ConnectionPresenceType.BUSY:
1277             return PresenceType.BUSY;
1278           case TelepathyGLib.ConnectionPresenceType.ERROR:
1279             return PresenceType.ERROR;
1280           case TelepathyGLib.ConnectionPresenceType.EXTENDED_AWAY:
1281             return PresenceType.EXTENDED_AWAY;
1282           case TelepathyGLib.ConnectionPresenceType.HIDDEN:
1283             return PresenceType.HIDDEN;
1284           case TelepathyGLib.ConnectionPresenceType.OFFLINE:
1285             return PresenceType.OFFLINE;
1286           case TelepathyGLib.ConnectionPresenceType.UNKNOWN:
1287             return PresenceType.UNKNOWN;
1288           case TelepathyGLib.ConnectionPresenceType.UNSET:
1289             return PresenceType.UNSET;
1290           default:
1291             return PresenceType.UNKNOWN;
1292         }
1293     }
1294
1295   private void _contact_notify_avatar ()
1296     {
1297       var contact = (Contact?) this._contact.get ();
1298       assert (contact != null); /* should never be called while cached */
1299
1300       var file = contact.avatar_file;
1301       var token = contact.avatar_token;
1302       Icon? icon = null;
1303       var from_cache = false;
1304
1305       /* Handle all the different cases of avatars. */
1306       if (token == "")
1307         {
1308           /* Definitely know there's no avatar. */
1309           file = null;
1310           from_cache = false;
1311         }
1312       else if (token != null && file != null)
1313         {
1314           /* Definitely know there's some avatar, so leave the file alone. */
1315           from_cache = false;
1316         }
1317       else
1318         {
1319           /* Not sure about the avatar; fall back to any cached avatar. */
1320           file = ((Tpf.PersonaStore) this.store)._query_avatar_cache (this.iid);
1321           from_cache = true;
1322         }
1323
1324       if (file != null)
1325         {
1326           icon = new FileIcon (file);
1327         }
1328
1329       if ((this._avatar == null) != (icon == null) || !this._avatar.equal (icon))
1330         {
1331           this._avatar = (LoadableIcon) icon;
1332           this.notify_property ("avatar");
1333
1334           if (from_cache == false)
1335             {
1336               /* Mark the persona cache as needing to be updated. */
1337               ((Tpf.PersonaStore) this.store)._set_cache_needs_update ();
1338
1339               /* Update the avatar cache. */
1340               ((Tpf.PersonaStore) this.store)._update_avatar_cache (this.iid,
1341                   file);
1342             }
1343         }
1344     }
1345
1346   /**
1347    * Look up a {@link Tpf.Persona} by its {@link TelepathyGLib.Contact}.
1348    *
1349    * If the {@link TelepathyGLib.Account} for the contact's
1350    * {@link TelepathyGLib.Connection} is ``null``, or if a
1351    * {@link Tpf.PersonaStore} can't be found for that account, ``null`` will be
1352    * returned. Otherwise, if a {@link Tpf.Persona} already exists for the given
1353    * contact, that will be returned; if one doesn't exist a new one will be
1354    * created and returned. In this case, the {@link Tpf.Persona} will be added
1355    * to the {@link PersonaStore} associated with the account, and will be
1356    * removed when ``contact`` is destroyed.
1357    *
1358    * @param contact the Telepathy contact of the persona
1359    * @return the persona associated with the contact, or ``null``
1360    * @since 0.6.6
1361    */
1362   public static Persona? dup_for_contact (Contact contact)
1363     {
1364       var account = contact.connection.get_account ();
1365
1366       debug ("Tpf.Persona.dup_for_contact (%p): got account %p", contact,
1367           account);
1368
1369       /* Account could be null; see the docs for tp_connection_get_account(). */
1370       if (account == null)
1371         {
1372           return null;
1373         }
1374
1375       var store = PersonaStore.dup_for_account (account);
1376       return store._ensure_persona_for_contact (contact);
1377     }
1378
1379 #if HAVE_ZEITGEIST
1380   internal void _increase_counter (string id, string interaction_type, Event event)
1381     {
1382       var timestamp = (uint) (event.get_timestamp () / 1000);
1383       var converted_datetime = new DateTime.from_unix_utc (timestamp);
1384       var interpretation = event.get_interpretation ();
1385
1386       /* Only count send/receive for IM interactions */
1387       if (interaction_type == Zeitgeist.NMO_IMMESSAGE &&
1388           (interpretation == Zeitgeist.ZG_SEND_EVENT ||
1389            interpretation == Zeitgeist.ZG_RECEIVE_EVENT))
1390         {
1391           this._im_interaction_count++;
1392           this.notify_property ("im-interaction-count");
1393           if (this._last_im_interaction_datetime == null ||
1394               this._last_im_interaction_datetime.compare (converted_datetime) == -1)
1395             {
1396               this._last_im_interaction_datetime = converted_datetime;
1397               this.notify_property ("last-im-interaction-datetime");
1398             }
1399           debug ("Persona %s IM interaction details changed:\n - count: %u \n - timestamp: %lld\n",
1400               id, this._im_interaction_count, this._last_im_interaction_datetime.format ("%H %M %S - %d %m %y"));
1401         }
1402       /* Only count successful call for call interactions */
1403       else if (interaction_type == Zeitgeist.NFO_AUDIO &&
1404                 interpretation == Zeitgeist.ZG_LEAVE_EVENT)
1405         {
1406           this._call_interaction_count++;
1407           this.notify_property ("call-interaction-count");
1408           if (this._last_call_interaction_datetime == null ||
1409               this._last_call_interaction_datetime.compare (converted_datetime) == -1)
1410             {
1411               this._last_call_interaction_datetime = converted_datetime;
1412               this.notify_property ("last-call-interaction-datetime");
1413             }
1414           debug ("Persona %s Call interaction details changed:\n - count: %u \n - timestamp: %lld\n",
1415              id, this._call_interaction_count, this._last_call_interaction_datetime.format ("%H %M %S - %d %m %y"));
1416         }
1417     }
1418
1419   internal void _reset_interaction ()
1420     {
1421       this._call_interaction_count = 0;
1422       this._im_interaction_count = 0;
1423       this._last_call_interaction_datetime = null;
1424       this._last_im_interaction_datetime = null;
1425     }
1426 #endif
1427 }