2 * Copyright (C) 2010 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 * Philip Withnall <philip.withnall@collabora.co.uk>
25 using TelepathyGLib.ContactFeature;
29 * A persona store which is associated with a single Telepathy account. It will
30 * create {@link Persona}s for each of the contacts in the published, stored or
32 * [[http://people.collabora.co.uk/~danni/telepathy-book/chapter.channel.html|channels]]
35 public class Tpf.PersonaStore : Folks.PersonaStore
37 private string[] undisplayed_groups = { "publish", "stored", "subscribe" };
39 private HashTable<string, Persona> _personas;
40 /* universal, contact owner handles (not channel-specific) */
41 private HashMap<uint, Persona> handle_persona_map;
42 private HashMap<Channel, HashSet<Persona>> channel_group_personas_map;
43 private HashMap<Channel, HashSet<uint>> channel_group_incoming_adds;
44 private HashMap<string, HashSet<Tpf.Persona>> group_outgoing_adds;
45 private HashMap<string, HashSet<Tpf.Persona>> group_outgoing_removes;
46 private HashMap<string, Channel> standard_channels_unready;
47 private HashMap<string, Channel> group_channels_unready;
48 private HashMap<string, Channel> groups;
49 /* FIXME: Should be HashSet<Handle> */
50 private HashSet<uint> favourite_handles;
51 private Channel publish;
52 private Channel stored;
53 private Channel subscribe;
54 private Connection conn;
55 private TpLowlevel ll;
56 private AccountManager account_manager;
57 private Logger logger;
59 internal signal void group_members_changed (string group,
60 GLib.List<Persona>? added, GLib.List<Persona>? removed);
61 internal signal void group_removed (string group, GLib.Error? error);
65 * The Telepathy account this store is based upon.
67 [Property(nick = "basis account",
68 blurb = "Telepathy account this store is based upon")]
69 public Account account { get; construct; }
74 public override string type_id { get; private set; }
79 public override string id { get; private set; }
84 public override HashTable<string, Persona> personas
86 get { return this._personas; }
90 * Create a new PersonaStore.
92 * Create a new persona store to store the {@link Persona}s for the contacts
93 * in the Telepathy account provided by `account`.
95 public PersonaStore (Account account)
97 Object (account: account);
99 this.type_id = "telepathy";
100 this.id = account.get_object_path ();
102 this._personas = new HashTable<string, Persona> (str_hash,
105 this.handle_persona_map = new HashMap<uint, Persona> ();
106 this.channel_group_personas_map = new HashMap<Channel, HashSet<Persona>> (
108 this.channel_group_incoming_adds = new HashMap<Channel, HashSet<uint>> ();
109 this.group_outgoing_adds = new HashMap<string, HashSet<Tpf.Persona>> ();
110 this.group_outgoing_removes = new HashMap<string, HashSet<Tpf.Persona>> (
114 this.subscribe = null;
115 this.standard_channels_unready = new HashMap<string, Channel> ();
116 this.group_channels_unready = new HashMap<string, Channel> ();
117 this.groups = new HashMap<string, Channel> ();
118 this.favourite_handles = new HashSet<uint> ();
119 this.ll = new TpLowlevel ();
120 this.account_manager = AccountManager.dup ();
122 this.account_manager.account_disabled.connect ((a) =>
124 if (this.account == a)
127 this.account_manager.account_removed.connect ((a) =>
129 if (this.account == a)
132 this.account_manager.account_validity_changed.connect ((a, valid) =>
134 if (!valid && this.account == a)
138 this.account.status_changed.connect (this.account_status_changed_cb);
140 TelepathyGLib.ConnectionStatusReason reason;
141 var status = this.account.get_connection_status (out reason);
142 /* immediately handle accounts which are not currently being disconnected
144 if (status != TelepathyGLib.ConnectionStatus.DISCONNECTED)
146 var details = new GLib.HashTable<weak string, weak void*> (
147 str_hash, str_equal);
148 this.account_status_changed_cb (
149 TelepathyGLib.ConnectionStatus.DISCONNECTED, status, reason, null,
155 this.logger = new Logger (this.id);
156 this.logger.invalidated.connect (() =>
158 warning ("lost connection to the telepathy-logger service");
161 this.logger.favourite_contacts_changed.connect (
162 this.favourite_contacts_changed_cb);
166 warning ("couldn't connect to the telepathy-logger service");
171 private async void initialise_favourite_contacts ()
173 /* Get an initial set of favourite contacts */
176 string[] contacts = yield this.logger.get_favourite_contacts ();
178 if (contacts.length == 0)
181 /* Note that we don't need to release these handles, as they're
182 * also held by the relevant contact objects, and will be released
183 * as appropriate by those objects (we're circumventing tp-glib's
184 * handle reference counting). */
185 this.conn.request_handles (-1, HandleType.CONTACT, contacts,
186 (c, ht, nh, h, i, e, w) =>
190 this.change_favourites_by_request_handles (nh, h, i, e,
195 warning ("couldn't get list of favourite contacts: %s",
200 /* FIXME: Have to pass this as weak_object parameter since Vala
201 * seems to swap the order of user_data and weak_object in the
206 warning ("couldn't get list of favourite contacts: %s", e.message);
210 private void change_favourites_by_request_handles (uint n_handles,
211 Handle[] handles, string[] ids, GLib.Error? error,
212 bool add) throws GLib.Error
217 for (var i = 0; i < n_handles; i++)
219 Handle h = handles[i];
220 Persona p = this.handle_persona_map[h];
222 /* Add/Remove the handle to the set of favourite handles, since we
223 * might not have the corresponding contact yet */
225 this.favourite_handles.add (h);
227 this.favourite_handles.remove (h);
229 /* If the persona isn't in the handle_persona_map yet, it's most
230 * likely because the account hasn't connected yet (and we haven't
231 * received the roster). If there are already entries in
232 * handle_persona_map, the account *is* connected and we should
233 * warn about the unknown persona. */
234 if (p == null && this.handle_persona_map.size > 0)
236 warning ("unknown persona '%s' in favourites list", ids[i]);
240 /* Mark or unmark the persona as a favourite */
242 p.is_favourite = add;
246 private void favourite_contacts_changed_cb (string[] added, string[] removed)
248 /* Don't listen to favourites updates if the account is disconnected. */
249 if (this.conn == null)
253 if (added.length > 0)
255 this.conn.request_handles (-1, HandleType.CONTACT, added,
256 (c, ht, nh, h, i, e, w) =>
260 this.change_favourites_by_request_handles (nh, h, i, e,
265 warning ("couldn't add favourite contacts: %s",
272 /* Remove favourites */
273 if (removed.length > 0)
275 this.conn.request_handles (-1, HandleType.CONTACT, removed,
276 (c, ht, nh, h, i, e, w) =>
280 this.change_favourites_by_request_handles (nh, h, i, e,
285 warning ("couldn't remove favourite contacts: %s",
293 private void account_status_changed_cb (uint old_status, uint new_status,
294 uint reason, string? dbus_error_name,
295 GLib.HashTable<weak string, weak GLib.Value>? details)
297 if (new_status != TelepathyGLib.ConnectionStatus.CONNECTED)
300 var conn = this.account.get_connection ();
301 conn.call_when_ready (this.connection_ready_cb);
304 private void connection_ready_cb (Connection conn, GLib.Error? error)
306 this.ll.connection_connect_to_new_group_channels (conn,
307 this.new_group_channels_cb);
309 this.add_standard_channel (conn, "publish");
310 this.add_standard_channel (conn, "stored");
311 this.add_standard_channel (conn, "subscribe");
314 /* We can only initialise the favourite contacts once we've got conn */
315 this.initialise_favourite_contacts.begin ();
318 private void new_group_channels_cb (void *data)
320 var channel = (Channel) data;
323 warning ("error creating channel for NewChannels signal");
327 this.set_up_new_group_channel (channel);
328 this.channel_group_changes_resolve (channel);
331 private void channel_group_changes_resolve (Channel channel)
333 var group = channel.get_identifier ();
335 var change_maps = new HashMap<HashSet<Tpf.Persona>, bool> ();
336 if (this.group_outgoing_adds[group] != null)
337 change_maps.set (this.group_outgoing_adds[group], true);
339 if (this.group_outgoing_removes[group] != null)
340 change_maps.set (this.group_outgoing_removes[group], false);
342 if (change_maps.size < 1)
345 foreach (var entry in change_maps)
347 var changes = entry.key;
349 foreach (var persona in changes)
353 this.ll.channel_group_change_membership (channel,
354 (Handle) persona.contact.handle, entry.value);
358 warning ("failed to change persona %s group %s membership to "
360 persona.uid, group, entry.value ? "true" : "false");
368 private void set_up_new_standard_channel (Channel channel)
370 /* hold a ref to the channel here until it's ready, so it doesn't
372 this.standard_channels_unready[channel.get_identifier ()] = channel;
374 channel.notify["channel-ready"].connect ((s, p) =>
377 var name = c.get_identifier ();
379 if (name == "publish")
383 c.group_members_changed.connect (
384 this.publish_channel_group_members_changed_cb);
386 else if (name == "stored")
390 c.group_members_changed.connect (
391 this.stored_channel_group_members_changed_cb);
393 else if (name == "subscribe")
397 c.group_members_changed.connect (
398 this.subscribe_channel_group_members_changed_cb);
401 this.standard_channels_unready.remove (name);
403 c.invalidated.connect (this.channel_invalidated_cb);
405 unowned IntSet? members = c.group_get_members ();
408 this.channel_group_pend_incoming_adds.begin (c,
409 members.to_array (), true);
414 private void publish_channel_group_members_changed_cb (Channel channel,
416 /* FIXME: Array<uint> => Array<Handle>; parser bug */
418 Array<uint>? removed,
419 Array<uint>? local_pending,
420 Array<uint>? remote_pending,
425 this.channel_group_pend_incoming_adds.begin (channel, added, true);
427 /* we refuse to send these contacts our presence, so remove them */
428 for (var i = 0; i < removed.length; i++)
430 var handle = removed.index (i);
431 this.ignore_by_handle_if_needed (handle);
434 /* FIXME: continue for the other arrays */
437 private void stored_channel_group_members_changed_cb (Channel channel,
439 /* FIXME: Array<uint> => Array<Handle>; parser bug */
441 Array<uint>? removed,
442 Array<uint>? local_pending,
443 Array<uint>? remote_pending,
448 this.channel_group_pend_incoming_adds.begin (channel, added, true);
450 for (var i = 0; i < removed.length; i++)
452 var handle = removed.index (i);
453 this.ignore_by_handle_if_needed (handle);
457 private void subscribe_channel_group_members_changed_cb (Channel channel,
459 /* FIXME: Array<uint> => Array<Handle>; parser bug */
461 Array<uint>? removed,
462 Array<uint>? local_pending,
463 Array<uint>? remote_pending,
469 this.channel_group_pend_incoming_adds.begin (channel, added, true);
471 /* expose ourselves to anyone we can see */
472 if (this.publish != null)
474 this.channel_group_pend_incoming_adds.begin (this.publish, added,
479 /* these contacts refused to send us their presence, so remove them */
480 for (var i = 0; i < removed.length; i++)
482 var handle = removed.index (i);
483 this.ignore_by_handle_if_needed (handle);
486 /* FIXME: continue for the other arrays */
489 private void channel_invalidated_cb (Proxy proxy, uint domain, int code,
492 var channel = (Channel) proxy;
494 this.channel_group_personas_map.remove (channel);
495 this.channel_group_incoming_adds.remove (channel);
497 if (proxy == this.publish)
499 else if (proxy == this.subscribe)
500 this.subscribe = null;
503 var error = new GLib.Error ((Quark) domain, code, "%s", message);
504 var name = channel.get_identifier ();
505 this.group_removed (name, error);
506 this.groups.remove (name);
510 private void ignore_by_handle_if_needed (uint handle)
512 unowned TelepathyGLib.IntSet members;
514 if (this.subscribe != null)
516 members = this.subscribe.group_get_members ();
517 if (members.is_member (handle))
520 members = this.subscribe.group_get_remote_pending ();
521 if (members.is_member (handle))
525 if (this.publish != null)
527 members = this.publish.group_get_members ();
528 if (members.is_member (handle))
532 this.ignore_by_handle (handle);
535 private void ignore_by_handle (uint handle)
537 var persona = this.handle_persona_map[handle];
540 * remove all handle-keyed entries
542 this.handle_persona_map.remove (handle);
544 /* skip channel_group_incoming_adds because they occurred after removal */
550 * remove all persona-keyed entries
552 foreach (var entry in this.channel_group_personas_map)
554 var channel = (Channel) entry.key;
555 var members = this.channel_group_personas_map[channel];
557 members.remove (persona);
560 foreach (var entry in this.group_outgoing_adds)
562 var name = (string) entry.key;
563 var members = this.group_outgoing_adds[name];
565 members.remove (persona);
568 var personas = new GLib.List<Persona> ();
569 personas.append (persona);
570 this.personas_removed (personas);
571 this._personas.remove (persona.iid);
577 public override void remove_persona (Folks.Persona persona)
579 var tp_persona = (Tpf.Persona) persona;
583 this.ll.channel_group_change_membership (this.stored,
584 (Handle) tp_persona.contact.handle, false);
588 warning ("failed to remove persona '%s' (%s) from stored list: %s",
589 tp_persona.uid, tp_persona.alias, e.message);
594 this.ll.channel_group_change_membership (this.subscribe,
595 (Handle) tp_persona.contact.handle, false);
599 warning ("failed to remove persona '%s' (%s) from subscribe list: %s",
600 tp_persona.uid, tp_persona.alias, e.message);
605 this.ll.channel_group_change_membership (this.publish,
606 (Handle) tp_persona.contact.handle, false);
610 warning ("failed to remove persona '%s' (%s) from publish list: %s",
611 tp_persona.uid, tp_persona.alias, e.message);
614 /* the contact will be actually removed (and signaled) when we hear back
618 /* Only non-group contact list channels should use create_personas == true,
619 * since the exposed set of Personas are meant to be filtered by them */
620 private async void channel_group_pend_incoming_adds (Channel channel,
622 bool create_personas)
624 var adds_length = adds != null ? adds.length : 0;
625 if (adds_length >= 1)
629 yield this.create_personas_from_channel_handles_async (channel,
633 for (var i = 0; i < adds.length; i++)
635 var channel_handle = (Handle) adds.index (i);
636 var contact_handle = channel.group_get_handle_owner (
638 var persona = this.handle_persona_map[contact_handle];
641 HashSet<uint>? contact_handles =
642 this.channel_group_incoming_adds[channel];
643 if (contact_handles == null)
645 contact_handles = new HashSet<uint> ();
646 this.channel_group_incoming_adds[channel] =
649 contact_handles.add (contact_handle);
654 this.channel_groups_add_new_personas ();
657 private void set_up_new_group_channel (Channel channel)
659 /* hold a ref to the channel here until it's ready, so it doesn't
661 this.group_channels_unready[channel.get_identifier ()] = channel;
663 channel.notify["channel-ready"].connect ((s, p) =>
666 var name = c.get_identifier ();
668 this.groups[name] = c;
669 this.group_channels_unready.remove (name);
671 c.invalidated.connect (this.channel_invalidated_cb);
672 c.group_members_changed_detailed.connect (
673 this.channel_group_members_changed_detailed_cb);
675 unowned IntSet members = c.group_get_members ();
678 this.channel_group_pend_incoming_adds.begin (c,
679 members.to_array (), false);
684 private void channel_group_members_changed_detailed_cb (Channel channel,
685 /* FIXME: Array<uint> => Array<Handle>; parser bug */
686 Array<weak uint> added,
687 Array<weak uint> removed,
688 Array<weak uint> local_pending,
689 Array<weak uint> remote_pending,
690 HashTable<weak string, weak Value> details)
693 this.channel_group_pend_incoming_adds.begin (channel, added, false);
695 /* FIXME: continue for the other arrays */
698 internal async void change_group_membership (Folks.Persona persona,
699 string group, bool is_member)
701 var tp_persona = (Tpf.Persona) persona;
702 var channel = this.groups[group];
703 var change_map = is_member ? this.group_outgoing_adds :
704 this.group_outgoing_removes;
705 var change_set = change_map[group];
707 if (change_set == null)
709 change_set = new HashSet<Tpf.Persona> ();
710 change_map[group] = change_set;
712 change_set.add (tp_persona);
716 /* the changes queued above will be resolve in the NewChannels handler
718 this.ll.connection_create_group_async (this.account.get_connection (),
723 /* the channel is already ready, so resolve immediately */
724 this.channel_group_changes_resolve (channel);
728 private void change_standard_contact_list_membership (
729 TelepathyGLib.Channel channel, Folks.Persona persona, bool is_member)
731 var tp_persona = (Tpf.Persona) persona;
735 this.ll.channel_group_change_membership (channel,
736 (Handle) tp_persona.contact.handle, is_member);
740 warning ("failed to change persona %s contact list %s " +
742 persona.uid, channel.get_identifier (),
743 is_member ? "true" : "false");
747 private async Channel? add_standard_channel (Connection conn, string name)
749 Channel? channel = null;
751 /* FIXME: handle the error GLib.Error from this function */
754 channel = yield this.ll.connection_open_contact_list_channel_async (
759 warning ("failed to add channel '%s': %s\n", name, e.message);
761 /* XXX: assuming there's no decent way to recover from this */
766 this.set_up_new_standard_channel (channel);
771 /* FIXME: Array<uint> => Array<Handle>; parser bug */
772 private async void create_personas_from_channel_handles_async (
774 Array<uint> channel_handles)
776 ContactFeature[] features =
779 /* XXX: also avatar token? */
783 uint[] contact_handles = {};
784 for (var i = 0; i < channel_handles.length; i++)
786 var channel_handle = (Handle) channel_handles.index (i);
787 var contact_handle = channel.group_get_handle_owner (channel_handle);
789 if (this.handle_persona_map[contact_handle] == null)
790 contact_handles += contact_handle;
795 if (contact_handles.length < 1)
798 unowned GLib.List<TelepathyGLib.Contact> contacts =
799 yield this.ll.connection_get_contacts_by_handle_async (
800 this.conn, contact_handles, features);
802 if (contacts == null || contacts.length () < 1)
805 var contacts_array = new TelepathyGLib.Contact[contacts.length ()];
807 unowned GLib.List<TelepathyGLib.Contact> l = contacts;
808 for (; l != null; l = l.next)
810 contacts_array[j] = l.data;
814 this.add_new_personas_from_contacts (contacts_array);
818 warning ("failed to create personas from incoming contacts in " +
820 channel.get_identifier (), e.message);
824 private async GLib.List<Tpf.Persona>? create_personas_from_contact_ids (
825 string[] contact_ids) throws GLib.Error
827 ContactFeature[] features =
830 /* XXX: also avatar token? */
834 if (contact_ids.length > 0)
836 unowned GLib.List<TelepathyGLib.Contact> contacts =
837 yield this.ll.connection_get_contacts_by_id_async (
838 this.conn, contact_ids, features);
840 GLib.List<Persona> personas = new GLib.List<Persona> ();
842 string err_format = "";
843 unowned GLib.List<TelepathyGLib.Contact> l;
844 for (l = contacts; l != null; l = l.next)
846 var contact = l.data;
849 var persona = this.add_persona_from_contact (contact);
851 personas.prepend (persona);
853 catch (TelepathyGLib.Error e)
856 err_format = "failed to create %u personas:\n";
858 err_format = "%s '%s' (%p): %s\n".printf (
859 err_format, contact.alias, contact, e.message);
866 throw new Folks.PersonaStoreError.CREATE_FAILED (err_format,
870 if (personas != null)
871 this.personas_added (personas);
879 private Tpf.Persona? add_persona_from_contact (Contact contact)
880 throws TelepathyGLib.Error
882 var h = contact.get_handle ();
883 if (this.handle_persona_map[h] == null)
885 var persona = new Tpf.Persona (contact, this);
887 this._personas.insert (persona.iid, persona);
888 this.handle_persona_map[h] = persona;
890 /* If the handle is a favourite, ensure the persona's marked
891 * as such. This deals with the case where we receive a
892 * contact _after_ we've discovered that they're a
894 persona.is_favourite = this.favourite_handles.contains (h);
903 private void add_new_personas_from_contacts (Contact[] contacts)
905 GLib.List<Persona> personas = new GLib.List<Persona> ();
906 foreach (Contact contact in contacts)
910 var persona = this.add_persona_from_contact (contact);
912 personas.prepend (persona);
914 catch (Tpf.PersonaError e)
916 warning ("failed to create persona from contact '%s' (%p)",
917 contact.alias, contact);
921 this.channel_groups_add_new_personas ();
923 if (personas != null)
924 this.personas_added (personas);
927 private void channel_groups_add_new_personas ()
929 foreach (var entry in this.channel_group_incoming_adds)
931 var channel = (Channel) entry.key;
932 var members_added = new GLib.List<Persona> ();
934 HashSet<Persona> members = this.channel_group_personas_map[channel];
936 members = new HashSet<Persona> ();
938 var contact_handles = entry.value;
939 if (contact_handles != null && contact_handles.size > 0)
941 var contact_handles_added = new HashSet<uint> ();
942 foreach (var contact_handle in contact_handles)
944 var persona = this.handle_persona_map[contact_handle];
947 members.add (persona);
948 members_added.prepend (persona);
949 contact_handles_added.add (contact_handle);
953 foreach (var handle in contact_handles_added)
954 contact_handles.remove (handle);
957 if (members.size > 0)
958 this.channel_group_personas_map[channel] = members;
960 var name = channel.get_identifier ();
961 if (this.group_is_display_group (name) &&
962 members_added.length () > 0)
964 members_added.reverse ();
965 this.group_members_changed (name, members_added, null);
970 private bool group_is_display_group (string group)
972 for (var i = 0; i < this.undisplayed_groups.length; i++)
974 if (this.undisplayed_groups[i] == group)
984 public override async Folks.Persona? add_persona_from_details (
985 HashTable<string, string> details) throws Folks.PersonaStoreError
987 var contact_id = details.lookup ("contact");
988 if (contact_id == null)
990 throw new PersonaStoreError.INVALID_ARGUMENT (
991 "persona store (%s, %s) requires the following details:\n" +
992 " contact (provided: '%s')\n",
993 this.type_id, this.id, contact_id);
996 string[] contact_ids = new string[1];
997 contact_ids[0] = contact_id;
1001 var personas = yield create_personas_from_contact_ids (
1004 if (personas != null && personas.length () == 1)
1006 var persona = personas.data;
1008 if (this.subscribe != null)
1009 change_standard_contact_list_membership (subscribe, persona,
1012 if (this.publish != null)
1014 var flags = publish.group_get_flags ();
1015 if ((flags & ChannelGroupFlags.CAN_ADD) ==
1016 ChannelGroupFlags.CAN_ADD)
1018 change_standard_contact_list_membership (publish, persona,
1027 warning ("requested a single persona, but got %u back",
1028 personas == null ? 0 : personas.length ());
1031 catch (GLib.Error e)
1033 warning ("failed to add a persona from details: %s", e.message);
1040 * Change the favourite status of a persona in this store.
1042 * This function is idempotent, but relies upon having a connection to the
1043 * Telepathy logger service, so may fail if that connection is not present.
1045 internal async void change_is_favourite (Folks.Persona persona,
1048 /* It's possible for us to not be able to connect to the logger;
1049 * see connection_ready_cb() */
1050 if (this.logger == null)
1052 warning ("failed to change favourite without connection to the " +
1053 "telepathy-logger service");
1059 /* Add or remove the persona to the list of favourites as
1061 var id = ((Tpf.Persona) persona).contact.get_identifier ();
1064 yield this.logger.add_favourite_contact (id);
1066 yield this.logger.remove_favourite_contact (id);
1068 catch (DBus.Error e)
1070 warning ("failed to change a persona's favourite status");
1074 internal async void change_alias (Tpf.Persona persona, string alias)
1076 debug ("Changing alias of persona %u to '%s'.", persona.contact.handle,
1078 this.ll.connection_set_contact_alias (this.conn,
1079 (Handle) persona.contact.handle, alias);