2 * Copyright (C) 2011 Collabora Ltd.
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.
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.
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/>.
18 * Travis Reitter <travis.reitter@collabora.co.uk>
19 * Marco Barisione <marco.barisione@collabora.co.uk>
20 * Raul Gutierrez Segales <raul.gutierrez.segales@collabora.co.uk>
30 * A persona subclass which represents a single EDS contact.
32 public class Edsf.Persona : Folks.Persona,
46 /* The following 4 definitions are used by the tests */
47 public static const string[] phone_fields = {
48 "assistant_phone", "business_phone", "business_phone_2", "callback_phone",
49 "car_phone", "company_phone", "home_phone", "home_phone_2", "isdn_phone",
50 "mobile_phone", "other_phone", "primary_phone"
52 public static const string[] address_fields = {
53 "address_home", "address_other", "address_work"
55 public static const string[] email_fields = {
56 "email_1", "email_2", "email_3", "email_4"
58 public static const string[] url_properties = {
59 "blog_url", "fburl", "homepage_url", "video_url"
63 * The vCard attribute used to specify a Contact's gender
66 * [[http://tools.ietf.org/html/draft-ietf-vcarddav-vcardrev-22]]
68 * Note that the above document is a draft and the gender property
69 * is still considered experimental, hence the "X-" prefix in the
70 * attribute name. So this might change.
74 public static const string gender_attribute_name = "X-GENDER";
77 * The value used to define the male gender for the
78 * X-GENDER vCard property.
81 * [[http://tools.ietf.org/html/draft-ietf-vcarddav-vcardrev-22]]
85 public static const string gender_male = "M";
88 * The value used to define the female gender for the
89 * X-GENDER vCard property.
92 * [[http://tools.ietf.org/html/draft-ietf-vcarddav-vcardrev-22]]
96 public static const string gender_female = "F";
98 private const string[] _linkable_properties = { "im-addresses",
100 "web-service-addresses" };
101 private const string[] _writeable_properties =
103 "web-service-addresses",
117 private HashSet<PhoneFieldDetails> _phone_numbers;
118 private Set<PhoneFieldDetails> _phone_numbers_ro;
119 private HashSet<EmailFieldDetails> _email_addresses;
120 private Set<EmailFieldDetails> _email_addresses_ro;
121 private HashSet<NoteFieldDetails> _notes;
122 private Set<NoteFieldDetails> _notes_ro;
123 private static HashTable<string, E.ContactField> _im_eds_map = null;
125 private HashSet<PostalAddressFieldDetails> _postal_addresses;
126 private Set<PostalAddressFieldDetails> _postal_addresses_ro;
128 private HashSet<string> _local_ids;
129 private Set<string> _local_ids_ro;
131 private HashMultiMap<string, WebServiceFieldDetails> _web_service_addresses;
134 * The e-d-s contact represented by this Persona
136 public E.Contact contact
145 [CCode (notify = false)]
146 public MultiMap<string, WebServiceFieldDetails> web_service_addresses
148 get { return this._web_service_addresses; }
151 var store = (Edsf.PersonaStore) this.store;
152 store._set_web_service_addresses (this, value);
157 * IDs used to link {@link Edsf.Persona}s.
159 [CCode (notify = false)]
160 public Set<string> local_ids
164 if (this._local_ids.contains (this.iid) == false)
166 this._local_ids.add (this.iid);
168 return this._local_ids_ro;
170 set { this.change_local_ids.begin (value); }
178 public async void change_local_ids (Set<string> local_ids)
181 yield ((Edsf.PersonaStore) this.store)._set_local_ids (this, local_ids);
185 * The postal addresses of the contact.
187 * A list of postal addresses associated to the contact.
191 [CCode (notify = false)]
192 public Set<PostalAddressFieldDetails> postal_addresses
194 get { return this._postal_addresses_ro; }
197 ((Edsf.PersonaStore) this.store)._set_postal_addresses (this, value);
206 [CCode (notify = false)]
207 public Set<PhoneFieldDetails> phone_numbers
209 get { return this._phone_numbers_ro; }
212 ((Edsf.PersonaStore) this.store)._set_phones (this, value);
221 [CCode (notify = false)]
222 public Set<EmailFieldDetails> email_addresses
224 get { return this._email_addresses_ro; }
225 set { this.change_email_addresses.begin (value); }
233 public async void change_email_addresses (
234 Set<EmailFieldDetails> email_addresses) throws PropertyError
236 yield ((Edsf.PersonaStore) this.store)._set_emails (this,
245 [CCode (notify = false)]
246 public Set<NoteFieldDetails> notes
248 get { return this._notes_ro; }
251 ((Edsf.PersonaStore) this.store)._set_notes (this, value);
260 public override string[] linkable_properties
262 get { return this._linkable_properties; }
270 public override string[] writeable_properties
272 get { return this._writeable_properties; }
275 private LoadableIcon? _avatar = null;
277 * An avatar for the Persona.
279 * See {@link Folks.AvatarDetails.avatar}.
283 [CCode (notify = false)]
284 public LoadableIcon? avatar
286 get { return this._avatar; }
287 set { this.change_avatar.begin (value); }
295 public async void change_avatar (LoadableIcon? avatar) throws PropertyError
297 if (this._avatar == null ||
298 !this._avatar.equal (avatar))
300 yield ((Edsf.PersonaStore) this.store)._set_avatar (this, avatar);
304 private StructuredName? _structured_name = null;
310 [CCode (notify = false)]
311 public StructuredName? structured_name
313 get { return this._structured_name; }
314 set { this.change_structured_name.begin (value); }
322 public async void change_structured_name (StructuredName? structured_name)
325 yield ((Edsf.PersonaStore) this.store)._set_structured_name (this,
330 * The e-d-s contact uid
334 public string contact_id { get; private set; }
336 private string _full_name = "";
342 [CCode (notify = false)]
343 public string full_name
345 get { return this._full_name; }
346 set { this.change_full_name.begin (value); }
354 public async void change_full_name (string full_name) throws PropertyError
356 yield ((Edsf.PersonaStore) this.store)._set_full_name (this, full_name);
359 private string _nickname = "";
365 [CCode (notify = false)]
366 public string nickname
368 get { return this._nickname; }
369 set { this.change_nickname.begin (value); }
377 public async void change_nickname (string nickname) throws PropertyError
379 yield ((Edsf.PersonaStore) this.store)._set_nickname (this, nickname);
382 private Gender _gender;
388 [CCode (notify = false)]
391 get { return this._gender; }
392 set { this.change_gender.begin (value); }
400 public async void change_gender (Gender gender) throws PropertyError
402 yield ((Edsf.PersonaStore) this.store)._set_gender (this, gender);
405 private HashSet<UrlFieldDetails> _urls;
406 private Set<UrlFieldDetails> _urls_ro;
412 [CCode (notify = false)]
413 public Set<UrlFieldDetails> urls
415 get { return this._urls_ro; }
418 ((Edsf.PersonaStore) this.store)._set_urls (this, value);
422 private HashMultiMap<string, ImFieldDetails> _im_addresses =
423 new HashMultiMap<string, ImFieldDetails> (null, null,
424 (GLib.HashFunc) ImFieldDetails.hash,
425 (GLib.EqualFunc) ImFieldDetails.equal);
432 [CCode (notify = false)]
433 public MultiMap<string, ImFieldDetails> im_addresses
435 get { return this._im_addresses; }
436 set { this.change_im_addresses.begin (value); }
444 public async void change_im_addresses (
445 MultiMap<string, ImFieldDetails> im_addresses) throws PropertyError
447 yield ((Edsf.PersonaStore) this.store)._set_im_fds (this, im_addresses);
450 private HashSet<string> _groups;
451 private Set<string> _groups_ro;
458 [CCode (notify = false)]
459 public Set<string> groups
461 get { return this._groups_ro; }
464 ((Edsf.PersonaStore) this.store)._set_groups (this, value);
473 public async void change_group (string group, bool is_member)
477 if ((is_member == true && this._groups.contains (group) == true) ||
478 (is_member == false && this._groups.contains (group) == false))
483 /* Replace the current set of groups with a modified one. */
484 var new_groups = new HashSet<string> ();
485 foreach (var category_name in this._groups)
487 new_groups.add (category_name);
490 if (is_member == false)
492 new_groups.remove (group);
496 new_groups.add (group);
499 this.groups = new_groups;
505 * @param store_id the {@link PersonaStore.id}
506 * @param contact the Contact
507 * @return a valid IID
511 internal static string build_iid_from_contact (string store_id,
515 (string) Edsf.Persona._get_property_from_contact (contact, "id");
516 return Edsf.Persona.build_iid (store_id, contact_id);
522 * @param store_id the {@link PersonaStore.id}
523 * @param contact_id the id belonging to the Contact
524 * @return a valid IID
528 internal static string build_iid (string store_id, string contact_id)
530 return "%s:%s".printf (store_id, contact_id);
535 * Create a new persona.
537 * Create a new persona for the {@link PersonaStore} `store`, representing
538 * the EDS contact given by `contact`.
542 public Persona (PersonaStore store, E.Contact contact)
545 (string) Edsf.Persona._get_property_from_contact (contact, "id");
546 var uid = this.build_uid (BACKEND_NAME, store.id, contact_id);
547 var iid = Edsf.Persona.build_iid (store.id, contact_id);
548 var is_user = BookClient.is_self (contact);
550 (string) Edsf.Persona._get_property_from_contact (contact,
553 debug ("Creating new Edsf.Persona with IID '%s'", iid);
555 Object (display_id: full_name,
561 this._gender = Gender.UNSPECIFIED;
562 this.contact_id = contact_id;
563 this._phone_numbers = new HashSet<PhoneFieldDetails> (
564 (GLib.HashFunc) PhoneFieldDetails.hash,
565 (GLib.EqualFunc) PhoneFieldDetails.equal);
566 this._phone_numbers_ro = this._phone_numbers.read_only_view;
567 this._email_addresses = new HashSet<EmailFieldDetails> (
568 (GLib.HashFunc) EmailFieldDetails.hash,
569 (GLib.EqualFunc) EmailFieldDetails.equal);
570 this._email_addresses_ro = this._email_addresses.read_only_view;
571 this._notes = new HashSet<NoteFieldDetails> (
572 (GLib.HashFunc) NoteFieldDetails.hash,
573 (GLib.EqualFunc) NoteFieldDetails.equal);
574 this._notes_ro = this._notes.read_only_view;
575 this._urls = new HashSet<UrlFieldDetails> (
576 (GLib.HashFunc) UrlFieldDetails.hash,
577 (GLib.EqualFunc) UrlFieldDetails.equal);
578 this._urls_ro = this._urls.read_only_view;
579 this._postal_addresses = new HashSet<PostalAddressFieldDetails> (
580 (GLib.HashFunc) PostalAddressFieldDetails.hash,
581 (GLib.EqualFunc) PostalAddressFieldDetails.equal);
582 this._postal_addresses_ro = this._postal_addresses.read_only_view;
583 this._local_ids = new HashSet<string> ();
584 this._local_ids_ro = this._local_ids.read_only_view;
585 this._web_service_addresses =
586 new HashMultiMap<string, WebServiceFieldDetails> (
588 (GLib.HashFunc) WebServiceFieldDetails.hash,
589 (GLib.EqualFunc) WebServiceFieldDetails.equal);
590 this._groups = new HashSet<string> ();
591 this._groups_ro = this._groups.read_only_view;
593 this._update (contact);
601 public override void linkable_property_to_links (string prop_name,
602 Folks.Persona.LinkablePropertyCallback callback)
604 if (prop_name == "im-addresses")
606 foreach (var protocol in this._im_addresses.get_keys ())
608 var im_fds = this._im_addresses.get (protocol);
610 foreach (var im_fd in im_fds)
611 callback (protocol + ":" + im_fd.value);
614 else if (prop_name == "local-ids")
616 /* Note: we need to use this.local_ids and not this._local_ids,
617 * otherwise this can have a different behaviour depending
618 * on the state of the current Persona depending on whether
619 * this.local_ids was called before or not. */
620 foreach (var id in this.local_ids)
625 else if (prop_name == "web-service-addresses")
627 foreach (var web_service in this.web_service_addresses.get_keys ())
629 var web_service_addresses =
630 this._web_service_addresses.get (web_service);
632 foreach (var ws_fd in web_service_addresses)
633 callback (web_service + ":" + ws_fd.value);
639 base.linkable_property_to_links (prop_name, callback);
645 debug ("Destroying Edsf.Persona '%s': %p", this.uid, this);
649 * Update attribs of the persona.
651 internal void _update (E.Contact contact)
653 this.contact = contact;
655 this._update_names ();
656 this._update_avatar ();
657 this._update_urls ();
658 this._update_phones ();
659 this._update_addresses ();
660 this._update_emails ();
661 this._update_im_addresses ();
662 this._update_groups ();
663 this._update_notes ();
664 this._update_local_ids ();
665 this._update_web_services_addresses ();
666 this._update_gender ();
669 private void _update_params (AbstractFieldDetails details,
670 E.VCardAttribute attr)
672 foreach (unowned E.VCardAttributeParam param in attr.get_params ())
674 string param_name = param.get_name ().down ();
675 foreach (unowned string param_value in param.get_values ())
677 details.add_parameter (param_name, param_value);
682 private void _update_gender ()
684 var gender = Gender.UNSPECIFIED;
686 this.contact.get_attribute (Edsf.Persona.gender_attribute_name);
688 if (gender_attr != null)
690 var gender_str = gender_attr.get_value ().up ();
692 if (gender_str == Edsf.Persona.gender_male)
694 gender = Gender.MALE;
696 else if (gender_str == Edsf.Persona.gender_female)
698 gender = Gender.FEMALE;
702 if (this._gender != gender)
704 this._gender = gender;
705 this.notify_property ("gender");
709 private void _update_web_services_addresses ()
711 this._web_service_addresses.clear ();
713 var services = this.contact.get_attribute ("X-FOLKS-WEB-SERVICES-IDS");
714 if (services != null)
716 foreach (var service in services.get_params ())
718 var service_name = service.get_name ().down ();
719 foreach (var service_id in service.get_values ())
721 this._web_service_addresses.set (service_name,
722 new WebServiceFieldDetails (service_id));
727 this.notify_property ("web-service-addresses");
730 private void _update_emails ()
732 this._email_addresses.clear ();
734 var attrs = this.contact.get_attributes (E.ContactField.EMAIL);
735 foreach (var attr in attrs)
737 var email_fd = new EmailFieldDetails (attr.get_value ());
738 this._update_params (email_fd, attr);
739 this._email_addresses.add (email_fd);
742 this.notify_property ("email-addresses");
745 private void _update_notes ()
747 this._notes.clear ();
749 string n = (string) this._get_property ("note");
750 if (n != null && n != "")
752 var note = new NoteFieldDetails (n);
753 this._notes.add (note);
756 this.notify_property ("notes");
759 private void _update_names ()
761 string full_name = (string) this._get_property ("full_name");
762 if (this._full_name != full_name)
764 this._full_name = full_name;
765 this.notify_property ("full-name");
768 string nickname = (string) this._get_property ("nickname");
769 if (this.nickname != nickname)
771 this._nickname = nickname;
772 this.notify_property ("nickname");
775 StructuredName? structured_name = null;
776 E.ContactName? cn = (E.ContactName) this._get_property ("name");
779 string family_name = cn.family;
780 string given_name = cn.given;
781 string additional_names = cn.additional;
782 string prefixes = cn.prefixes;
783 string suffixes = cn.suffixes;
784 structured_name = new StructuredName (family_name, given_name,
785 additional_names, prefixes,
789 if (structured_name != null && !structured_name.is_empty ())
791 this._structured_name = structured_name;
792 this.notify_property ("structured-name");
794 else if (this._structured_name != null)
796 this._structured_name = null;
797 this.notify_property ("structured-name");
801 private LoadableIcon? _contact_photo_to_loadable_icon (ContactPhoto? p)
810 case ContactPhotoType.URI:
811 if (p.get_uri () == null)
816 return new FileIcon (File.new_for_uri (p.get_uri ()));
817 case ContactPhotoType.INLINED:
818 if (p.get_mime_type () == null || p.get_inlined () == null)
823 return new Edsf.MemoryIcon (p.get_mime_type (), p.get_inlined ());
829 private void _update_avatar ()
831 E.ContactPhoto? p = (E.ContactPhoto) this._get_property ("photo");
833 var cache = AvatarCache.dup ();
835 // Convert the ContactPhoto to a LoadableIcon and store or update it.
836 var new_avatar = this._contact_photo_to_loadable_icon (p);
838 if (this._avatar != null && new_avatar == null)
840 // Remove the old cached avatar, ignoring errors.
841 cache.remove_avatar.begin (this.uid, (obj, res) =>
845 cache.remove_avatar.end (res);
847 catch (GLib.Error e1) {}
850 this.notify_property ("avatar");
853 else if ((this.avatar == null && new_avatar != null) ||
854 (this.avatar != null && new_avatar != null &&
855 this._avatar.equal (new_avatar) == false))
857 // Store the new avatar in the cache.
858 cache.store_avatar.begin (this.uid, new_avatar, (obj, res) =>
862 cache.store_avatar.end (res);
863 this._avatar = new_avatar;
864 this.notify_property ("avatar");
866 catch (GLib.Error e2)
868 warning ("Couldn't cache avatar for Edsf.Persona '%s': %s",
869 this.uid, e2.message);
870 new_avatar = null; /* failure */
876 private void _update_urls ()
879 var urls_temp = new HashSet<UrlFieldDetails> ();
881 /* First we get the standard Evo urls.. */
882 foreach (string url_property in this.url_properties)
884 string u = (string) this._get_property (url_property);
885 if (u != null && u != "")
887 var fd_u = new UrlFieldDetails (u);
888 fd_u.set_parameter("type", url_property);
889 urls_temp.add (fd_u);
893 /* Now we go for extra URLs */
894 var vcard = (E.VCard) this.contact;
895 foreach (unowned E.VCardAttribute attr in vcard.get_attributes ())
897 if (attr.get_name () == "X-URIS")
899 var url_fd = new UrlFieldDetails (attr.get_value ());
900 this._update_params (url_fd, attr);
901 urls_temp.add (url_fd);
905 if (!Utils.set_afd_equal (urls_temp, this._urls))
909 foreach (var url_fd in urls_temp)
911 this._urls.add (url_fd);
914 this.notify_property ("urls");
918 private void _update_im_addresses ()
920 var im_eds_map = this._get_im_eds_map ();
921 this._im_addresses.clear ();
923 foreach (var im_proto in im_eds_map.get_keys ())
925 var addresses = this.contact.get_attributes (
926 im_eds_map.lookup (im_proto));
927 foreach (var attr in addresses)
931 var addr = attr.get_value ();
932 string normalised_addr =
933 (owned) ImDetails.normalise_im_address (addr, im_proto);
934 var im_fd = new ImFieldDetails (normalised_addr);
935 this._im_addresses.set (im_proto, im_fd);
937 catch (Folks.ImDetailsError e)
940 "Problem when trying to normalise address: %s\n",
946 this.notify_property ("im-addresses");
949 private void _update_groups ()
951 unowned GLib.List<string> category_names =
952 (GLib.List<string>) this._contact.get (E.ContactField.CATEGORY_LIST);
953 var new_categories = new HashSet<string> ();
954 var added_categories = new LinkedList<string> ();
956 foreach (var category_name in category_names)
958 new_categories.add (category_name);
960 /* Is this a new category? */
961 if (!this._groups.contains (category_name))
963 added_categories.add (category_name);
967 /* Work out which categories have been removed. */
968 var removed_categories = new LinkedList<string> ();
970 foreach (var category_name in this._groups)
972 if (!new_categories.contains (category_name))
974 removed_categories.add (category_name);
978 /* Make the changes to this._groups and emit signals. */
979 foreach (var category_name in removed_categories)
981 this.group_changed (category_name, false);
982 this._groups.remove (category_name);
985 foreach (var category_name in added_categories)
987 this._groups.add (category_name);
988 this.group_changed (category_name, true);
991 /* Notify if anything's changed. */
992 if (added_categories.size != 0 || removed_categories.size != 0)
994 this.notify_property ("groups");
999 * build a table of im protocols / im protocol aliases
1001 internal static HashTable<string, E.ContactField> _get_im_eds_map ()
1003 lock (Edsf.Persona._im_eds_map)
1005 if (Edsf.Persona._im_eds_map == null)
1007 Edsf.Persona._im_eds_map =
1008 new HashTable<string, E.ContactField> (str_hash, str_equal);
1009 Edsf.Persona._im_eds_map.insert ("aim", ContactField.IM_AIM);
1010 Edsf.Persona._im_eds_map.insert ("yahoo", ContactField.IM_YAHOO);
1011 Edsf.Persona._im_eds_map.insert ("groupwise",
1012 ContactField.IM_GROUPWISE);
1013 Edsf.Persona._im_eds_map.insert ("jabber",
1014 ContactField.IM_JABBER);
1015 Edsf.Persona._im_eds_map.insert ("msn",
1016 ContactField.IM_MSN);
1017 Edsf.Persona._im_eds_map.insert ("icq",
1018 ContactField.IM_ICQ);
1019 Edsf.Persona._im_eds_map.insert ("gadugadu",
1020 ContactField.IM_GADUGADU);
1021 Edsf.Persona._im_eds_map.insert ("skype",
1022 ContactField.IM_SKYPE);
1026 return Edsf.Persona._im_eds_map;
1029 private void _update_phones ()
1031 this._phone_numbers.clear ();
1033 var attrs = this.contact.get_attributes (E.ContactField.TEL);
1034 foreach (var attr in attrs)
1036 var phone_fd = new PhoneFieldDetails (attr.get_value ());
1037 this._update_params (phone_fd, attr);
1038 this._phone_numbers.add (phone_fd);
1041 this.notify_property ("phone-numbers");
1044 private PostalAddress _postal_address_from_attribute (E.VCardAttribute attr)
1046 unowned GLib.List<string?> values = attr.get_values();
1047 unowned GLib.List<string?> l = values;
1049 var address_format = "";
1055 var postal_code = "";
1085 postal_code = l.data;
1094 return new PostalAddress (po_box, extension, street,
1095 locality, region, postal_code, country,
1096 address_format, null);
1100 * TODO: we should check if addresses corresponding to different types
1101 * are the same and if so instantiate only one PostalAddress
1102 * (with the given types).
1104 private void _update_addresses ()
1106 this._postal_addresses.clear ();
1108 var attrs = this.contact.get_attributes (E.ContactField.ADDRESS);
1109 foreach (unowned E.VCardAttribute attr in attrs)
1111 var pa_fd = new PostalAddressFieldDetails (
1112 this._postal_address_from_attribute (attr));
1113 this._update_params (pa_fd, attr);
1114 this._postal_addresses.add (pa_fd);
1117 this.notify_property ("postal-addresses");
1120 private void _update_local_ids ()
1122 this._local_ids.clear ();
1124 var ids = this.contact.get_attribute ("X-FOLKS-CONTACTS-IDS");
1127 unowned GLib.List<string> ids_v = ids.get_values ();
1129 foreach (var local_id in ids_v)
1131 this._local_ids.add (local_id);
1135 this.notify_property ("local-ids");
1138 internal static void * _get_property_from_contact (E.Contact contact,
1141 void *prop_value = null;
1142 prop_value = contact.get (E.Contact.field_id (prop_name));
1146 private void * _get_property (string prop_name)
1148 return Edsf.Persona._get_property_from_contact (this.contact,