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>
24 using Tp.ContactFeature;
27 public class Tpf.PersonaStore : Folks.PersonaStore
29 private string[] undisplayed_groups = { "publish", "stored", "subscribe" };
31 private HashTable<string, Persona> _personas;
32 /* universal, contact owner handles (not channel-specific) */
33 private HashMap<uint, Persona> handle_persona_map;
34 private HashMap<Channel, HashSet<Persona>> channel_group_personas_map;
35 private HashMap<Channel, HashSet<uint>> channel_group_incoming_adds;
36 private HashMap<string, HashSet<Tpf.Persona>> group_outgoing_adds;
37 private HashMap<string, HashSet<Tpf.Persona>> group_outgoing_removes;
38 private HashMap<string, Channel> standard_channels_unready;
39 private HashMap<string, Channel> group_channels_unready;
40 private HashMap<string, Channel> groups;
41 private Channel publish;
42 private Channel stored;
43 private Channel subscribe;
44 private Connection conn;
45 private TpLowlevel ll;
46 private AccountManager account_manager;
48 internal signal void group_members_changed (string group,
49 GLib.List<Persona>? added, GLib.List<Persona>? removed);
50 internal signal void group_removed (string group, GLib.Error? error);
52 [Property(nick = "basis account",
53 blurb = "Telepathy account this store is based upon")]
54 public Account account { get; construct; }
55 public override string type_id { get; private set; }
56 public override string id { get; private set; }
57 public override HashTable<string, Persona> personas
59 get { return this._personas; }
62 public PersonaStore (Account account)
64 Object (account: account);
66 this.type_id = "telepathy";
67 this.id = account.get_object_path (account);
69 this._personas = new HashTable<string, Persona> (str_hash,
72 this.handle_persona_map = new HashMap<uint, Persona> ();
73 this.channel_group_personas_map = new HashMap<Channel, HashSet<Persona>> (
75 this.channel_group_incoming_adds = new HashMap<Channel, HashSet<uint>> ();
76 this.group_outgoing_adds = new HashMap<string, HashSet<Tpf.Persona>> ();
77 this.group_outgoing_removes = new HashMap<string, HashSet<Tpf.Persona>> (
81 this.subscribe = null;
82 this.standard_channels_unready = new HashMap<string, Channel> ();
83 this.group_channels_unready = new HashMap<string, Channel> ();
84 this.groups = new HashMap<string, Channel> ();
85 this.ll = new TpLowlevel ();
86 this.account_manager = AccountManager.dup ();
88 this.account_manager.account_disabled.connect ((a) =>
90 if (this.account == a)
93 this.account_manager.account_removed.connect ((a) =>
95 if (this.account == a)
98 this.account_manager.account_validity_changed.connect ((a, valid) =>
100 if (!valid && this.account == a)
104 this.account.status_changed.connect (this.account_status_changed_cb);
106 Tp.ConnectionStatusReason reason;
107 var status = this.account.get_connection_status (out reason);
108 /* immediately handle accounts which are not currently being disconnected
110 if (status != Tp.ConnectionStatus.DISCONNECTED)
112 this.account_status_changed_cb (Tp.ConnectionStatus.DISCONNECTED,
113 status, reason, null, null);
117 private void account_status_changed_cb (ConnectionStatus old_status,
118 ConnectionStatus new_status, ConnectionStatusReason reason,
119 string? dbus_error_name, GLib.HashTable? details)
121 if (new_status != Tp.ConnectionStatus.CONNECTED)
124 var conn = this.account.get_connection ();
125 conn.call_when_ready (this.connection_ready_cb);
128 private void connection_ready_cb (Connection conn, GLib.Error? error)
130 this.ll.connection_connect_to_new_group_channels (conn,
131 this.new_group_channels_cb);
133 this.add_standard_channel (conn, "publish");
134 this.add_standard_channel (conn, "stored");
135 this.add_standard_channel (conn, "subscribe");
139 private void new_group_channels_cb (void *data)
141 var channel = (Channel) data;
144 warning ("error creating channel for NewChannels signal");
148 this.set_up_new_group_channel (channel);
149 this.channel_group_changes_resolve (channel);
152 private void channel_group_changes_resolve (Channel channel)
154 var group = channel.get_identifier ();
156 var change_maps = new HashMap<HashSet<Tpf.Persona>, bool> ();
157 if (this.group_outgoing_adds[group] != null)
158 change_maps.set (this.group_outgoing_adds[group], true);
160 if (this.group_outgoing_removes[group] != null)
161 change_maps.set (this.group_outgoing_removes[group], false);
163 if (change_maps.size < 1)
166 foreach (var entry in change_maps)
168 var changes = entry.key;
170 foreach (var persona in changes)
174 this.ll.channel_group_change_membership (channel,
175 (Handle) persona.contact.handle, entry.value);
179 warning ("failed to change persona %s group %s membership to "
181 persona.uid, group, entry.value ? "true" : "false");
189 private void set_up_new_standard_channel (Channel channel)
191 /* hold a ref to the channel here until it's ready, so it doesn't
193 this.standard_channels_unready[channel.get_identifier ()] = channel;
195 channel.notify["channel-ready"].connect ((s, p) =>
198 var name = c.get_identifier ();
200 if (name == "publish")
204 c.group_members_changed.connect (
205 this.publish_channel_group_members_changed_cb);
207 else if (name == "stored")
211 c.group_members_changed.connect (
212 this.stored_channel_group_members_changed_cb);
214 else if (name == "subscribe")
218 c.group_members_changed.connect (
219 this.subscribe_channel_group_members_changed_cb);
222 this.standard_channels_unready.remove (name);
224 c.invalidated.connect (this.channel_invalidated_cb);
226 unowned IntSet members = c.group_get_members ();
229 this.channel_group_pend_incoming_adds (c, members.to_array (),
235 private void publish_channel_group_members_changed_cb (Channel channel,
237 /* FIXME: Array<uint> => Array<Handle>; parser bug */
239 Array<uint>? removed,
240 Array<uint>? local_pending,
241 Array<uint>? remote_pending,
246 this.channel_group_pend_incoming_adds (channel, added, true);
248 /* we refuse to send these contacts our presence, so remove them */
249 for (var i = 0; i < removed.length; i++)
251 var handle = removed.index (i);
252 this.ignore_by_handle_if_needed (handle);
255 /* FIXME: continue for the other arrays */
258 private void stored_channel_group_members_changed_cb (Channel channel,
260 /* FIXME: Array<uint> => Array<Handle>; parser bug */
262 Array<uint>? removed,
263 Array<uint>? local_pending,
264 Array<uint>? remote_pending,
270 this.channel_group_pend_incoming_adds (channel, added, true);
273 for (var i = 0; i < removed.length; i++)
275 var handle = removed.index (i);
276 this.ignore_by_handle_if_needed (handle);
280 private void subscribe_channel_group_members_changed_cb (Channel channel,
282 /* FIXME: Array<uint> => Array<Handle>; parser bug */
284 Array<uint>? removed,
285 Array<uint>? local_pending,
286 Array<uint>? remote_pending,
292 this.channel_group_pend_incoming_adds (channel, added, true);
294 /* expose ourselves to anyone we can see */
295 if (this.publish != null)
297 this.channel_group_pend_incoming_adds (this.publish, added, true);
301 /* these contacts refused to send us their presence, so remove them */
302 for (var i = 0; i < removed.length; i++)
304 var handle = removed.index (i);
305 this.ignore_by_handle_if_needed (handle);
308 /* FIXME: continue for the other arrays */
311 private void channel_invalidated_cb (Proxy proxy, uint domain, int code,
314 var channel = (Channel) proxy;
316 this.channel_group_personas_map.remove (channel);
317 this.channel_group_incoming_adds.remove (channel);
319 if (proxy == this.publish)
321 else if (proxy == this.subscribe)
322 this.subscribe = null;
325 var error = new GLib.Error ((Quark) domain, code, "%s", message);
326 var name = channel.get_identifier ();
327 this.group_removed (name, error);
328 this.groups.remove (name);
332 private void ignore_by_handle_if_needed (uint handle)
334 unowned Tp.IntSet members;
336 if (this.subscribe != null)
338 members = this.subscribe.group_get_members ();
339 if (members.is_member (handle))
342 members = this.subscribe.group_get_remote_pending ();
343 if (members.is_member (handle))
347 if (this.publish != null)
349 members = this.publish.group_get_members ();
350 if (members.is_member (handle))
354 var persona = this.handle_persona_map[handle];
355 this.ignore_persona (persona);
358 private void ignore_persona (Tpf.Persona? persona)
363 foreach (var entry in this.channel_group_incoming_adds)
365 var channel = (Channel) entry.key;
366 var members = this.channel_group_personas_map[channel];
368 members.remove (persona);
371 foreach (var entry in this.group_outgoing_adds)
373 var name = (string) entry.key;
374 var members = this.group_outgoing_adds[name];
376 members.remove (persona);
379 var personas = new GLib.List<Persona> ();
380 personas.append (persona);
381 this.personas_removed (personas);
382 this._personas.remove (persona.iid);
386 * Remove the given persona from the server entirely
388 public override void remove_persona (Folks.Persona persona)
390 var tp_persona = (Tpf.Persona) persona;
394 this.ll.channel_group_change_membership (this.stored,
395 (Handle) tp_persona.contact.handle, false);
399 warning ("failed to remove persona '%s' (%s) from stored list: %s",
400 tp_persona.uid, tp_persona.alias, e.message);
405 this.ll.channel_group_change_membership (this.subscribe,
406 (Handle) tp_persona.contact.handle, false);
410 warning ("failed to remove persona '%s' (%s) from subscribe list: %s",
411 tp_persona.uid, tp_persona.alias, e.message);
416 this.ll.channel_group_change_membership (this.publish,
417 (Handle) tp_persona.contact.handle, false);
421 warning ("failed to remove persona '%s' (%s) from publish list: %s",
422 tp_persona.uid, tp_persona.alias, e.message);
425 var personas = new GLib.List<Persona> ();
426 personas.append (tp_persona);
427 this.personas_removed (personas);
430 /* Only non-group contact list channels should use create_personas == true,
431 * since the exposed set of Personas are meant to be filtered by them */
432 private void channel_group_pend_incoming_adds (Channel channel,
434 bool create_personas)
436 var adds_length = adds != null ? adds.length : 0;
437 if (adds_length >= 1)
439 /* this won't complete before we would add the personas to the group,
440 * so we have to buffer the contact handles below */
442 this.create_personas_from_channel_handles_async (channel, adds);
444 for (var i = 0; i < adds.length; i++)
446 var channel_handle = (Handle) adds.index (i);
447 var contact_handle = channel.group_get_handle_owner (
449 var persona = this.handle_persona_map[contact_handle];
452 HashSet<uint>? contact_handles =
453 this.channel_group_incoming_adds[channel];
454 if (contact_handles == null)
456 contact_handles = new HashSet<uint> ();
457 this.channel_group_incoming_adds[channel] =
460 contact_handles.add (contact_handle);
465 this.channel_groups_add_new_personas ();
468 private void set_up_new_group_channel (Channel channel)
470 /* hold a ref to the channel here until it's ready, so it doesn't
472 this.group_channels_unready[channel.get_identifier ()] = channel;
474 channel.notify["channel-ready"].connect ((s, p) =>
477 var name = c.get_identifier ();
479 this.groups[name] = c;
480 this.group_channels_unready.remove (name);
482 c.invalidated.connect (this.channel_invalidated_cb);
483 c.group_members_changed.connect (
484 this.group_channel_group_members_changed_cb);
486 unowned IntSet members = c.group_get_members ();
489 this.channel_group_pend_incoming_adds (c, members.to_array (),
495 private void group_channel_group_members_changed_cb (Channel channel,
497 /* FIXME: Array<uint> => Array<Handle>; parser bug */
499 Array<uint>? removed,
500 Array<uint>? local_pending,
501 Array<uint>? remote_pending,
506 this.channel_group_pend_incoming_adds (channel, added, false);
508 /* FIXME: continue for the other arrays */
511 internal async void change_group_membership (Folks.Persona persona,
512 string group, bool is_member)
514 var tp_persona = (Tpf.Persona) persona;
515 var channel = this.groups[group];
516 var change_map = is_member ? this.group_outgoing_adds :
517 this.group_outgoing_removes;
518 var change_set = change_map[group];
520 if (change_set == null)
522 change_set = new HashSet<Tpf.Persona> ();
523 change_map[group] = change_set;
525 change_set.add (tp_persona);
529 /* the changes queued above will be resolve in the NewChannels handler
531 this.ll.connection_create_group_async (this.account.get_connection (),
536 /* the channel is already ready, so resolve immediately */
537 this.channel_group_changes_resolve (channel);
541 private void change_standard_contact_list_membership (Tp.Channel channel,
542 Folks.Persona persona, bool is_member)
544 var tp_persona = (Tpf.Persona) persona;
548 this.ll.channel_group_change_membership (channel,
549 (Handle) tp_persona.contact.handle, is_member);
553 warning ("failed to change persona %s contact list %s " +
555 persona.uid, channel.get_identifier (),
556 is_member ? "true" : "false");
560 private async Channel? add_standard_channel (Connection conn, string name)
562 Channel? channel = null;
564 /* FIXME: handle the error GLib.Error from this function */
567 channel = yield this.ll.connection_open_contact_list_channel_async (
572 warning ("failed to add channel '%s': %s\n", name, e.message);
574 /* XXX: assuming there's no decent way to recover from this */
579 this.set_up_new_standard_channel (channel);
584 /* FIXME: Array<uint> => Array<Handle>; parser bug */
585 private void create_personas_from_channel_handles_async (Channel channel,
586 Array<uint> channel_handles)
588 ContactFeature[] features =
591 /* XXX: also avatar token? */
595 Handle[] contact_handles = {};
596 for (var i = 0; i < channel_handles.length; i++)
598 var channel_handle = (Handle) channel_handles.index (i);
599 var contact_handle = channel.group_get_handle_owner (channel_handle);
601 if (this.handle_persona_map[contact_handle] == null)
602 contact_handles += contact_handle;
605 /* FIXME: we have to use 'this' as the weak object because the
606 * weak object gets passed into the underlying callback as the
607 * object instance; there may be a way to fix this with the
608 * instance_pos directive, but I couldn't get it to work */
609 if (contact_handles.length > 0)
610 this.conn.get_contacts_by_handle (contact_handles, features,
611 this.get_contacts_by_handle_cb, this);
614 private void get_contacts_by_handle_cb (Connection connection,
616 [CCode (array_length = false)]
619 [CCode (array_length = false)]
622 GLib.Object weak_object)
625 warning ("failed to retrieve contacts for handles:");
627 for (var i = 0; i < n_failed; i++)
629 Handle h = failed[i];
630 warning (" %u", (uint) h);
633 /* we have to manually pass the length since we don't get it */
634 this.add_new_personas_from_contacts (contacts, n_contacts);
637 private async GLib.List<Tpf.Persona>? create_personas_from_contact_ids (
638 string[] contact_ids) throws GLib.Error
640 ContactFeature[] features =
643 /* XXX: also avatar token? */
647 if (contact_ids.length > 0)
649 unowned GLib.List<Tp.Contact> contacts =
650 yield this.ll.connection_get_contacts_by_id_async (
651 this.conn, contact_ids, features);
653 GLib.List<Persona> personas = new GLib.List<Persona> ();
655 string err_format = "";
656 unowned GLib.List<Tp.Contact> l;
657 for (l = contacts; l != null; l = l.next)
659 var contact = l.data;
662 var persona = new Tpf.Persona (contact, this);
663 personas.prepend (persona);
668 err_format = "failed to create %u personas:\n";
670 err_format = "%s '%s' (%p): %s\n".printf (
671 err_format, contact.alias, contact, e.message);
678 throw new Folks.PersonaStoreError.CREATE_FAILED (err_format,
688 private void add_new_personas_from_contacts (Contact[] contacts,
691 var personas_new = new HashTable<string, Persona> (str_hash, str_equal);
692 for (var i = 0; i < n_contacts; i++)
694 var contact = contacts[i];
698 var persona = new Tpf.Persona (contact, this);
699 if (this._personas.lookup (persona.iid) == null)
701 personas_new.insert (persona.iid, persona);
703 this._personas.insert (persona.iid, persona);
704 this.handle_persona_map[contact.get_handle ()] = persona;
709 warning ("failed to create persona from contact '%s' (%p)",
710 contact.alias, contact);
714 this.channel_groups_add_new_personas ();
716 if (personas_new.size () >= 1)
718 GLib.List<Persona> personas = personas_new.get_values ();
719 this.personas_added (personas);
723 private void channel_groups_add_new_personas ()
725 foreach (var entry in this.channel_group_incoming_adds)
727 var channel = (Channel) entry.key;
728 var members_added = new GLib.List<Persona> ();
730 HashSet<Persona> members = this.channel_group_personas_map[channel];
732 members = new HashSet<Persona> ();
734 var contact_handles = entry.value;
735 if (contact_handles != null && contact_handles.size > 0)
737 var contact_handles_added = new HashSet<uint> ();
738 foreach (var contact_handle in contact_handles)
740 var persona = this.handle_persona_map[contact_handle];
743 members.add (persona);
744 members_added.prepend (persona);
745 contact_handles_added.add (contact_handle);
749 foreach (var handle in contact_handles_added)
750 contact_handles.remove (handle);
753 if (members.size > 0)
754 this.channel_group_personas_map[channel] = members;
756 var name = channel.get_identifier ();
757 if (this.group_is_display_group (name) &&
758 members_added.length () > 0)
760 members_added.reverse ();
761 this.group_members_changed (name, members_added, null);
766 private bool group_is_display_group (string group)
768 for (var i = 0; i < this.undisplayed_groups.length; i++)
770 if (this.undisplayed_groups[i] == group)
777 public override async Folks.Persona? add_persona_from_details (
778 HashTable<string, string> details) throws Folks.PersonaStoreError
780 var contact_id = details.lookup ("contact");
781 if (contact_id == null)
783 throw new PersonaStoreError.INVALID_ARGUMENT (
784 "persona store (%s, %s) requires the following details:\n" +
785 " contact (provided: '%s')\n",
786 this.type_id, this.id, contact_id);
789 string[] contact_ids = new string[1];
790 contact_ids[0] = contact_id;
794 var personas = yield create_personas_from_contact_ids (
797 if (personas != null && personas.length () == 1)
799 var persona = personas.data;
801 if (this.subscribe != null)
802 change_standard_contact_list_membership (subscribe, persona,
805 if (this.publish != null)
807 var flags = publish.group_get_flags ();
808 if ((flags & ChannelGroupFlags.CAN_ADD) ==
809 ChannelGroupFlags.CAN_ADD)
811 change_standard_contact_list_membership (publish, persona,
820 warning ("requested a single persona, but got %u back",
821 personas == null ? 0 : personas.length ());
826 warning ("failed to add a persona from details: %s", e.message);