Adjust to TelepathyGLib.ConnectionContactsByHandleCb implicitly passing the array...
[platform/upstream/folks.git] / backends / telepathy / tpf-persona-store.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  *       Philip Withnall <philip.withnall@collabora.co.uk>
20  */
21
22 using GLib;
23 using Gee;
24 using TelepathyGLib;
25 using TelepathyGLib.ContactFeature;
26 using Folks;
27
28 /**
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
31  * subscribed
32  * [[http://people.collabora.co.uk/~danni/telepathy-book/chapter.channel.html|channels]]
33  * of the account.
34  */
35 public class Tpf.PersonaStore : Folks.PersonaStore
36 {
37   private string[] undisplayed_groups = { "publish", "stored", "subscribe" };
38
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;
58
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);
62
63
64   /**
65    * The Telepathy account this store is based upon.
66    */
67   [Property(nick = "basis account",
68       blurb = "Telepathy account this store is based upon")]
69   public Account account { get; construct; }
70
71   /**
72    * {@inheritDoc}
73    */
74   public override string type_id { get; private set; }
75
76   /**
77    * {@inheritDoc}
78    */
79   public override string id { get; private set; }
80
81   /**
82    * {@inheritDoc}
83    */
84   public override HashTable<string, Persona> personas
85     {
86       get { return this._personas; }
87     }
88
89   /**
90    * Create a new PersonaStore.
91    *
92    * Create a new persona store to store the {@link Persona}s for the contacts
93    * in the Telepathy account provided by `account`.
94    */
95   public PersonaStore (Account account)
96     {
97       Object (account: account);
98
99       this.type_id = "telepathy";
100       this.id = account.get_object_path ();
101
102       this._personas = new HashTable<string, Persona> (str_hash,
103           str_equal);
104       this.conn = null;
105       this.handle_persona_map = new HashMap<uint, Persona> ();
106       this.channel_group_personas_map = new HashMap<Channel, HashSet<Persona>> (
107           );
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>> (
111           );
112       this.publish = null;
113       this.stored = null;
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 ();
121
122       this.account_manager.account_disabled.connect ((a) =>
123         {
124           if (this.account == a)
125             this.removed ();
126         });
127       this.account_manager.account_removed.connect ((a) =>
128         {
129           if (this.account == a)
130             this.removed ();
131         });
132       this.account_manager.account_validity_changed.connect ((a, valid) =>
133         {
134           if (!valid && this.account == a)
135             this.removed ();
136         });
137
138       this.account.status_changed.connect (this.account_status_changed_cb);
139
140       TelepathyGLib.ConnectionStatusReason reason;
141       var status = this.account.get_connection_status (out reason);
142       /* immediately handle accounts which are not currently being disconnected
143        */
144       if (status != TelepathyGLib.ConnectionStatus.DISCONNECTED)
145         {
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,
150               details);
151         }
152
153       try
154         {
155           this.logger = new Logger (this.id);
156           this.logger.invalidated.connect (() =>
157             {
158               warning ("lost connection to the telepathy-logger service");
159               this.logger = null;
160             });
161           this.logger.favourite_contacts_changed.connect (
162               this.favourite_contacts_changed_cb);
163         }
164       catch (DBus.Error e)
165         {
166           warning ("couldn't connect to the telepathy-logger service");
167           this.logger = null;
168         }
169     }
170
171   private async void initialise_favourite_contacts ()
172     {
173       /* Get an initial set of favourite contacts */
174       try
175         {
176           string[] contacts = yield this.logger.get_favourite_contacts ();
177
178           if (contacts.length == 0)
179             return;
180
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) =>
187               {
188                 try
189                   {
190                     this.change_favourites_by_request_handles (nh, h, i, e,
191                         true);
192                   }
193                 catch (GLib.Error e)
194                   {
195                     warning ("couldn't get list of favourite contacts: %s",
196                         e.message);
197                   }
198               },
199             this);
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
202            * callback. */
203         }
204       catch (DBus.Error e)
205         {
206           warning ("couldn't get list of favourite contacts: %s", e.message);
207         }
208     }
209
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
213     {
214       if (error != null)
215         throw error;
216
217       for (var i = 0; i < n_handles; i++)
218         {
219           Handle h = handles[i];
220           Persona p = this.handle_persona_map[h];
221
222           /* Add/Remove the handle to the set of favourite handles, since we
223            * might not have the corresponding contact yet */
224           if (add)
225             this.favourite_handles.add (h);
226           else
227             this.favourite_handles.remove (h);
228
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)
235             {
236               warning ("unknown persona '%s' in favourites list", ids[i]);
237               continue;
238             }
239
240           /* Mark or unmark the persona as a favourite */
241           if (p != null)
242             p.is_favourite = add;
243         }
244     }
245
246   private void favourite_contacts_changed_cb (string[] added, string[] removed)
247     {
248       /* Don't listen to favourites updates if the account is disconnected. */
249       if (this.conn == null)
250         return;
251
252       /* Add favourites */
253       if (added.length > 0)
254         {
255           this.conn.request_handles (-1, HandleType.CONTACT, added,
256               (c, ht, nh, h, i, e, w) =>
257                 {
258                   try
259                     {
260                       this.change_favourites_by_request_handles (nh, h, i, e,
261                           true);
262                     }
263                   catch (GLib.Error e)
264                     {
265                       warning ("couldn't add favourite contacts: %s",
266                           e.message);
267                     }
268                 },
269               this);
270         }
271
272       /* Remove favourites */
273       if (removed.length > 0)
274         {
275           this.conn.request_handles (-1, HandleType.CONTACT, removed,
276               (c, ht, nh, h, i, e, w) =>
277                 {
278                   try
279                     {
280                       this.change_favourites_by_request_handles (nh, h, i, e,
281                           false);
282                     }
283                   catch (GLib.Error e)
284                     {
285                       warning ("couldn't remove favourite contacts: %s",
286                           e.message);
287                     }
288                 },
289               this);
290         }
291     }
292
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)
296     {
297       if (new_status != TelepathyGLib.ConnectionStatus.CONNECTED)
298         return;
299
300       var conn = this.account.get_connection ();
301       conn.call_when_ready (this.connection_ready_cb);
302     }
303
304   private void connection_ready_cb (Connection conn, GLib.Error? error)
305     {
306       this.ll.connection_connect_to_new_group_channels (conn,
307           this.new_group_channels_cb);
308
309       this.add_standard_channel (conn, "publish");
310       this.add_standard_channel (conn, "stored");
311       this.add_standard_channel (conn, "subscribe");
312       this.conn = conn;
313
314       /* We can only initialise the favourite contacts once we've got conn */
315       this.initialise_favourite_contacts.begin ();
316     }
317
318   private void new_group_channels_cb (void *data)
319     {
320       var channel = (Channel) data;
321       if (channel == null)
322         {
323           warning ("error creating channel for NewChannels signal");
324           return;
325         }
326
327       this.set_up_new_group_channel (channel);
328       this.channel_group_changes_resolve (channel);
329     }
330
331   private void channel_group_changes_resolve (Channel channel)
332     {
333       var group = channel.get_identifier ();
334
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);
338
339       if (this.group_outgoing_removes[group] != null)
340         change_maps.set (this.group_outgoing_removes[group], false);
341
342       if (change_maps.size < 1)
343         return;
344
345       foreach (var entry in change_maps)
346         {
347           var changes = entry.key;
348
349           foreach (var persona in changes)
350             {
351               try
352                 {
353                   this.ll.channel_group_change_membership (channel,
354                       (Handle) persona.contact.handle, entry.value);
355                 }
356               catch (GLib.Error e)
357                 {
358                   warning ("failed to change persona %s group %s membership to "
359                       + "%s",
360                       persona.uid, group, entry.value ? "true" : "false");
361                 }
362             }
363
364           changes.clear ();
365         }
366     }
367
368   private void set_up_new_standard_channel (Channel channel)
369     {
370       /* hold a ref to the channel here until it's ready, so it doesn't
371        * disappear */
372       this.standard_channels_unready[channel.get_identifier ()] = channel;
373
374       channel.notify["channel-ready"].connect ((s, p) =>
375         {
376           var c = (Channel) s;
377           var name = c.get_identifier ();
378
379           if (name == "publish")
380             {
381               this.publish = c;
382
383               c.group_members_changed.connect (
384                   this.publish_channel_group_members_changed_cb);
385             }
386           else if (name == "stored")
387             {
388               this.stored = c;
389
390               c.group_members_changed.connect (
391                   this.stored_channel_group_members_changed_cb);
392             }
393           else if (name == "subscribe")
394             {
395               this.subscribe = c;
396
397               c.group_members_changed.connect (
398                   this.subscribe_channel_group_members_changed_cb);
399             }
400
401           this.standard_channels_unready.remove (name);
402
403           c.invalidated.connect (this.channel_invalidated_cb);
404
405           unowned IntSet? members = c.group_get_members ();
406           if (members != null)
407             {
408               this.channel_group_pend_incoming_adds.begin (c,
409                   members.to_array (), true);
410             }
411         });
412     }
413
414   private void publish_channel_group_members_changed_cb (Channel channel,
415       string message,
416       /* FIXME: Array<uint> => Array<Handle>; parser bug */
417       Array<uint>? added,
418       Array<uint>? removed,
419       Array<uint>? local_pending,
420       Array<uint>? remote_pending,
421       uint actor,
422       uint reason)
423     {
424       if (added != null)
425         this.channel_group_pend_incoming_adds.begin (channel, added, true);
426
427       /* we refuse to send these contacts our presence, so remove them */
428       for (var i = 0; i < removed.length; i++)
429         {
430           var handle = removed.index (i);
431           this.ignore_by_handle_if_needed (handle);
432         }
433
434       /* FIXME: continue for the other arrays */
435     }
436
437   private void stored_channel_group_members_changed_cb (Channel channel,
438       string message,
439       /* FIXME: Array<uint> => Array<Handle>; parser bug */
440       Array<uint>? added,
441       Array<uint>? removed,
442       Array<uint>? local_pending,
443       Array<uint>? remote_pending,
444       uint actor,
445       uint reason)
446     {
447       if (added != null)
448         this.channel_group_pend_incoming_adds.begin (channel, added, true);
449
450       for (var i = 0; i < removed.length; i++)
451         {
452           var handle = removed.index (i);
453           this.ignore_by_handle_if_needed (handle);
454         }
455     }
456
457   private void subscribe_channel_group_members_changed_cb (Channel channel,
458       string message,
459       /* FIXME: Array<uint> => Array<Handle>; parser bug */
460       Array<uint>? added,
461       Array<uint>? removed,
462       Array<uint>? local_pending,
463       Array<uint>? remote_pending,
464       uint actor,
465       uint reason)
466     {
467       if (added != null)
468         {
469           this.channel_group_pend_incoming_adds.begin (channel, added, true);
470
471           /* expose ourselves to anyone we can see */
472           if (this.publish != null)
473             {
474               this.channel_group_pend_incoming_adds.begin (this.publish, added,
475                   true);
476             }
477         }
478
479       /* these contacts refused to send us their presence, so remove them */
480       for (var i = 0; i < removed.length; i++)
481         {
482           var handle = removed.index (i);
483           this.ignore_by_handle_if_needed (handle);
484         }
485
486       /* FIXME: continue for the other arrays */
487     }
488
489   private void channel_invalidated_cb (Proxy proxy, uint domain, int code,
490       string message)
491     {
492       var channel = (Channel) proxy;
493
494       this.channel_group_personas_map.remove (channel);
495       this.channel_group_incoming_adds.remove (channel);
496
497       if (proxy == this.publish)
498         this.publish = null;
499       else if (proxy == this.subscribe)
500         this.subscribe = null;
501       else
502         {
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);
507         }
508     }
509
510   private void ignore_by_handle_if_needed (uint handle)
511     {
512       unowned TelepathyGLib.IntSet members;
513
514       if (this.subscribe != null)
515         {
516           members = this.subscribe.group_get_members ();
517           if (members.is_member (handle))
518             return;
519
520           members = this.subscribe.group_get_remote_pending ();
521           if (members.is_member (handle))
522             return;
523         }
524
525       if (this.publish != null)
526         {
527           members = this.publish.group_get_members ();
528           if (members.is_member (handle))
529             return;
530         }
531
532       this.ignore_by_handle (handle);
533     }
534
535   private void ignore_by_handle (uint handle)
536     {
537       var persona = this.handle_persona_map[handle];
538
539       /*
540        * remove all handle-keyed entries
541        */
542       this.handle_persona_map.remove (handle);
543
544       /* skip channel_group_incoming_adds because they occurred after removal */
545
546       if (persona == null)
547         return;
548
549       /*
550        * remove all persona-keyed entries
551        */
552       foreach (var entry in this.channel_group_personas_map)
553         {
554           var channel = (Channel) entry.key;
555           var members = this.channel_group_personas_map[channel];
556           if (members != null)
557             members.remove (persona);
558         }
559
560       foreach (var entry in this.group_outgoing_adds)
561         {
562           var name = (string) entry.key;
563           var members = this.group_outgoing_adds[name];
564           if (members != null)
565             members.remove (persona);
566         }
567
568       var personas = new GLib.List<Persona> ();
569       personas.append (persona);
570       this.personas_removed (personas);
571       this._personas.remove (persona.iid);
572     }
573
574   /**
575    * {@inheritDoc}
576    */
577   public override void remove_persona (Folks.Persona persona)
578     {
579       var tp_persona = (Tpf.Persona) persona;
580
581       try
582         {
583           this.ll.channel_group_change_membership (this.stored,
584               (Handle) tp_persona.contact.handle, false);
585         }
586       catch (GLib.Error e)
587         {
588           warning ("failed to remove persona '%s' (%s) from stored list: %s",
589               tp_persona.uid, tp_persona.alias, e.message);
590         }
591
592       try
593         {
594           this.ll.channel_group_change_membership (this.subscribe,
595               (Handle) tp_persona.contact.handle, false);
596         }
597       catch (GLib.Error e)
598         {
599           warning ("failed to remove persona '%s' (%s) from subscribe list: %s",
600               tp_persona.uid, tp_persona.alias, e.message);
601         }
602
603       try
604         {
605           this.ll.channel_group_change_membership (this.publish,
606               (Handle) tp_persona.contact.handle, false);
607         }
608       catch (GLib.Error e)
609         {
610           warning ("failed to remove persona '%s' (%s) from publish list: %s",
611               tp_persona.uid, tp_persona.alias, e.message);
612         }
613
614       /* the contact will be actually removed (and signaled) when we hear back
615        * from the server */
616     }
617
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,
621       Array<uint> adds,
622       bool create_personas)
623     {
624       var adds_length = adds != null ? adds.length : 0;
625       if (adds_length >= 1)
626         {
627           if (create_personas)
628             {
629               yield this.create_personas_from_channel_handles_async (channel,
630                   adds);
631             }
632
633           for (var i = 0; i < adds.length; i++)
634             {
635               var channel_handle = (Handle) adds.index (i);
636               var contact_handle = channel.group_get_handle_owner (
637                 channel_handle);
638               var persona = this.handle_persona_map[contact_handle];
639               if (persona == null)
640                 {
641                   HashSet<uint>? contact_handles =
642                       this.channel_group_incoming_adds[channel];
643                   if (contact_handles == null)
644                     {
645                       contact_handles = new HashSet<uint> ();
646                       this.channel_group_incoming_adds[channel] =
647                           contact_handles;
648                     }
649                   contact_handles.add (contact_handle);
650                 }
651             }
652         }
653
654       this.channel_groups_add_new_personas ();
655     }
656
657   private void set_up_new_group_channel (Channel channel)
658     {
659       /* hold a ref to the channel here until it's ready, so it doesn't
660        * disappear */
661       this.group_channels_unready[channel.get_identifier ()] = channel;
662
663       channel.notify["channel-ready"].connect ((s, p) =>
664         {
665           var c = (Channel) s;
666           var name = c.get_identifier ();
667
668           this.groups[name] = c;
669           this.group_channels_unready.remove (name);
670
671           c.invalidated.connect (this.channel_invalidated_cb);
672           c.group_members_changed_detailed.connect (
673             this.channel_group_members_changed_detailed_cb);
674
675           unowned IntSet members = c.group_get_members ();
676           if (members != null)
677             {
678               this.channel_group_pend_incoming_adds.begin (c,
679                 members.to_array (), false);
680             }
681         });
682     }
683
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)
691     {
692       if (added != null)
693         this.channel_group_pend_incoming_adds.begin (channel, added, false);
694
695       /* FIXME: continue for the other arrays */
696     }
697
698   internal async void change_group_membership (Folks.Persona persona,
699       string group, bool is_member)
700     {
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];
706
707       if (change_set == null)
708         {
709           change_set = new HashSet<Tpf.Persona> ();
710           change_map[group] = change_set;
711         }
712       change_set.add (tp_persona);
713
714       if (channel == null)
715         {
716           /* the changes queued above will be resolve in the NewChannels handler
717            */
718           this.ll.connection_create_group_async (this.account.get_connection (),
719               group);
720         }
721       else
722         {
723           /* the channel is already ready, so resolve immediately */
724           this.channel_group_changes_resolve (channel);
725         }
726     }
727
728   private void change_standard_contact_list_membership (
729       TelepathyGLib.Channel channel, Folks.Persona persona, bool is_member)
730     {
731       var tp_persona = (Tpf.Persona) persona;
732
733       try
734         {
735           this.ll.channel_group_change_membership (channel,
736               (Handle) tp_persona.contact.handle, is_member);
737         }
738       catch (GLib.Error e)
739         {
740           warning ("failed to change persona %s contact list %s " +
741               "membership to %s",
742               persona.uid, channel.get_identifier (),
743               is_member ? "true" : "false");
744         }
745     }
746
747   private async Channel? add_standard_channel (Connection conn, string name)
748     {
749       Channel? channel = null;
750
751       /* FIXME: handle the error GLib.Error from this function */
752       try
753         {
754           channel = yield this.ll.connection_open_contact_list_channel_async (
755               conn, name);
756         }
757       catch (GLib.Error e)
758         {
759           warning ("failed to add channel '%s': %s\n", name, e.message);
760
761           /* XXX: assuming there's no decent way to recover from this */
762
763           return null;
764         }
765
766       this.set_up_new_standard_channel (channel);
767
768       return channel;
769     }
770
771   /* FIXME: Array<uint> => Array<Handle>; parser bug */
772   private async void create_personas_from_channel_handles_async (
773       Channel channel,
774       Array<uint> channel_handles)
775     {
776       ContactFeature[] features =
777         {
778           ALIAS,
779           /* XXX: also avatar token? */
780           PRESENCE
781         };
782
783       uint[] contact_handles = {};
784       for (var i = 0; i < channel_handles.length; i++)
785         {
786           var channel_handle = (Handle) channel_handles.index (i);
787           var contact_handle = channel.group_get_handle_owner (channel_handle);
788
789           if (this.handle_persona_map[contact_handle] == null)
790             contact_handles += contact_handle;
791         }
792
793       try
794         {
795           if (contact_handles.length < 1)
796             return;
797
798           unowned GLib.List<TelepathyGLib.Contact> contacts =
799               yield this.ll.connection_get_contacts_by_handle_async (
800                   this.conn, contact_handles, features);
801
802           if (contacts == null || contacts.length () < 1)
803             return;
804
805           var contacts_array = new TelepathyGLib.Contact[contacts.length ()];
806           var j = 0;
807           unowned GLib.List<TelepathyGLib.Contact> l = contacts;
808           for (; l != null; l = l.next)
809             {
810               contacts_array[j] = l.data;
811               j++;
812             }
813
814           this.add_new_personas_from_contacts (contacts_array);
815         }
816       catch (GLib.Error e)
817         {
818           warning ("failed to create personas from incoming contacts in " +
819               "channel '%s': %s",
820               channel.get_identifier (), e.message);
821         }
822     }
823
824   private async GLib.List<Tpf.Persona>? create_personas_from_contact_ids (
825       string[] contact_ids) throws GLib.Error
826     {
827       ContactFeature[] features =
828         {
829           ALIAS,
830           /* XXX: also avatar token? */
831           PRESENCE
832         };
833
834       if (contact_ids.length > 0)
835         {
836           unowned GLib.List<TelepathyGLib.Contact> contacts =
837               yield this.ll.connection_get_contacts_by_id_async (
838                   this.conn, contact_ids, features);
839
840           GLib.List<Persona> personas = new GLib.List<Persona> ();
841           uint err_count = 0;
842           string err_format = "";
843           unowned GLib.List<TelepathyGLib.Contact> l;
844           for (l = contacts; l != null; l = l.next)
845             {
846               var contact = l.data;
847               try
848                 {
849                   var persona = this.add_persona_from_contact (contact);
850                   if (persona != null)
851                     personas.prepend (persona);
852                 }
853               catch (TelepathyGLib.Error e)
854                 {
855                   if (err_count == 0)
856                     err_format = "failed to create %u personas:\n";
857
858                   err_format = "%s        '%s' (%p): %s\n".printf (
859                     err_format, contact.alias, contact, e.message);
860                   err_count++;
861                 }
862             }
863
864           if (err_count > 0)
865             {
866               throw new Folks.PersonaStoreError.CREATE_FAILED (err_format,
867                   err_count);
868             }
869
870           if (personas != null)
871             this.personas_added (personas);
872
873           return personas;
874         }
875
876       return null;
877     }
878
879   private Tpf.Persona? add_persona_from_contact (Contact contact)
880       throws TelepathyGLib.Error
881     {
882       var h = contact.get_handle ();
883       if (this.handle_persona_map[h] == null)
884         {
885           var persona = new Tpf.Persona (contact, this);
886
887           this._personas.insert (persona.iid, persona);
888           this.handle_persona_map[h] = persona;
889
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
893            * favourite. */
894           persona.is_favourite = this.favourite_handles.contains (h);
895
896           return persona;
897         }
898
899       return null;
900     }
901
902
903   private void add_new_personas_from_contacts (Contact[] contacts)
904     {
905       GLib.List<Persona> personas = new GLib.List<Persona> ();
906       foreach (Contact contact in contacts)
907         {
908           try
909             {
910               var persona = this.add_persona_from_contact (contact);
911               if (persona != null)
912                 personas.prepend (persona);
913             }
914           catch (Tpf.PersonaError e)
915             {
916               warning ("failed to create persona from contact '%s' (%p)",
917                   contact.alias, contact);
918             }
919         }
920
921       this.channel_groups_add_new_personas ();
922
923       if (personas != null)
924         this.personas_added (personas);
925     }
926
927   private void channel_groups_add_new_personas ()
928     {
929       foreach (var entry in this.channel_group_incoming_adds)
930         {
931           var channel = (Channel) entry.key;
932           var members_added = new GLib.List<Persona> ();
933
934           HashSet<Persona> members = this.channel_group_personas_map[channel];
935           if (members == null)
936             members = new HashSet<Persona> ();
937
938           var contact_handles = entry.value;
939           if (contact_handles != null && contact_handles.size > 0)
940             {
941               var contact_handles_added = new HashSet<uint> ();
942               foreach (var contact_handle in contact_handles)
943                 {
944                   var persona = this.handle_persona_map[contact_handle];
945                   if (persona != null)
946                     {
947                       members.add (persona);
948                       members_added.prepend (persona);
949                       contact_handles_added.add (contact_handle);
950                     }
951                 }
952
953               foreach (var handle in contact_handles_added)
954                 contact_handles.remove (handle);
955             }
956
957           if (members.size > 0)
958             this.channel_group_personas_map[channel] = members;
959
960           var name = channel.get_identifier ();
961           if (this.group_is_display_group (name) &&
962               members_added.length () > 0)
963             {
964               members_added.reverse ();
965               this.group_members_changed (name, members_added, null);
966             }
967         }
968     }
969
970   private bool group_is_display_group (string group)
971     {
972       for (var i = 0; i < this.undisplayed_groups.length; i++)
973         {
974           if (this.undisplayed_groups[i] == group)
975             return false;
976         }
977
978       return true;
979     }
980
981   /**
982    * {@inheritDoc}
983    */
984   public override async Folks.Persona? add_persona_from_details (
985       HashTable<string, string> details) throws Folks.PersonaStoreError
986     {
987       var contact_id = details.lookup ("contact");
988       if (contact_id == null)
989         {
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);
994         }
995
996       string[] contact_ids = new string[1];
997       contact_ids[0] = contact_id;
998
999       try
1000         {
1001           var personas = yield create_personas_from_contact_ids (
1002               contact_ids);
1003
1004           if (personas != null && personas.length () == 1)
1005             {
1006               var persona = personas.data;
1007
1008               if (this.subscribe != null)
1009                 change_standard_contact_list_membership (subscribe, persona,
1010                     true);
1011
1012               if (this.publish != null)
1013                 {
1014                   var flags = publish.group_get_flags ();
1015                   if ((flags & ChannelGroupFlags.CAN_ADD) ==
1016                       ChannelGroupFlags.CAN_ADD)
1017                     {
1018                       change_standard_contact_list_membership (publish, persona,
1019                           true);
1020                     }
1021                 }
1022
1023               return persona;
1024             }
1025           else
1026             {
1027               warning ("requested a single persona, but got %u back",
1028                   personas == null ? 0 : personas.length ());
1029             }
1030         }
1031       catch (GLib.Error e)
1032         {
1033           warning ("failed to add a persona from details: %s", e.message);
1034         }
1035
1036       return null;
1037     }
1038
1039   /**
1040    * Change the favourite status of a persona in this store.
1041    *
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.
1044    */
1045   internal async void change_is_favourite (Folks.Persona persona,
1046       bool is_favourite)
1047     {
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)
1051         {
1052           warning ("failed to change favourite without connection to the " +
1053                    "telepathy-logger service");
1054           return;
1055         }
1056
1057       try
1058         {
1059           /* Add or remove the persona to the list of favourites as
1060            * appropriate. */
1061           var id = ((Tpf.Persona) persona).contact.get_identifier ();
1062
1063           if (is_favourite)
1064             yield this.logger.add_favourite_contact (id);
1065           else
1066             yield this.logger.remove_favourite_contact (id);
1067         }
1068       catch (DBus.Error e)
1069         {
1070           warning ("failed to change a persona's favourite status");
1071         }
1072     }
1073
1074   internal async void change_alias (Tpf.Persona persona, string alias)
1075     {
1076       debug ("Changing alias of persona %u to '%s'.", persona.contact.handle,
1077           alias);
1078       this.ll.connection_set_contact_alias (this.conn,
1079           (Handle) persona.contact.handle, alias);
1080     }
1081 }