~ubuntu-branches/ubuntu/saucy/folks/saucy

« back to all changes in this revision

Viewing changes to backends/telepathy/lib/tpf-persona-store.vala

  • Committer: Package Import Robot
  • Author(s): Sjoerd Simons
  • Date: 2012-03-30 20:03:30 UTC
  • mfrom: (32.1.2 precise)
  • Revision ID: package-import@ubuntu.com-20120330200330-l61hwayt5vjz1zcf
Tags: 0.6.8-2
* d/p/0001-tpf-persona-use-tp_connection_get_account.patch
  + Added, fixes crash when accounts are disconnecting/connecting
* Target unstable

Show diffs side-by-side

added added

removed removed

Lines of Context:
17
17
 * Authors:
18
18
 *       Travis Reitter <travis.reitter@collabora.co.uk>
19
19
 *       Philip Withnall <philip.withnall@collabora.co.uk>
20
 
 *       Xavier Claessens <xavier.claessens@collabora.co.uk>
21
20
 */
22
21
 
23
22
using GLib;
24
23
using Gee;
25
24
using TelepathyGLib;
26
25
using Folks;
27
 
using Zeitgeist;
 
26
 
28
27
extern const string G_LOG_DOMAIN;
29
28
extern const string BACKEND_NAME;
30
29
 
31
30
/**
32
31
 * A persona store which is associated with a single Telepathy account. It will
33
 
 * create {@link Persona}s for each of the contacts in the account's
34
 
 * contact list.
35
 
 *
36
 
 * User must define contact features it wants on the #TpSimpleClientFactory of
37
 
 * the default #TpAccountManager returned by tp_account_manager_dup() *before*
38
 
 * preparing telepathy stores. Note that this is a behaviour change since
39
 
 * 0.7.0, folks won't force preparing any feature anymore.
 
32
 * create {@link Persona}s for each of the contacts in the published, stored or
 
33
 * subscribed
 
34
 * [[http://people.collabora.co.uk/~danni/telepathy-book/chapter.channel.html|channels]]
 
35
 * of the account.
40
36
 */
41
37
public class Tpf.PersonaStore : Folks.PersonaStore
42
38
{
43
 
  private string[] _always_writeable_properties = {};
44
 
 
45
 
  /* Sets of Personas exposed by this store.
46
 
   * This is the roster + self_contact */
 
39
  /* FIXME: expose the interface strings in the introspected tp-glib bindings
 
40
   */
 
41
  private static string _tp_channel_iface = "org.freedesktop.Telepathy.Channel";
 
42
  private static string _tp_channel_contact_list_type = _tp_channel_iface +
 
43
      ".Type.ContactList";
 
44
  private static string _tp_channel_channel_type = _tp_channel_iface +
 
45
      ".ChannelType";
 
46
  private static string _tp_channel_handle_type = _tp_channel_iface +
 
47
      ".TargetHandleType";
 
48
  private static string[] _undisplayed_groups =
 
49
      {
 
50
        "publish",
 
51
        "stored",
 
52
        "subscribe"
 
53
      };
 
54
  private static ContactFeature[] _contact_features =
 
55
      {
 
56
        ContactFeature.ALIAS,
 
57
        ContactFeature.AVATAR_DATA,
 
58
        ContactFeature.AVATAR_TOKEN,
 
59
        ContactFeature.CAPABILITIES,
 
60
        ContactFeature.CLIENT_TYPES,
 
61
        ContactFeature.PRESENCE,
 
62
        ContactFeature.CONTACT_INFO
 
63
      };
 
64
 
 
65
  private static GLib.Quark[] _connection_features =
 
66
      {
 
67
        TelepathyGLib.Connection.get_feature_quark_contact_info (),
 
68
        0
 
69
      };
 
70
 
 
71
  private const string[] _always_writeable_properties =
 
72
    {
 
73
      "is-favourite"
 
74
    };
 
75
 
47
76
  private HashMap<string, Persona> _personas;
48
77
  private Map<string, Persona> _personas_ro;
49
78
  private HashSet<Persona> _persona_set;
50
 
 
51
 
  /* Map from weakly-referenced TpContacts to their Persona.
52
 
   * This map contains all the TpContact we know about, could be more than the
53
 
   * the roster. Persona is kept in the map until its TpContact is disposed. */
54
 
  private HashMap<unowned Contact, Persona> _contact_persona_map;
55
 
 
56
 
  /* TpContact IDs. Note that this should *not* be cleared in _reset().
57
 
   * See bgo#630822. */
58
 
  private HashSet<string> _favourite_ids = new HashSet<string> ();
59
 
 
 
79
  /* universal, contact owner handles (not channel-specific) */
 
80
  private HashMap<uint, Persona> _handle_persona_map;
 
81
  /* Map from weakly-referenced TpContacts to their original TpHandles;
 
82
   * necessary because the handles get set to 0 before our weak_notify callback
 
83
   * is called, and we need the handle to remove the contact. */
 
84
  private HashMap<unowned Contact, uint> _weakly_referenced_contacts;
 
85
  private HashMap<Channel, HashSet<Persona>> _channel_group_personas_map;
 
86
  private HashMap<Channel, HashSet<uint>> _channel_group_incoming_adds;
 
87
  private HashMap<string, HashSet<Tpf.Persona>> _group_outgoing_adds;
 
88
  private HashMap<string, HashSet<Tpf.Persona>> _group_outgoing_removes;
 
89
  private HashMap<string, Channel> _standard_channels_unready;
 
90
  private HashMap<string, Channel> _group_channels_unready;
 
91
  private HashMap<string, Channel> _groups;
 
92
  /* FIXME: Should be HashSet<Handle> */
 
93
  private HashSet<uint> _favourite_handles;
 
94
  private Channel _publish;
 
95
  private Channel _stored;
 
96
  private Channel _subscribe;
60
97
  private Connection _conn;
61
98
  private AccountManager? _account_manager; /* only null before prepare() */
62
99
  private Logger _logger;
63
 
  private Persona? _self_persona;
64
 
 
65
 
  /* Connection's capabilities */
 
100
  private Contact? _self_contact;
66
101
  private MaybeBool _can_add_personas = MaybeBool.UNSET;
67
102
  private MaybeBool _can_alias_personas = MaybeBool.UNSET;
68
103
  private MaybeBool _can_group_personas = MaybeBool.UNSET;
69
104
  private MaybeBool _can_remove_personas = MaybeBool.UNSET;
70
 
 
71
105
  private bool _is_prepared = false;
72
106
  private bool _prepare_pending = false;
73
107
  private bool _is_quiescent = false;
74
 
  private bool _got_initial_members = false;
75
 
  private bool _got_initial_self_contact = false;
76
 
 
 
108
  private bool _got_stored_channel_members = false;
 
109
  private bool _got_self_handle = false;
77
110
  private Debug _debug;
78
111
  private PersonaStoreCache _cache;
79
112
  private Cancellable? _load_cache_cancellable = null;
 
113
  private bool _cached = false;
80
114
 
81
115
  /* marshalled from ContactInfo.SupportedFields */
82
116
  internal HashSet<string> _supported_fields;
83
117
  internal Set<string> _supported_fields_ro;
 
118
  internal signal void group_members_changed (string group,
 
119
      GLib.List<Persona>? added, GLib.List<Persona>? removed);
 
120
  internal signal void group_removed (string group, GLib.Error? error);
84
121
 
85
122
  private Account _account;
86
123
 
87
 
  private Zeitgeist.Log? _log= null;
88
 
  private Zeitgeist.Monitor? _monitor = null;
89
 
 
90
124
  /**
91
125
   * The Telepathy account this store is based upon.
92
126
   */
193
227
 
194
228
  private void _notify_if_is_quiescent ()
195
229
    {
196
 
      if (this._got_initial_members == true &&
197
 
          this._got_initial_self_contact == true &&
 
230
      if (this._got_stored_channel_members == true &&
 
231
          this._got_self_handle == true &&
198
232
          this._is_quiescent == false)
199
233
        {
200
234
          this._is_quiescent = true;
202
236
        }
203
237
    }
204
238
 
205
 
  private void _force_quiescent ()
206
 
    {
207
 
        this._got_initial_self_contact = true;
208
 
        this._got_initial_members = true;
209
 
        this._notify_if_is_quiescent ();
210
 
    }
211
 
 
212
239
  /**
213
240
   * The {@link Persona}s exposed by this PersonaStore.
214
241
   *
304
331
      debug.print_key_value_pairs (domain, level,
305
332
          "ID", this.id,
306
333
          "Prepared?", this._is_prepared ? "yes" : "no",
307
 
          "Has initial members?", this._got_initial_members ? "yes" : "no",
308
 
          "Has self contact?", this._got_initial_self_contact ? "yes" : "no",
 
334
          "Has stored contact members?", this._got_stored_channel_members ? "yes" : "no",
 
335
          "Has self handle?", this._got_self_handle ? "yes" : "no",
 
336
          "Publish TpChannel", "%p".printf (this._publish),
 
337
          "Stored TpChannel", "%p".printf (this._stored),
 
338
          "Subscribe TpChannel", "%p".printf (this._subscribe),
309
339
          "TpConnection", "%p".printf (this._conn),
310
340
          "TpAccountManager", "%p".printf (this._account_manager),
311
 
          "Self-Persona", "%p".printf (this._self_persona),
 
341
          "Self-TpContact", "%p".printf (this._self_contact),
312
342
          "Can add personas?", this._format_maybe_bool (this._can_add_personas),
313
343
          "Can alias personas?",
314
344
              this._format_maybe_bool (this._can_alias_personas),
336
366
 
337
367
      debug.unindent ();
338
368
 
339
 
      debug.print_line (domain, level, "%u TpContact–Persona mappings:",
340
 
          this._contact_persona_map.size);
 
369
      debug.print_line (domain, level, "%u handle–Persona mappings:",
 
370
          this._handle_persona_map.size);
341
371
      debug.indent ();
342
372
 
343
 
      var iter1 = this._contact_persona_map.map_iterator ();
 
373
      var iter1 = this._handle_persona_map.map_iterator ();
344
374
      while (iter1.next () == true)
345
375
        {
346
376
          debug.print_line (domain, level,
347
 
              "%s → %p", iter1.get_key ().get_identifier (), iter1.get_value ());
348
 
        }
349
 
 
350
 
      debug.unindent ();
351
 
 
352
 
      debug.print_line (domain, level, "%u favourite TpContact IDs:",
353
 
          this._favourite_ids.size);
354
 
      debug.indent ();
355
 
 
356
 
      foreach (var id in this._favourite_ids)
357
 
        {
358
 
          debug.print_line (domain, level, "%s", id);
 
377
              "%u → %p", iter1.get_key (), iter1.get_value ());
 
378
        }
 
379
 
 
380
      debug.unindent ();
 
381
 
 
382
      debug.print_line (domain, level, "%u channel group Persona sets:",
 
383
          this._channel_group_personas_map.size);
 
384
      debug.indent ();
 
385
 
 
386
      var iter2 = this._channel_group_personas_map.map_iterator ();
 
387
      while (iter2.next () == true)
 
388
        {
 
389
          debug.print_heading (domain, level,
 
390
              "Channel (%p):", iter2.get_key ());
 
391
 
 
392
          debug.indent ();
 
393
 
 
394
          foreach (var persona in iter2.get_value ())
 
395
            {
 
396
              debug.print_line (domain, level, "%p", persona);
 
397
            }
 
398
 
 
399
          debug.unindent ();
 
400
        }
 
401
 
 
402
      debug.unindent ();
 
403
 
 
404
      debug.print_line (domain, level, "%u channel group incoming handle sets:",
 
405
          this._channel_group_incoming_adds.size);
 
406
      debug.indent ();
 
407
 
 
408
      var iter3 = this._channel_group_incoming_adds.map_iterator ();
 
409
      while (iter3.next () == true)
 
410
        {
 
411
          debug.print_heading (domain, level,
 
412
              "Channel (%p):", iter3.get_key ());
 
413
 
 
414
          debug.indent ();
 
415
 
 
416
          foreach (var handle in iter3.get_value ())
 
417
            {
 
418
              debug.print_line (domain, level, "%u", handle);
 
419
            }
 
420
 
 
421
          debug.unindent ();
 
422
        }
 
423
 
 
424
      debug.unindent ();
 
425
 
 
426
      debug.print_line (domain, level, "%u group outgoing add sets:",
 
427
          this._group_outgoing_adds.size);
 
428
      debug.indent ();
 
429
 
 
430
      var iter4 = this._group_outgoing_adds.map_iterator ();
 
431
      while (iter4.next () == true)
 
432
        {
 
433
          debug.print_heading (domain, level, "Group (%s):", iter4.get_key ());
 
434
 
 
435
          debug.indent ();
 
436
 
 
437
          foreach (var persona in iter4.get_value ())
 
438
            {
 
439
              debug.print_line (domain, level, "%p", persona);
 
440
            }
 
441
 
 
442
          debug.unindent ();
 
443
        }
 
444
 
 
445
      debug.unindent ();
 
446
 
 
447
      debug.print_line (domain, level, "%u group outgoing remove sets:",
 
448
          this._group_outgoing_removes.size);
 
449
      debug.indent ();
 
450
 
 
451
      var iter5 = this._group_outgoing_removes.map_iterator ();
 
452
      while (iter5.next () == true)
 
453
        {
 
454
          debug.print_heading (domain, level, "Group (%s):", iter5.get_key ());
 
455
 
 
456
          debug.indent ();
 
457
 
 
458
          foreach (var persona in iter5.get_value ())
 
459
            {
 
460
              debug.print_line (domain, level, "%p", persona);
 
461
            }
 
462
 
 
463
          debug.unindent ();
 
464
        }
 
465
 
 
466
      debug.unindent ();
 
467
 
 
468
      debug.print_line (domain, level, "%u unready standard channels:",
 
469
          this._standard_channels_unready.size);
 
470
      debug.indent ();
 
471
 
 
472
      var iter6 = this._standard_channels_unready.map_iterator ();
 
473
      while (iter6.next () == true)
 
474
        {
 
475
          debug.print_line (domain, level,
 
476
              "%s → %p", iter6.get_key (), iter6.get_value ());
 
477
        }
 
478
 
 
479
      debug.unindent ();
 
480
 
 
481
      debug.print_line (domain, level, "%u unready group channels:",
 
482
          this._group_channels_unready.size);
 
483
      debug.indent ();
 
484
 
 
485
      var iter7 = this._group_channels_unready.map_iterator ();
 
486
      while (iter7.next () == true)
 
487
        {
 
488
          debug.print_line (domain, level,
 
489
              "%s → %p", iter7.get_key (), iter7.get_value ());
 
490
        }
 
491
 
 
492
      debug.unindent ();
 
493
 
 
494
      debug.print_line (domain, level, "%u ready group channels:",
 
495
          this._groups.size);
 
496
      debug.indent ();
 
497
 
 
498
      var iter8 = this._groups.map_iterator ();
 
499
      while (iter8.next () == true)
 
500
        {
 
501
          debug.print_line (domain, level,
 
502
              "%s → %p", iter8.get_key (), iter8.get_value ());
 
503
        }
 
504
 
 
505
      debug.unindent ();
 
506
 
 
507
      debug.print_line (domain, level, "%u favourite handles:",
 
508
          this._favourite_handles.size);
 
509
      debug.indent ();
 
510
 
 
511
      foreach (var handle in this._favourite_handles)
 
512
        {
 
513
          debug.print_line (domain, level, "%u", handle);
359
514
        }
360
515
 
361
516
      debug.unindent ();
381
536
 
382
537
      if (this._conn != null)
383
538
        {
384
 
          this._conn.notify["self-contact"].disconnect (
385
 
              this._self_contact_changed_cb);
386
 
          this._conn.notify["contact-list-state"].disconnect (
387
 
              this._contact_list_state_changed_cb);
388
 
          this._conn.contact_list_changed.disconnect (
389
 
              this._contact_list_changed_cb);
390
 
 
 
539
          this._conn.notify["self-handle"].disconnect (
 
540
              this._self_handle_changed_cb);
391
541
          this._conn = null;
392
542
        }
393
543
 
394
 
      if (this._contact_persona_map != null)
 
544
      if (this._weakly_referenced_contacts != null)
395
545
        {
396
 
          var iter = this._contact_persona_map.map_iterator ();
 
546
          var iter = this._weakly_referenced_contacts.map_iterator ();
397
547
          while (iter.next () == true)
398
548
            {
399
549
              var contact = iter.get_key ();
401
551
            }
402
552
        }
403
553
 
404
 
      this._contact_persona_map = new HashMap<unowned Contact, Persona> ();
 
554
      this._weakly_referenced_contacts =
 
555
          new HashMap<unowned Contact, uint> ();
 
556
 
 
557
      this._handle_persona_map = new HashMap<uint, Persona> ();
 
558
      this._channel_group_personas_map =
 
559
          new HashMap<Channel, HashSet<Persona>> ();
 
560
      this._channel_group_incoming_adds =
 
561
          new HashMap<Channel, HashSet<uint>> ();
 
562
      this._group_outgoing_adds = new HashMap<string, HashSet<Tpf.Persona>> ();
 
563
      this._group_outgoing_removes = new HashMap<string, HashSet<Tpf.Persona>> (
 
564
          );
 
565
 
 
566
      if (this._publish != null)
 
567
        {
 
568
          this._disconnect_from_standard_channel (this._publish);
 
569
          this._publish = null;
 
570
        }
 
571
 
 
572
      if (this._stored != null)
 
573
        {
 
574
          this._disconnect_from_standard_channel (this._stored);
 
575
          this._stored = null;
 
576
        }
 
577
 
 
578
      if (this._subscribe != null)
 
579
        {
 
580
          this._disconnect_from_standard_channel (this._subscribe);
 
581
          this._subscribe = null;
 
582
        }
 
583
 
 
584
      this._standard_channels_unready = new HashMap<string, Channel> ();
 
585
      this._group_channels_unready = new HashMap<string, Channel> ();
 
586
 
 
587
      if (this._groups != null)
 
588
        {
 
589
          foreach (var channel in this._groups.values)
 
590
            {
 
591
              if (channel != null)
 
592
                this._disconnect_from_group_channel (channel);
 
593
            }
 
594
        }
405
595
 
406
596
      this._supported_fields = new HashSet<string> ();
407
597
      this._supported_fields_ro = this._supported_fields.read_only_view;
408
 
      this._self_persona = null;
 
598
      this._groups = new HashMap<string, Channel> ();
 
599
      this._favourite_handles = new HashSet<uint> ();
 
600
      this._self_contact = null;
409
601
    }
410
602
 
411
603
  private void _remove_store ()
420
612
   * Prepare the PersonaStore for use.
421
613
   *
422
614
   * See {@link Folks.PersonaStore.prepare}.
423
 
   *
424
 
   * @throws GLib.Error currently unused
425
615
   */
426
616
  public override async void prepare () throws GLib.Error
427
617
    {
428
 
      Internal.profiling_start ("preparing Tpf.PersonaStore (ID: %s)", this.id);
429
 
 
430
618
      lock (this._is_prepared)
431
619
        {
432
620
          if (this._is_prepared || this._prepare_pending)
440
628
 
441
629
              this._account_manager = AccountManager.dup ();
442
630
 
443
 
              /* FIXME: Add all contact features on AM's factory. We should not
444
 
               * force preparing all features but let app define what it needs,
445
 
               * but this is for backward compatibility.
446
 
               * Note that if application already prepared TpContacts before
447
 
               * preparing this store, this will have no effect on existing
448
 
               * contacts. */
449
 
              var factory = this._account_manager.get_factory ();
450
 
              factory.add_contact_features ({
451
 
                  ContactFeature.ALIAS,
452
 
                  ContactFeature.AVATAR_DATA,
453
 
                  ContactFeature.AVATAR_TOKEN,
454
 
                  ContactFeature.CAPABILITIES,
455
 
                  ContactFeature.CLIENT_TYPES,
456
 
                  ContactFeature.PRESENCE,
457
 
                  ContactFeature.CONTACT_INFO,
458
 
                  ContactFeature.CONTACT_GROUPS
459
 
              });
460
 
 
461
631
              this._account_manager.invalidated.connect (
462
632
                  this._account_manager_invalidated_cb);
463
633
 
464
 
              /* Note: For the three signal handlers below, we do *not* need to
465
 
               * store personas to the cache before removing the store, as
466
 
               * _remove_store() deletes the cache file. */
467
634
              this._account_manager.account_removed.connect ((a) =>
468
635
                {
469
636
                  if (this.account == a)
470
637
                    {
471
 
                      debug ("Account %p (‘%s’) removed.", a, a.display_name);
472
638
                      this._remove_store ();
473
639
                    }
474
640
                });
475
641
              this._account_manager.account_validity_changed.connect (
476
642
                  (a, valid) =>
477
643
                    {
 
644
                      debug ("Account validity changed for %p (‘%s’) to %s.",
 
645
                          a, a.display_name, valid ? "true" : "false");
 
646
 
478
647
                      if (!valid && this.account == a)
479
648
                        {
480
 
                          debug ("Account %p (‘%s’) invalid.", a,
481
 
                              a.display_name);
482
649
                          this._remove_store ();
483
650
                        }
484
651
                    });
485
 
              this._account_manager.account_disabled.connect ((a) =>
486
 
                {
487
 
                  if (this.account == a)
488
 
                    {
489
 
                      debug ("Account %p (‘%s’) disabled.", a, a.display_name);
490
 
                      this._remove_store ();
491
 
                    }
492
 
                });
493
 
 
494
 
              Internal.profiling_point ("created account manager in " +
495
 
                  "Tpf.PersonaStore (ID: %s)", this.id);
496
 
 
497
 
              this._favourite_ids.clear ();
498
 
              this._logger = new Logger (this.id);
499
 
              this._logger.invalidated.connect (
500
 
                  this._logger_invalidated_cb);
501
 
              this._logger.favourite_contacts_changed.connect (
502
 
                  this._favourite_contacts_changed_cb);
503
 
              Internal.profiling_start ("initialising favourite contacts in " +
504
 
                  "Tpf.PersonaStore (ID: %s)", this.id);
505
 
              this._initialise_favourite_contacts.begin ((o, r) =>
506
 
                {
507
 
                  try
508
 
                    {
509
 
                      this._initialise_favourite_contacts.end (r);
510
 
                      Internal.profiling_end ("initialising favourite " +
511
 
                          "contacts in Tpf.PersonaStore (ID: %s)", this.id);
512
 
                    }
513
 
                  catch (GLib.Error e)
514
 
                    {
515
 
                      debug ("Failed to initialise favourite contacts: %s",
516
 
                          e.message);
517
 
                      this._logger = null;
518
 
                    }
519
 
                });
520
 
 
521
 
              Internal.profiling_point ("created logger in Tpf.PersonaStore " +
522
 
                  "(ID: %s)", this.id);
 
652
 
 
653
              /* We have to connect to the logger before dealing with the
 
654
               * account status, because if the account's already connected we
 
655
               * want to be able to query favourite information immediately. */
 
656
              try
 
657
                {
 
658
                  this._logger = new Logger (this.id);
 
659
                  yield this._logger.prepare ();
 
660
                  this._logger.invalidated.connect (
 
661
                      this._logger_invalidated_cb);
 
662
                  this._logger.favourite_contacts_changed.connect (
 
663
                      this._favourite_contacts_changed_cb);
 
664
                }
 
665
              catch (GLib.Error e)
 
666
                {
 
667
                  warning (
 
668
                      _("Couldn't connect to the telepathy-logger service."));
 
669
                  this._logger = null;
 
670
                }
523
671
 
524
672
              this.account.notify["connection"].connect (
525
673
                  this._notify_connection_cb);
534
682
                {
535
683
                  /* If we're disconnected, advertise personas from the cache
536
684
                   * instead. */
537
 
                  yield this._load_cache (null);
538
 
                  this._force_quiescent ();
 
685
                  yield this._load_cache ();
 
686
 
 
687
                  /* We've reached a quiescent state. */
 
688
                  this._got_self_handle = true;
 
689
                  this._got_stored_channel_members = true;
 
690
                  this._notify_if_is_quiescent ();
539
691
                }
540
692
 
541
 
              Internal.profiling_point ("loaded cache in Tpf.PersonaStore " +
542
 
                  "(ID: %s)", this.id);
543
 
 
544
693
              this._is_prepared = true;
545
694
              this.notify_property ("is-prepared");
546
695
            }
549
698
              this._prepare_pending = false;
550
699
            }
551
700
        }
552
 
 
553
 
      Internal.profiling_end ("preparing Tpf.PersonaStore (ID: %s)", this.id);
554
701
    }
555
702
 
556
703
  private void _account_manager_invalidated_cb (uint domain, int code,
572
719
    {
573
720
      this._logger.invalidated.disconnect (this._logger_invalidated_cb);
574
721
 
575
 
      debug ("Lost connection to the telepathy-logger service.");
 
722
      warning (_("Lost connection to the telepathy-logger service."));
576
723
      this._logger = null;
577
724
    }
578
725
 
579
 
  private async void _initialise_favourite_contacts () throws GLib.Error
 
726
  private async void _initialise_favourite_contacts ()
580
727
    {
581
728
      if (this._logger == null)
582
729
        return;
583
730
 
584
 
      yield this._logger.prepare ();
585
 
 
586
 
      var contacts = yield this._logger.get_favourite_contacts ();
587
 
      this._favourite_contacts_changed_cb (contacts, {});
588
 
 
589
 
      this._always_writeable_properties += "is-favourite";
590
 
      this.notify_property ("always-writeable-properties");
 
731
      /* Get an initial set of favourite contacts */
 
732
      try
 
733
        {
 
734
          var contacts = yield this._logger.get_favourite_contacts ();
 
735
 
 
736
          if (contacts.length == 0)
 
737
            return;
 
738
 
 
739
          /* Note that we don't need to release these handles, as they're
 
740
           * also held by the relevant contact objects, and will be released
 
741
           * as appropriate by those objects (we're circumventing tp-glib's
 
742
           * handle reference counting). */
 
743
          this._conn.request_handles (-1, HandleType.CONTACT, contacts,
 
744
            (c, ht, h, i, e, w) =>
 
745
              {
 
746
                try
 
747
                  {
 
748
                    this._change_favourites_by_request_handles ((Handle[]) h, i,
 
749
                        e, true);
 
750
                  }
 
751
                catch (GLib.Error e)
 
752
                  {
 
753
                    /* Translators: the parameter is an error message. */
 
754
                    warning (_("Couldn't get list of favorite contacts: %s"),
 
755
                        e.message);
 
756
                  }
 
757
              },
 
758
            this);
 
759
          /* FIXME: Have to pass this as weak_object parameter since Vala
 
760
           * seems to swap the order of user_data and weak_object in the
 
761
           * callback. */
 
762
        }
 
763
      catch (GLib.Error e)
 
764
        {
 
765
          /* Translators: the parameter is an error message. */
 
766
          warning (_("Couldn't get list of favorite contacts: %s"), e.message);
 
767
        }
591
768
    }
592
769
 
593
 
  private Persona? _lookup_persona_by_id (string id)
 
770
  private void _change_favourites_by_request_handles (Handle[] handles,
 
771
      string[] ids, GLib.Error? error, bool add) throws GLib.Error
594
772
    {
595
 
      /* This is not efficient, but better than doing DBus roundtrip to get a
596
 
       * TpContact. */
597
 
      var iter = this._contact_persona_map.map_iterator ();
598
 
      while (iter.next ())
 
773
      if (error != null)
 
774
        throw error;
 
775
 
 
776
      for (var i = 0; i < handles.length; i++)
599
777
        {
600
 
          if (iter.get_key ().get_identifier () == id)
 
778
          var h = handles[i];
 
779
          var p = this._handle_persona_map[h];
 
780
 
 
781
          /* Add/Remove the handle to the set of favourite handles, since we
 
782
           * might not have the corresponding contact yet */
 
783
          if (add)
 
784
            this._favourite_handles.add (h);
 
785
          else
 
786
            this._favourite_handles.remove (h);
 
787
 
 
788
          /* If the persona isn't in the _handle_persona_map yet, it's most
 
789
           * likely because the account hasn't connected yet (and we haven't
 
790
           * received the roster). If there are already entries in
 
791
           * _handle_persona_map, the account *is* connected and we should
 
792
           * warn about the unknown persona.
 
793
           * We have to take into account that this._self_contact may be
 
794
           * retrieved before or after the rest of the account's contact list,
 
795
           * affecting the size of this._handle_persona_map. */
 
796
          if (p == null &&
 
797
              ((this._self_contact == null &&
 
798
                this._handle_persona_map.size > 0) ||
 
799
               (this._self_contact != null &&
 
800
                    this._handle_persona_map.size > 1)))
601
801
            {
602
 
              return iter.get_value ();
 
802
              /* Translators: the parameter is an identifier. */
 
803
              warning (_("Unknown Telepathy contact ‘%s’ in favorites list."),
 
804
                  ids[i]);
 
805
              continue;
603
806
            }
 
807
 
 
808
          /* Mark or unmark the persona as a favourite */
 
809
          if (p != null)
 
810
            p.is_favourite = add;
604
811
        }
605
 
      return null;
606
812
    }
607
813
 
608
814
  private void _favourite_contacts_changed_cb (string[] added, string[] removed)
609
815
    {
610
 
      foreach (var id in added)
 
816
      /* Don't listen to favourites updates if the account is disconnected. */
 
817
      if (this._conn == null)
 
818
        return;
 
819
 
 
820
      /* Add favourites */
 
821
      if (added.length > 0)
611
822
        {
612
 
          this._favourite_ids.add (id);
613
 
          var p = this._lookup_persona_by_id (id);
614
 
          if (p != null)
615
 
            {
616
 
              p._set_is_favourite (true);
617
 
            }
 
823
          this._conn.request_handles (-1, HandleType.CONTACT, added,
 
824
              (c, ht, h, i, e, w) =>
 
825
                {
 
826
                  try
 
827
                    {
 
828
                      this._change_favourites_by_request_handles ((Handle[]) h,
 
829
                          i, e, true);
 
830
                    }
 
831
                  catch (GLib.Error e)
 
832
                    {
 
833
                      /* Translators: the parameter is an error message. */
 
834
                      warning (_("Couldn't add favorite contacts: %s"),
 
835
                          e.message);
 
836
                    }
 
837
                },
 
838
              this);
618
839
        }
619
 
      foreach (var id in removed)
 
840
 
 
841
      /* Remove favourites */
 
842
      if (removed.length > 0)
620
843
        {
621
 
          this._favourite_ids.remove (id);
622
 
          var p = this._lookup_persona_by_id (id);
623
 
          if (p != null)
624
 
            {
625
 
              p._set_is_favourite (false);
626
 
            }
 
844
          this._conn.request_handles (-1, HandleType.CONTACT, removed,
 
845
              (c, ht, h, i, e, w) =>
 
846
                {
 
847
                  try
 
848
                    {
 
849
                      this._change_favourites_by_request_handles ((Handle[]) h,
 
850
                          i, e, false);
 
851
                    }
 
852
                  catch (GLib.Error e)
 
853
                    {
 
854
                      /* Translators: the parameter is an error message. */
 
855
                      warning (_("Couldn't remove favorite contacts: %s"),
 
856
                          e.message);
 
857
                    }
 
858
                },
 
859
              this);
627
860
        }
628
861
    }
629
862
 
654
887
           * cache, assuming we were connected before. */
655
888
          if (this._conn != null)
656
889
            {
657
 
              /* Call reset immediately, otherwise TpConnection's invalidation
658
 
               * will cause all contacts to weak notify. See bug #675141 */
659
 
              var old_personas = this._persona_set;
660
 
              this._reset ();
 
890
              this._conn = null;
661
891
 
662
 
              /* Only store/load the cache if the account is enabled and valid;
663
 
               * otherwise, the PersonaStore will get removed and the cache
664
 
               * deleted later anyway. */
665
 
              if (this.account.enabled && this.account.valid)
 
892
              this._store_cache.begin ((o, r) =>
666
893
                {
667
 
                  this._store_cache.begin (old_personas, (o, r) =>
 
894
                  this._store_cache.end (r);
 
895
 
 
896
                  this._load_cache.begin ((o2, r2) =>
668
897
                    {
669
 
                      this._store_cache.end (r);
670
 
 
671
 
                      if (this.account.enabled && this.account.valid)
672
 
                        {
673
 
                          this._load_cache.begin (old_personas, (o2, r2) =>
674
 
                            {
675
 
                              this._load_cache.end (r2);
676
 
                            });
677
 
                        }
 
898
                      this._load_cache.end (r2);
678
899
                    });
679
 
                }
 
900
                });
 
901
            }
 
902
 
 
903
          /* If the account was disabled, remove it. We do this here rather than
 
904
           * in a handler for the AccountManager::account-disabled signal so
 
905
           * that we can wait until the personas have been stored to the cache,
 
906
           * which only happens once the account is disconnected (above). We can
 
907
           * do this because it's guaranteed that the account will be
 
908
           * disconnected after being disabled (if it was connected to begin
 
909
           * with). */
 
910
          if (this.account.enabled == false)
 
911
            {
 
912
              this._remove_store ();
680
913
            }
681
914
 
682
915
          /* If the persona store starts offline, we've reached a quiescent
683
916
           * state. */
684
 
          this._force_quiescent ();
 
917
          this._got_self_handle = true;
 
918
          this._got_stored_channel_members = true;
 
919
          this._notify_if_is_quiescent ();
685
920
 
686
921
          return;
687
922
        }
694
929
      debug ("_notify_connection_cb_async() for Tpf.PersonaStore %p ('%s').",
695
930
          this, this.id);
696
931
 
697
 
      Internal.profiling_start ("notify connection for Tpf.PersonaStore " +
698
 
          "(ID: %s)", this.id);
699
 
 
700
932
      /* Ensure the connection is prepared as necessary. */
701
 
      yield this.account.connection.prepare_async ({
702
 
          TelepathyGLib.Connection.get_feature_quark_contact_list (),
703
 
          TelepathyGLib.Connection.get_feature_quark_contact_groups (),
704
 
          TelepathyGLib.Connection.get_feature_quark_contact_info (),
705
 
          TelepathyGLib.Connection.get_feature_quark_connected (),
706
 
          0
707
 
      });
708
 
 
709
 
      if (!this.account.connection.has_interface_by_id (
710
 
          iface_quark_connection_interface_contact_list ()))
711
 
        {
712
 
          warning ("Connection does not implement ContactList iface; " +
713
 
              "legacy CMs are not supported any more.");
714
 
 
715
 
          this._remove_store ();
716
 
 
717
 
          return;
718
 
        }
 
933
      yield this.account.connection.prepare_async (this._connection_features);
719
934
 
720
935
      // We're connected, so can stop advertising personas from the cache
721
936
      this._unload_cache ();
722
937
 
723
 
      this._conn = this.account.connection;
724
 
 
725
 
      this.freeze_notify ();
 
938
      var conn = this.account.connection;
 
939
      conn.notify["connection-ready"].connect (this._connection_ready_cb);
 
940
 
 
941
      /* Deal with the case where the connection is already ready
 
942
       * FIXME: We have to access the property manually until bgo#571348 is
 
943
       * fixed. */
 
944
      var connection_ready = false;
 
945
      conn.get ("connection-ready", out connection_ready);
 
946
 
 
947
      if (connection_ready == true)
 
948
        this._connection_ready_cb (conn, null);
 
949
      else
 
950
        yield conn.prepare_async (null);
 
951
    }
 
952
 
 
953
  private void _connection_ready_cb (Object s, ParamSpec? p)
 
954
    {
 
955
      debug ("_connection_ready_cb() for Tpf.PersonaStore %p ('%s').",
 
956
          this, this.id);
 
957
 
 
958
      var c = (Connection) s;
 
959
      FolksTpLowlevel.connection_connect_to_new_group_channels (c,
 
960
          this._new_group_channels_cb);
 
961
 
726
962
      this._marshall_supported_fields ();
727
963
      this.notify_property ("supported-fields");
728
964
 
729
 
      if (this._conn.get_group_storage () != ContactMetadataStorageType.NONE)
730
 
        {
731
 
          this._can_group_personas = MaybeBool.TRUE;
732
 
 
733
 
          this._always_writeable_properties += "groups";
734
 
          this.notify_property ("always-writeable-properties");
735
 
        }
736
 
      else
737
 
        {
738
 
          this._can_group_personas = MaybeBool.FALSE;
739
 
        }
740
 
      this.notify_property ("can-group-personas");
741
 
 
742
 
      if (this._conn.get_can_change_contact_list ())
743
 
        {
744
 
          this._can_add_personas = MaybeBool.TRUE;
745
 
          this._can_remove_personas = MaybeBool.TRUE;
746
 
        }
747
 
      else
748
 
        {
749
 
          this._can_add_personas = MaybeBool.FALSE;
750
 
          this._can_remove_personas = MaybeBool.FALSE;
751
 
        }
752
 
      this.notify_property ("can-add-personas");
753
 
      this.notify_property ("can-remove-personas");
754
 
 
755
 
      /* FIXME: TpConnection still does not have high-level API for this.
756
 
       * See fd.o#14540 */
757
 
      /* We have to do this before emitting the self persona so that code which
758
 
       * checks the self persona's writeable fields gets correct values. */
759
 
      var new_can_alias = MaybeBool.FALSE;
760
 
 
761
 
      try
762
 
        {
763
 
          var flags = yield FolksTpLowlevel.connection_get_alias_flags_async (
764
 
              this._conn);
765
 
 
766
 
          if ((flags &
767
 
               ConnectionAliasFlags.CONNECTION_ALIAS_FLAG_USER_SET) > 0)
768
 
            {
769
 
              new_can_alias = MaybeBool.TRUE;
770
 
 
771
 
              this._always_writeable_properties += "alias";
772
 
              this.notify_property ("always-writeable-properties");
773
 
            }
774
 
        }
775
 
      catch (GLib.Error e)
776
 
        {
777
 
          GLib.warning (
778
 
              /* Translators: the first parameter is the display name for
779
 
               * the Telepathy account, and the second is an error
780
 
               * message. */
781
 
              _("Failed to determine whether we can set aliases on Telepathy account '%s': %s"),
782
 
              this.display_name, e.message);
783
 
        }
784
 
 
785
 
      this._can_alias_personas = new_can_alias;
786
 
      this.notify_property ("can-alias-personas");
787
 
 
788
 
      this.thaw_notify ();
 
965
      FolksTpLowlevel.connection_get_alias_flags_async.begin (c, (s2, res) =>
 
966
          {
 
967
            var new_can_alias = MaybeBool.FALSE;
 
968
            try
 
969
              {
 
970
                var flags =
 
971
                    FolksTpLowlevel.connection_get_alias_flags_async.end (res);
 
972
                if ((flags &
 
973
                    ConnectionAliasFlags.CONNECTION_ALIAS_FLAG_USER_SET) > 0)
 
974
                  {
 
975
                    new_can_alias = MaybeBool.TRUE;
 
976
                  }
 
977
              }
 
978
            catch (GLib.Error e)
 
979
              {
 
980
                GLib.warning (
 
981
                    /* Translators: the first parameter is the display name for
 
982
                     * the Telepathy account, and the second is an error
 
983
                     * message. */
 
984
                    _("Failed to determine whether we can set aliases on Telepathy account '%s': %s"),
 
985
                    this.display_name, e.message);
 
986
              }
 
987
 
 
988
            this._can_alias_personas = new_can_alias;
 
989
            this.notify_property ("can-alias-personas");
 
990
          });
 
991
 
 
992
      FolksTpLowlevel.connection_get_requestable_channel_classes_async.begin (c,
 
993
          (s3, res3) =>
 
994
          {
 
995
            var new_can_group = MaybeBool.FALSE;
 
996
            try
 
997
              {
 
998
                GenericArray<weak void*> v;
 
999
                int i;
 
1000
 
 
1001
                v = FolksTpLowlevel.
 
1002
                    connection_get_requestable_channel_classes_async.end (res3);
 
1003
 
 
1004
                for (i = 0; i < v.length; i++)
 
1005
                  {
 
1006
                    unowned ValueArray @class = (ValueArray) v.get (i);
 
1007
                    var val = @class.get_nth (0);
 
1008
                    if (val != null)
 
1009
                      {
 
1010
                        var props = (HashTable<weak string, weak Value?>)
 
1011
                            val.get_boxed ();
 
1012
 
 
1013
                        var channel_type = TelepathyGLib.asv_get_string (props,
 
1014
                            this._tp_channel_channel_type);
 
1015
                        bool handle_type_valid;
 
1016
                        var handle_type = TelepathyGLib.asv_get_uint32 (props,
 
1017
                            this._tp_channel_handle_type,
 
1018
                            out handle_type_valid);
 
1019
 
 
1020
                        if ((channel_type ==
 
1021
                              this._tp_channel_contact_list_type) &&
 
1022
                            handle_type_valid &&
 
1023
                            (handle_type == HandleType.GROUP))
 
1024
                          {
 
1025
                            new_can_group = MaybeBool.TRUE;
 
1026
                            break;
 
1027
                          }
 
1028
                      }
 
1029
                  }
 
1030
              }
 
1031
            catch (GLib.Error e3)
 
1032
              {
 
1033
                GLib.warning (
 
1034
                    /* Translators: the first parameter is the display name for
 
1035
                     * the Telepathy account, and the second is an error
 
1036
                     * message. */
 
1037
                    _("Failed to determine whether we can set groups on Telepathy account '%s': %s"),
 
1038
                    this.display_name, e3.message);
 
1039
              }
 
1040
 
 
1041
            this._can_group_personas = new_can_group;
 
1042
            this.notify_property ("can-group-personas");
 
1043
          });
 
1044
 
 
1045
      this._add_standard_channel (c, "publish");
 
1046
      this._add_standard_channel (c, "stored");
 
1047
      this._add_standard_channel (c, "subscribe");
 
1048
      this._conn = c;
789
1049
 
790
1050
      /* Add the local user */
791
 
      this._conn.notify["self-contact"].connect (this._self_contact_changed_cb);
792
 
      this._self_contact_changed_cb (this._conn, null);
793
 
 
794
 
      this._conn.notify["contact-list-state"].connect (this._contact_list_state_changed_cb);
795
 
      this._contact_list_state_changed_cb (this._conn, null);
796
 
 
797
 
      Internal.profiling_end ("notify connection for Tpf.PersonaStore " +
798
 
          "(ID: %s)", this.id);
 
1051
      _conn.notify["self-handle"].connect (this._self_handle_changed_cb);
 
1052
      if (this._conn.self_handle != 0)
 
1053
        this._self_handle_changed_cb (this._conn, null);
 
1054
 
 
1055
      /* We can only initialise the favourite contacts once _conn is prepared */
 
1056
      this._initialise_favourite_contacts.begin ();
799
1057
    }
800
1058
 
801
1059
  private void _marshall_supported_fields ()
826
1084
 
827
1085
  /**
828
1086
   * If our account is disconnected, we want to continue to export a static
829
 
   * view of personas from the cache. old_personas will be notified as removed.
 
1087
   * view of personas from the cache.
830
1088
   */
831
 
  private async void _load_cache (HashSet<Persona>? old_personas)
 
1089
  private async void _load_cache ()
832
1090
    {
833
1091
      /* Only load from the cache if the account is enabled and valid. */
834
1092
      if (this.account.enabled == false || this.account.valid == false)
856
1114
 
857
1115
      // Load the persona set from the cache and notify of the change
858
1116
      var cached_personas = yield this._cache.load_objects (cancellable);
 
1117
      var old_personas = this._persona_set;
859
1118
 
860
1119
      /* If the load operation was cancelled, don't change the state
861
1120
       * of the persona store at all. */
866
1125
        }
867
1126
 
868
1127
      this._reset ();
 
1128
      this._cached = true;
869
1129
 
870
1130
      this._persona_set = new HashSet<Persona> ();
871
1131
      if (cached_personas != null)
872
1132
        {
873
1133
          foreach (var p in cached_personas)
874
1134
            {
875
 
              this._add_persona (p);
 
1135
              this._personas.set (p.iid, p);
 
1136
              this._persona_set.add (p);
876
1137
            }
877
1138
        }
878
1139
 
883
1144
      this._can_alias_personas = MaybeBool.FALSE;
884
1145
      this._can_group_personas = MaybeBool.FALSE;
885
1146
      this._can_remove_personas = MaybeBool.FALSE;
886
 
 
887
 
      if (this._logger != null)
888
 
        {
889
 
          this._always_writeable_properties = { "is-favourite" };
890
 
        }
891
 
      else
892
 
        {
893
 
          this._always_writeable_properties = {};
894
 
        }
895
 
 
896
 
      this.notify_property ("always-writeable-properties");
897
1147
    }
898
1148
 
899
1149
  /**
900
1150
   * When we're about to disconnect, store the current set of personas to the
901
1151
   * cache file so that we can access them once offline.
902
1152
   */
903
 
  private async void _store_cache (HashSet<Persona> old_personas)
 
1153
  private async void _store_cache ()
904
1154
    {
905
1155
      debug ("Storing cache for Tpf.PersonaStore %p ('%s').", this, this.id);
906
1156
 
907
 
      yield this._cache.store_objects (old_personas);
 
1157
      yield this._cache.store_objects (this._persona_set);
908
1158
    }
909
1159
 
910
1160
  /**
927
1177
          GroupDetails.ChangeReason.NONE);
928
1178
 
929
1179
      this._reset ();
930
 
    }
931
 
 
932
 
  private bool _add_persona (Persona p)
933
 
    {
934
 
      if (this._persona_set.add (p))
935
 
        {
936
 
          debug ("Add persona %p with uid %s", p, p.uid);
937
 
          this._personas.set (p.iid, p);
938
 
          return true;
939
 
        }
940
 
 
941
 
      return false;
942
 
    }
943
 
 
944
 
  private bool _remove_persona (Persona p)
945
 
    {
946
 
      if (this._persona_set.remove (p))
947
 
        {
948
 
          debug ("Remove persona %p with uid %s", p, p.uid);
949
 
          this._personas.unset (p.iid);
950
 
          if (this._self_persona == p)
951
 
            {
952
 
              this._self_persona = null;
953
 
            }
954
 
 
955
 
          return true;
956
 
        }
957
 
 
958
 
      return false;
959
 
    }
960
 
 
961
 
  private void _contact_weak_notify_cb (Object obj)
962
 
    {
963
 
      if (this._contact_persona_map == null)
964
 
        {
965
 
          return;
966
 
        }
967
 
 
968
 
      Contact contact = obj as Contact;
969
 
      debug ("Weak notify for TpContact %s", contact.get_identifier ());
970
 
 
971
 
      Persona? persona = null;
972
 
      this._contact_persona_map.unset (contact, out persona);
 
1180
      this._cached = false;
 
1181
    }
 
1182
 
 
1183
  private void _self_handle_changed_cb (Object s, ParamSpec? p)
 
1184
    {
 
1185
      var c = (Connection) s;
 
1186
 
 
1187
      /* Remove the old self persona */
 
1188
      if (this._self_contact != null)
 
1189
        this._ignore_by_handle (this._self_contact.handle, null, null, 0);
 
1190
 
 
1191
      if (c.self_handle == 0)
 
1192
        {
 
1193
          /* We can only claim to have reached a quiescent state once we've
 
1194
           * got the stored contact list and the self handle. */
 
1195
          this._got_self_handle = true;
 
1196
          this._notify_if_is_quiescent ();
 
1197
 
 
1198
          return;
 
1199
        }
 
1200
 
 
1201
      uint[] contact_handles = { c.self_handle };
 
1202
 
 
1203
      /* We have to do it this way instead of using
 
1204
       * TpLowleve.get_contacts_by_handle_async() as we're in a notification
 
1205
       * callback */
 
1206
      c.get_contacts_by_handle (contact_handles,
 
1207
          (uint[]) this._contact_features,
 
1208
          (conn, contacts, failed, error, weak_object) =>
 
1209
            {
 
1210
              if (error != null)
 
1211
                {
 
1212
                  warning (
 
1213
                      /* Translators: the first parameter is a Telepathy handle,
 
1214
                       * and the second is an error message. */
 
1215
                      _("Failed to create contact for self handle '%u': %s"),
 
1216
                      conn.self_handle, error.message);
 
1217
                  return;
 
1218
                }
 
1219
 
 
1220
              debug ("Creating persona from self-handle");
 
1221
 
 
1222
              /* Add the local user */
 
1223
              Contact contact = contacts[0];
 
1224
              bool added;
 
1225
              Persona persona = this._add_persona_from_contact (contact, false, out added);
 
1226
 
 
1227
              var personas = new HashSet<Persona> ();
 
1228
              if (added)
 
1229
                personas.add (persona);
 
1230
 
 
1231
              this._self_contact = contact;
 
1232
              this._emit_personas_changed (personas, null);
 
1233
 
 
1234
              this._got_self_handle = true;
 
1235
              this._notify_if_is_quiescent ();
 
1236
            },
 
1237
          this);
 
1238
    }
 
1239
 
 
1240
  private void _new_group_channels_cb (TelepathyGLib.Channel? channel,
 
1241
      GLib.AsyncResult? result)
 
1242
    {
 
1243
      if (channel == null)
 
1244
        {
 
1245
          /* Translators: do not translate "NewChannels", as it's a D-Bus
 
1246
           * signal name. */
 
1247
          warning (_("Error creating channel for NewChannels signal."));
 
1248
          return;
 
1249
        }
 
1250
 
 
1251
      this._set_up_new_group_channel (channel);
 
1252
      this._channel_group_changes_resolve (channel);
 
1253
    }
 
1254
 
 
1255
  private void _channel_group_changes_resolve (Channel channel)
 
1256
    {
 
1257
      unowned string group = channel.get_identifier ();
 
1258
 
 
1259
      var change_maps = new HashMap<HashSet<Tpf.Persona>, bool> ();
 
1260
      if (this._group_outgoing_adds[group] != null)
 
1261
        change_maps.set (this._group_outgoing_adds[group], true);
 
1262
 
 
1263
      if (this._group_outgoing_removes[group] != null)
 
1264
        change_maps.set (this._group_outgoing_removes[group], false);
 
1265
 
 
1266
      if (change_maps.size < 1)
 
1267
        return;
 
1268
 
 
1269
      foreach (var entry in change_maps.entries)
 
1270
        {
 
1271
          var changes = entry.key;
 
1272
 
 
1273
          foreach (var persona in changes)
 
1274
            {
 
1275
              try
 
1276
                {
 
1277
                  FolksTpLowlevel.channel_group_change_membership (channel,
 
1278
                      (Handle) persona.contact.handle, entry.value, null);
 
1279
                }
 
1280
              catch (GLib.Error e)
 
1281
                {
 
1282
                  if (entry.value == true)
 
1283
                    {
 
1284
                      /* Translators: the parameter is a persona identifier and
 
1285
                       * the second parameter is a group name. */
 
1286
                      warning (_("Failed to add Telepathy contact ‘%s’ to group ‘%s’."),
 
1287
                          persona.contact != null ?
 
1288
                              persona.contact.identifier :
 
1289
                              "(nil)",
 
1290
                          group);
 
1291
                    }
 
1292
                  else
 
1293
                    {
 
1294
                      warning (
 
1295
                          /* Translators: the parameter is a persona identifier
 
1296
                           * and the second parameter is a group name. */
 
1297
                          _("Failed to remove Telepathy contact ‘%s’ from group ‘%s’."),
 
1298
 
 
1299
                          persona.contact != null ?
 
1300
                              persona.contact.identifier :
 
1301
                              "(nil)",
 
1302
                          group);
 
1303
                    }
 
1304
                }
 
1305
            }
 
1306
 
 
1307
          changes.clear ();
 
1308
        }
 
1309
    }
 
1310
 
 
1311
  private void _set_up_new_standard_channel (Channel channel)
 
1312
    {
 
1313
      debug ("Setting up new standard channel '%s' for Tpf.PersonaStore " +
 
1314
          "%p ('%s').", this, this.id, channel.get_identifier ());
 
1315
 
 
1316
      /* hold a ref to the channel here until it's ready, so it doesn't
 
1317
       * disappear */
 
1318
      this._standard_channels_unready[channel.get_identifier ()] = channel;
 
1319
 
 
1320
      channel.notify["channel-ready"].connect ((s, p) =>
 
1321
        {
 
1322
          var c = (Channel) s;
 
1323
          unowned string name = c.get_identifier ();
 
1324
 
 
1325
          debug ("Channel '%s' is ready.", name);
 
1326
 
 
1327
          if (name == "publish")
 
1328
            {
 
1329
              this._publish = c;
 
1330
 
 
1331
              c.group_members_changed_detailed.connect (
 
1332
                  this._publish_channel_group_members_changed_detailed_cb);
 
1333
            }
 
1334
          else if (name == "stored")
 
1335
            {
 
1336
              this._stored = c;
 
1337
 
 
1338
              c.group_members_changed_detailed.connect (
 
1339
                  this._stored_channel_group_members_changed_detailed_cb);
 
1340
            }
 
1341
          else if (name == "subscribe")
 
1342
            {
 
1343
              this._subscribe = c;
 
1344
 
 
1345
              c.group_members_changed_detailed.connect (
 
1346
                  this._subscribe_channel_group_members_changed_detailed_cb);
 
1347
 
 
1348
              c.group_flags_changed.connect (
 
1349
                  this._subscribe_channel_group_flags_changed_cb);
 
1350
 
 
1351
              this._subscribe_channel_group_flags_changed_cb (c,
 
1352
                  c.group_get_flags (), 0);
 
1353
            }
 
1354
 
 
1355
          this._standard_channels_unready.unset (name);
 
1356
 
 
1357
          c.invalidated.connect (this._channel_invalidated_cb);
 
1358
 
 
1359
          unowned Intset? members = c.group_get_members ();
 
1360
          if (members != null && name == "stored")
 
1361
            {
 
1362
              this._channel_group_pend_incoming_adds.begin (c,
 
1363
                  members.to_array (), true, (obj, res) =>
 
1364
                    {
 
1365
                      this._channel_group_pend_incoming_adds.end (res);
 
1366
 
 
1367
                      /* We've got some members for the stored channel group. */
 
1368
                      this._got_stored_channel_members = true;
 
1369
                      this._notify_if_is_quiescent ();
 
1370
                    });
 
1371
            }
 
1372
          else if (members != null)
 
1373
            {
 
1374
              this._channel_group_pend_incoming_adds.begin (c,
 
1375
                  members.to_array (), true);
 
1376
            }
 
1377
        });
 
1378
    }
 
1379
 
 
1380
  private void _disconnect_from_standard_channel (Channel channel)
 
1381
    {
 
1382
      var name = channel.get_identifier ();
 
1383
      debug ("Disconnecting from channel '%s' for Tpf.PersonaStore %p ('%s').",
 
1384
          name, this, this.id);
 
1385
 
 
1386
      channel.invalidated.disconnect (this._channel_invalidated_cb);
 
1387
 
 
1388
      if (name == "publish")
 
1389
        {
 
1390
          channel.group_members_changed_detailed.disconnect (
 
1391
              this._publish_channel_group_members_changed_detailed_cb);
 
1392
        }
 
1393
      else if (name == "stored")
 
1394
        {
 
1395
          channel.group_members_changed_detailed.disconnect (
 
1396
              this._stored_channel_group_members_changed_detailed_cb);
 
1397
        }
 
1398
      else if (name == "subscribe")
 
1399
        {
 
1400
          channel.group_members_changed_detailed.disconnect (
 
1401
              this._subscribe_channel_group_members_changed_detailed_cb);
 
1402
          channel.group_flags_changed.disconnect (
 
1403
              this._subscribe_channel_group_flags_changed_cb);
 
1404
        }
 
1405
    }
 
1406
 
 
1407
  private void _publish_channel_group_members_changed_detailed_cb (
 
1408
      Channel channel,
 
1409
      /* FIXME: Array<uint> => Array<Handle>; parser bug */
 
1410
      Array<uint> added,
 
1411
      Array<uint> removed,
 
1412
      Array<uint> local_pending,
 
1413
      Array<uint> remote_pending,
 
1414
      HashTable details)
 
1415
    {
 
1416
      if (added.length > 0)
 
1417
        this._channel_group_pend_incoming_adds.begin (channel, added, true);
 
1418
 
 
1419
      /* we refuse to send these contacts our presence, so remove them */
 
1420
      for (var i = 0; i < removed.length; i++)
 
1421
        {
 
1422
          var handle = removed.index (i);
 
1423
          this._ignore_by_handle_if_needed (handle, details);
 
1424
        }
 
1425
 
 
1426
      /* FIXME: continue for the other arrays */
 
1427
    }
 
1428
 
 
1429
  private void _stored_channel_group_members_changed_detailed_cb (
 
1430
      Channel channel,
 
1431
      /* FIXME: Array<uint> => Array<Handle>; parser bug */
 
1432
      Array<uint> added,
 
1433
      Array<uint> removed,
 
1434
      Array<uint> local_pending,
 
1435
      Array<uint> remote_pending,
 
1436
      HashTable details)
 
1437
    {
 
1438
      if (added.length > 0)
 
1439
        {
 
1440
          this._channel_group_pend_incoming_adds.begin (channel, added, true,
 
1441
              (obj, res) =>
 
1442
            {
 
1443
              this._channel_group_pend_incoming_adds.end (res);
 
1444
 
 
1445
              /* We can only claim to have reached a quiescent state once we've
 
1446
               * got the stored contact list and the self handle. */
 
1447
              this._got_stored_channel_members = true;
 
1448
              this._notify_if_is_quiescent ();
 
1449
            });
 
1450
        }
 
1451
 
 
1452
      for (var i = 0; i < removed.length; i++)
 
1453
        {
 
1454
          var handle = removed.index (i);
 
1455
          this._ignore_by_handle_if_needed (handle, details);
 
1456
        }
 
1457
    }
 
1458
 
 
1459
  private void _subscribe_channel_group_flags_changed_cb (
 
1460
      Channel? channel,
 
1461
      uint added,
 
1462
      uint removed)
 
1463
    {
 
1464
      this._update_capability ((ChannelGroupFlags) added,
 
1465
          (ChannelGroupFlags) removed, ChannelGroupFlags.CAN_ADD,
 
1466
          ref this._can_add_personas, "can-add-personas");
 
1467
 
 
1468
      this._update_capability ((ChannelGroupFlags) added,
 
1469
          (ChannelGroupFlags) removed, ChannelGroupFlags.CAN_REMOVE,
 
1470
          ref this._can_remove_personas, "can-remove-personas");
 
1471
    }
 
1472
 
 
1473
  private void _update_capability (
 
1474
      ChannelGroupFlags added,
 
1475
      ChannelGroupFlags removed,
 
1476
      ChannelGroupFlags tp_flag,
 
1477
      ref MaybeBool private_member,
 
1478
      string prop_name)
 
1479
    {
 
1480
      var new_value = private_member;
 
1481
 
 
1482
      if ((added & tp_flag) != 0)
 
1483
        new_value = MaybeBool.TRUE;
 
1484
 
 
1485
      if ((removed & tp_flag) != 0)
 
1486
        new_value = MaybeBool.FALSE;
 
1487
 
 
1488
      if (new_value != private_member)
 
1489
        {
 
1490
          private_member = new_value;
 
1491
          this.notify_property (prop_name);
 
1492
        }
 
1493
    }
 
1494
 
 
1495
  private void _subscribe_channel_group_members_changed_detailed_cb (
 
1496
      Channel channel,
 
1497
      /* FIXME: Array<uint> => Array<Handle>; parser bug */
 
1498
      Array<uint> added,
 
1499
      Array<uint> removed,
 
1500
      Array<uint> local_pending,
 
1501
      Array<uint> remote_pending,
 
1502
      HashTable details)
 
1503
    {
 
1504
      if (added.length > 0)
 
1505
        {
 
1506
          this._channel_group_pend_incoming_adds.begin (channel, added, true);
 
1507
 
 
1508
          /* expose ourselves to anyone we can see */
 
1509
          if (this._publish != null)
 
1510
            {
 
1511
              this._channel_group_pend_incoming_adds.begin (this._publish,
 
1512
                  added, true);
 
1513
            }
 
1514
        }
 
1515
 
 
1516
      /* these contacts refused to send us their presence, so remove them */
 
1517
      for (var i = 0; i < removed.length; i++)
 
1518
        {
 
1519
          var handle = removed.index (i);
 
1520
          this._ignore_by_handle_if_needed (handle, details);
 
1521
        }
 
1522
 
 
1523
      /* FIXME: continue for the other arrays */
 
1524
    }
 
1525
 
 
1526
  private void _channel_invalidated_cb (TelepathyGLib.Proxy proxy, uint domain,
 
1527
      int code, string message)
 
1528
    {
 
1529
      var channel = (Channel) proxy;
 
1530
 
 
1531
      this._channel_group_personas_map.unset (channel);
 
1532
      this._channel_group_incoming_adds.unset (channel);
 
1533
 
 
1534
      if (proxy == this._publish)
 
1535
        this._publish = null;
 
1536
      else if (proxy == this._stored)
 
1537
        this._stored = null;
 
1538
      else if (proxy == this._subscribe)
 
1539
        this._subscribe = null;
 
1540
      else
 
1541
        {
 
1542
          var error = new GLib.Error ((Quark) domain, code, "%s", message);
 
1543
          var name = channel.get_identifier ();
 
1544
          this.group_removed (name, error);
 
1545
          this._groups.unset (name);
 
1546
        }
 
1547
    }
 
1548
 
 
1549
  private void _ignore_by_handle_if_needed (uint handle,
 
1550
      HashTable<string, HashTable<string, Value?>> details)
 
1551
    {
 
1552
      unowned TelepathyGLib.Intset members;
 
1553
 
 
1554
      if (this._subscribe != null)
 
1555
        {
 
1556
          members = this._subscribe.group_get_members ();
 
1557
          if (members.is_member (handle))
 
1558
            return;
 
1559
 
 
1560
          members = this._subscribe.group_get_remote_pending ();
 
1561
          if (members.is_member (handle))
 
1562
            return;
 
1563
        }
 
1564
 
 
1565
      if (this._publish != null)
 
1566
        {
 
1567
          members = this._publish.group_get_members ();
 
1568
          if (members.is_member (handle))
 
1569
            return;
 
1570
        }
 
1571
 
 
1572
      unowned string message = TelepathyGLib.asv_get_string (details,
 
1573
          "message");
 
1574
      bool valid;
 
1575
      Persona? actor = null;
 
1576
      var actor_handle = TelepathyGLib.asv_get_uint32 (details, "actor",
 
1577
          out valid);
 
1578
      if (actor_handle > 0 && valid)
 
1579
        actor = this._handle_persona_map[actor_handle];
 
1580
 
 
1581
      GroupDetails.ChangeReason reason = GroupDetails.ChangeReason.NONE;
 
1582
      var tp_reason = TelepathyGLib.asv_get_uint32 (details, "change-reason",
 
1583
          out valid);
 
1584
      if (valid)
 
1585
        reason = Tpf.PersonaStore._change_reason_from_tp_reason (tp_reason);
 
1586
 
 
1587
      this._ignore_by_handle (handle, message, actor, reason);
 
1588
    }
 
1589
 
 
1590
  private static GroupDetails.ChangeReason _change_reason_from_tp_reason (
 
1591
      uint reason)
 
1592
    {
 
1593
      return (GroupDetails.ChangeReason) reason;
 
1594
    }
 
1595
 
 
1596
  private void _ignore_by_handle (uint handle, string? message, Persona? actor,
 
1597
      GroupDetails.ChangeReason reason)
 
1598
    {
 
1599
      var persona = this._handle_persona_map[handle];
 
1600
 
 
1601
      debug ("Ignoring handle %u (persona: %p)", handle, persona);
 
1602
 
 
1603
      if (this._self_contact != null && this._self_contact.handle == handle)
 
1604
        this._self_contact = null;
 
1605
 
 
1606
      /*
 
1607
       * remove all handle-keyed entries
 
1608
       */
 
1609
      this._handle_persona_map.unset (handle);
 
1610
 
 
1611
      /* skip _channel_group_incoming_adds because they occurred after removal
 
1612
       */
 
1613
 
973
1614
      if (persona == null)
974
 
        {
975
 
          return;
976
 
        }
977
 
 
978
 
      if (this._remove_persona (persona))
979
 
        {
980
 
          /* This should never happen because TpConnection keeps a ref on
981
 
           * self and roster TpContacts, so they should have been removed
982
 
           * already. But deal with it just in case... */
983
 
          warning ("A TpContact part of the ContactList is disposed");
984
 
          var personas = new HashSet<Persona> ();
985
 
          personas.add (persona);
986
 
          this._emit_personas_changed (null, personas);
987
 
        }
988
 
    }
989
 
 
990
 
  /* Ensure that we have a Persona wrapping this TpContact. This will keep the
991
 
   * Persona internally only (won't emit personas_changed) and until the
992
 
   * TpContact is destroyed (we keep only weak ref). */
993
 
  internal Tpf.Persona _ensure_persona_for_contact (Contact contact)
994
 
    {
995
 
      Persona? persona = this._contact_persona_map[contact];
996
 
      if (persona != null)
997
 
        return (!) persona;
998
 
 
999
 
      persona = new Tpf.Persona (contact, this);
1000
 
      this._contact_persona_map[contact] = persona;
1001
 
      contact.weak_ref (this._contact_weak_notify_cb);
1002
 
 
1003
 
      var is_favourite = this._favourite_ids.contains (contact.get_identifier ());
1004
 
      persona._set_is_favourite (is_favourite);
1005
 
 
1006
 
      debug ("Persona %p with uid %s created for TpContact %s, favourite: %s",
1007
 
          persona, persona.uid, contact.get_identifier (),
1008
 
          is_favourite ? "yes" : "no");
1009
 
 
1010
 
      return persona;
1011
 
    }
1012
 
 
1013
 
  private void _self_contact_changed_cb (Object s, ParamSpec? p)
1014
 
    {
1015
 
      var contact = this._conn.self_contact;
1016
 
 
1017
 
      var personas_added = new HashSet<Persona> ();
1018
 
      var personas_removed = new HashSet<Persona> ();
1019
 
 
1020
 
      /* Remove old self persona if not also part of roster */
1021
 
      if (this._self_persona != null &&
1022
 
          !this._self_persona.is_in_contact_list &&
1023
 
          this._remove_persona (this._self_persona))
1024
 
        {
1025
 
          personas_removed.add (this._self_persona);
1026
 
        }
1027
 
      this._self_persona = null;
1028
 
 
1029
 
      if (contact != null)
1030
 
        {
1031
 
          /* Add the local user to roster */
1032
 
          this._self_persona = this._ensure_persona_for_contact (contact);
1033
 
          if (this._add_persona (this._self_persona))
1034
 
            personas_added.add (this._self_persona);
1035
 
        }
1036
 
 
1037
 
      this._emit_personas_changed (personas_added, personas_removed);
1038
 
 
1039
 
      this._got_initial_self_contact = true;
1040
 
      this._notify_if_is_quiescent ();
1041
 
    }
1042
 
 
1043
 
  private void _contact_list_state_changed_cb (Object s, ParamSpec? p)
1044
 
    {
1045
 
      /* Once the contact list is downloaded from server, state moves to
1046
 
       * SUCCESS and won't change anymore */
1047
 
      if (this._conn.contact_list_state != ContactListState.SUCCESS)
1048
1615
        return;
1049
1616
 
1050
 
      this._conn.contact_list_changed.connect (this._contact_list_changed_cb);
1051
 
      this._contact_list_changed_cb (this._conn.dup_contact_list (),
1052
 
          new GLib.GenericArray<TelepathyGLib.Contact> ());
1053
 
 
1054
 
      this._got_initial_members = true;
1055
 
      this._populate_counters ();
1056
 
      this._notify_if_is_quiescent ();
1057
 
    }
1058
 
 
1059
 
  private void _contact_list_changed_cb (GLib.GenericArray<TelepathyGLib.Contact> added,
1060
 
      GLib.GenericArray<TelepathyGLib.Contact> removed)
1061
 
    {
1062
 
      var personas_added = new HashSet<Persona> ();
1063
 
      var personas_removed = new HashSet<Persona> ();
1064
 
 
1065
 
      debug ("contact list changed: %d added, %d removed",
1066
 
          added.length, removed.length);
1067
 
 
1068
 
      foreach (Contact contact in added.data)
1069
 
        {
1070
 
          var persona = this._ensure_persona_for_contact (contact);
1071
 
 
1072
 
          if (!persona.is_in_contact_list)
1073
 
            {
1074
 
              persona.is_in_contact_list = true;
1075
 
            }
1076
 
 
1077
 
          if (this._add_persona (persona))
1078
 
            {
1079
 
              personas_added.add (persona);
1080
 
            }
1081
 
        }
1082
 
 
1083
 
      foreach (Contact contact in removed.data)
1084
 
        {
1085
 
          var persona = this._contact_persona_map[contact];
1086
 
 
1087
 
          if (persona == null)
1088
 
            {
1089
 
              warning ("Unknown TpContact removed from ContactList: %s",
1090
 
                  contact.get_identifier ());
1091
 
              continue;
1092
 
            }
1093
 
 
1094
 
          /* If self contact was also part of the roster but got removed,
1095
 
           * we keep it in our persona store, but with is_in_contact_list=false.
1096
 
           * This matches behaviour of _self_contact_changed_cb() where we add
1097
 
           * the self persona into the user-visible set even if it is not part
1098
 
           * of the roster. */
1099
 
          if (persona == this._self_persona)
1100
 
            {
1101
 
              persona.is_in_contact_list = false;
1102
 
              continue;
1103
 
            }
1104
 
 
1105
 
          if (this._remove_persona (persona))
1106
 
            {
1107
 
              personas_removed.add (persona);
1108
 
            }
1109
 
        }
1110
 
 
1111
 
      this._emit_personas_changed (personas_added, personas_removed);
 
1617
      /* If we hold a weak ref. on the persona's TpContact, release that. */
 
1618
      if (persona.contact != null &&
 
1619
          this._weakly_referenced_contacts.unset (persona.contact) == true)
 
1620
        {
 
1621
          persona.contact.weak_unref (this._contact_weak_notify_cb);
 
1622
        }
 
1623
 
 
1624
      /*
 
1625
       * remove all persona-keyed entries
 
1626
       */
 
1627
      foreach (var channel in this._channel_group_personas_map.keys)
 
1628
        {
 
1629
          var members = this._channel_group_personas_map[channel];
 
1630
          if (members != null)
 
1631
            members.remove (persona);
 
1632
        }
 
1633
 
 
1634
      foreach (var name in this._group_outgoing_adds.keys)
 
1635
        {
 
1636
          var members = this._group_outgoing_adds[name];
 
1637
          if (members != null)
 
1638
            members.remove (persona);
 
1639
        }
 
1640
 
 
1641
      var personas = new HashSet<Persona> ();
 
1642
      personas.add (persona);
 
1643
 
 
1644
      this._emit_personas_changed (null, personas, message, actor, reason);
 
1645
      this._personas.unset (persona.iid);
 
1646
      this._persona_set.remove (persona);
1112
1647
    }
1113
1648
 
1114
1649
  /**
1115
1650
   * Remove a {@link Persona} from the PersonaStore.
1116
1651
   *
1117
1652
   * See {@link Folks.PersonaStore.remove_persona}.
1118
 
   *
1119
 
   * @throws Folks.PersonaStoreError.UNSUPPORTED_ON_USER if `persona` is the
1120
 
   * local user — removing the local user isn’t supported
1121
 
   * @throws Folks.PersonaStoreError.REMOVE_FAILED if removing the contact
1122
 
   * failed
1123
1653
   */
1124
1654
  public override async void remove_persona (Folks.Persona persona)
1125
1655
      throws Folks.PersonaStoreError
1126
1656
    {
1127
1657
      var tp_persona = (Tpf.Persona) persona;
1128
1658
 
1129
 
      if (tp_persona.contact == null)
1130
 
        {
1131
 
          warning ("Skipping server-side removal of Tpf.Persona %p because " +
1132
 
              "it has no attached TpContact", tp_persona);
1133
 
          return;
1134
 
        }
1135
 
 
1136
 
      if (persona == this._self_persona &&
 
1659
      if (tp_persona.contact == this._self_contact &&
1137
1660
          tp_persona.is_in_contact_list == false)
1138
1661
        {
1139
1662
          throw new PersonaStoreError.UNSUPPORTED_ON_USER (
1140
1663
              _("Telepathy contacts representing the local user may not be removed."));
1141
1664
        }
1142
1665
 
1143
 
      try
1144
 
        {
1145
 
          yield tp_persona.contact.remove_async ();
1146
 
        }
1147
 
      catch (GLib.Error e)
1148
 
        {
1149
 
          /* Translators: the parameter is an error message. */
1150
 
          throw new PersonaStoreError.REMOVE_FAILED (
1151
 
              _("Failed to remove a persona from store: %s"), e.message);
1152
 
        }
1153
 
    }
1154
 
 
1155
 
  private async Persona _ensure_persona_for_id (string contact_id)
1156
 
      throws GLib.Error
1157
 
    {
1158
 
      var contact = yield this._conn.dup_contact_by_id_async (contact_id, {});
1159
 
      return this._ensure_persona_for_contact (contact);
 
1666
      if (tp_persona.contact == null)
 
1667
        {
 
1668
          warning ("Skipping server-side removal of Tpf.Persona %p because " +
 
1669
              "it has no attached TpContact", tp_persona);
 
1670
          return;
 
1671
        }
 
1672
 
 
1673
      try
 
1674
        {
 
1675
          FolksTpLowlevel.channel_group_change_membership (this._stored,
 
1676
              (Handle) tp_persona.contact.handle, false, null);
 
1677
        }
 
1678
      catch (GLib.Error e1)
 
1679
        {
 
1680
          warning (
 
1681
              /* Translators: The first parameter is a contact identifier, the
 
1682
               * second is a contact list identifier and the third is an error
 
1683
               * message. */
 
1684
              _("Failed to remove Telepathy contact ‘%s’ from ‘%s’ list: %s"),
 
1685
              tp_persona.contact.identifier, "stored", e1.message);
 
1686
        }
 
1687
 
 
1688
      try
 
1689
        {
 
1690
          FolksTpLowlevel.channel_group_change_membership (this._subscribe,
 
1691
              (Handle) tp_persona.contact.handle, false, null);
 
1692
        }
 
1693
      catch (GLib.Error e2)
 
1694
        {
 
1695
          warning (
 
1696
              /* Translators: The first parameter is a contact identifier, the
 
1697
               * second is a contact list identifier and the third is an error
 
1698
               * message. */
 
1699
              _("Failed to remove Telepathy contact ‘%s’ from ‘%s’ list: %s"),
 
1700
              tp_persona.contact.identifier, "subscribe", e2.message);
 
1701
        }
 
1702
 
 
1703
      try
 
1704
        {
 
1705
          FolksTpLowlevel.channel_group_change_membership (this._publish,
 
1706
              (Handle) tp_persona.contact.handle, false, null);
 
1707
        }
 
1708
      catch (GLib.Error e3)
 
1709
        {
 
1710
          warning (
 
1711
              /* Translators: The first parameter is a contact identifier, the
 
1712
               * second is a contact list identifier and the third is an error
 
1713
               * message. */
 
1714
              _("Failed to remove Telepathy contact ‘%s’ from ‘%s’ list: %s"),
 
1715
              tp_persona.contact.identifier, "publish", e3.message);
 
1716
        }
 
1717
 
 
1718
      /* the contact will be actually removed (and signaled) when we hear back
 
1719
       * from the server */
 
1720
    }
 
1721
 
 
1722
  /* Only non-group contact list channels should use create_personas == true,
 
1723
   * since the exposed set of Personas are meant to be filtered by them */
 
1724
  private async void _channel_group_pend_incoming_adds (Channel channel,
 
1725
      Array<uint> adds,
 
1726
      bool create_personas)
 
1727
    {
 
1728
      var adds_length = adds != null ? adds.length : 0;
 
1729
      if (adds_length >= 1)
 
1730
        {
 
1731
          if (create_personas)
 
1732
            {
 
1733
              yield this._create_personas_from_channel_handles_async (channel,
 
1734
                  adds);
 
1735
            }
 
1736
 
 
1737
          for (var i = 0; i < adds.length; i++)
 
1738
            {
 
1739
              var channel_handle = (Handle) adds.index (i);
 
1740
              var contact_handle = channel.group_get_handle_owner (
 
1741
                channel_handle);
 
1742
 
 
1743
              HashSet<uint>? contact_handles =
 
1744
                  this._channel_group_incoming_adds[channel];
 
1745
              if (contact_handles == null)
 
1746
                {
 
1747
                  contact_handles = new HashSet<uint> ();
 
1748
                  this._channel_group_incoming_adds[channel] =
 
1749
                      contact_handles;
 
1750
                }
 
1751
              contact_handles.add (contact_handle);
 
1752
            }
 
1753
        }
 
1754
 
 
1755
      this._channel_groups_add_new_personas ();
 
1756
    }
 
1757
 
 
1758
  private void _channel_group_handle_incoming_removes (Channel channel,
 
1759
      Array<uint> removes)
 
1760
    {
 
1761
      var removes_length = removes != null ? removes.length : 0;
 
1762
      if (removes_length >= 1)
 
1763
        {
 
1764
          var members_removed = new GLib.List<Persona> ();
 
1765
 
 
1766
          HashSet<Persona> members = this._channel_group_personas_map[channel];
 
1767
          if (members == null)
 
1768
            members = new HashSet<Persona> ();
 
1769
 
 
1770
          for (var i = 0; i < removes.length; i++)
 
1771
            {
 
1772
              var channel_handle = (Handle) removes.index (i);
 
1773
              var contact_handle = channel.group_get_handle_owner (
 
1774
                channel_handle);
 
1775
              var persona = this._handle_persona_map[contact_handle];
 
1776
 
 
1777
 
 
1778
              if (persona != null)
 
1779
                {
 
1780
                  members.remove (persona);
 
1781
                  members_removed.prepend (persona);
 
1782
                }
 
1783
            }
 
1784
 
 
1785
          this._channel_group_personas_map[channel] = members;
 
1786
 
 
1787
          var name = channel.get_identifier ();
 
1788
          if (this._group_is_display_group (name) &&
 
1789
              members_removed.length () > 0)
 
1790
            {
 
1791
              members_removed.reverse ();
 
1792
              this.group_members_changed (name, null, members_removed);
 
1793
            }
 
1794
        }
 
1795
    }
 
1796
 
 
1797
  private void _set_up_new_group_channel (Channel channel)
 
1798
    {
 
1799
      /* hold a ref to the channel here until it's ready, so it doesn't
 
1800
       * disappear */
 
1801
      this._group_channels_unready[channel.get_identifier ()] = channel;
 
1802
 
 
1803
      channel.notify["channel-ready"].connect ((s, p) =>
 
1804
        {
 
1805
          var c = (Channel) s;
 
1806
          var name = c.get_identifier ();
 
1807
 
 
1808
          var existing_channel = this._groups[name];
 
1809
          if (existing_channel != null)
 
1810
            {
 
1811
              /* Somehow, this group channel has already been set up. We have to
 
1812
               * hold a reference to the existing group while unsetting it in
 
1813
               * the group map so that unsetting it doesn't cause it to be
 
1814
               * destroyed. If that were to happen, channel_invalidated_cb()
 
1815
               * would remove it from the group map a second time, causing a
 
1816
               * double unref. */
 
1817
              existing_channel.ref ();
 
1818
              this._groups.unset (name);
 
1819
              existing_channel.unref ();
 
1820
            }
 
1821
 
 
1822
          /* Drop all references before we set the new channel */
 
1823
          existing_channel = null;
 
1824
 
 
1825
          this._groups[name] = c;
 
1826
          this._group_channels_unready.unset (name);
 
1827
 
 
1828
          c.invalidated.connect (this._channel_invalidated_cb);
 
1829
          c.group_members_changed_detailed.connect (
 
1830
            this._channel_group_members_changed_detailed_cb);
 
1831
 
 
1832
          unowned Intset members = c.group_get_members ();
 
1833
          if (members != null)
 
1834
            {
 
1835
              this._channel_group_pend_incoming_adds.begin (c,
 
1836
                members.to_array (), false);
 
1837
            }
 
1838
        });
 
1839
    }
 
1840
 
 
1841
  private void _disconnect_from_group_channel (Channel channel)
 
1842
    {
 
1843
      var name = channel.get_identifier ();
 
1844
      debug ("Disconnecting from group channel '%s'.", name);
 
1845
 
 
1846
      channel.group_members_changed_detailed.disconnect (
 
1847
          this._channel_group_members_changed_detailed_cb);
 
1848
      channel.invalidated.disconnect (this._channel_invalidated_cb);
 
1849
    }
 
1850
 
 
1851
  private void _channel_group_members_changed_detailed_cb (Channel channel,
 
1852
      /* FIXME: Array<uint> => Array<Handle>; parser bug */
 
1853
      Array<uint> added,
 
1854
      Array<uint> removed,
 
1855
      Array<uint> local_pending,
 
1856
      Array<uint> remote_pending,
 
1857
      HashTable details)
 
1858
    {
 
1859
      if (added != null)
 
1860
        this._channel_group_pend_incoming_adds.begin (channel, added, false);
 
1861
 
 
1862
      if (removed != null)
 
1863
        {
 
1864
          this._channel_group_handle_incoming_removes (channel, removed);
 
1865
        }
 
1866
 
 
1867
      /* FIXME: continue for the other arrays */
 
1868
    }
 
1869
 
 
1870
  internal async void _change_group_membership (Folks.Persona persona,
 
1871
      string group, bool is_member)
 
1872
    {
 
1873
      var tp_persona = (Tpf.Persona) persona;
 
1874
      var channel = this._groups[group];
 
1875
      var change_map = is_member ? this._group_outgoing_adds :
 
1876
        this._group_outgoing_removes;
 
1877
      var change_set = change_map[group];
 
1878
 
 
1879
      if (change_set == null)
 
1880
        {
 
1881
          change_set = new HashSet<Tpf.Persona> ();
 
1882
          change_map[group] = change_set;
 
1883
        }
 
1884
      change_set.add (tp_persona);
 
1885
 
 
1886
      if (channel == null)
 
1887
        {
 
1888
          /* the changes queued above will be resolve in the NewChannels handler
 
1889
           */
 
1890
          FolksTpLowlevel.connection_create_group_async (this.account.connection,
 
1891
              group);
 
1892
        }
 
1893
      else
 
1894
        {
 
1895
          /* the channel is already ready, so resolve immediately */
 
1896
          this._channel_group_changes_resolve (channel);
 
1897
        }
 
1898
    }
 
1899
 
 
1900
  private void _change_standard_contact_list_membership (
 
1901
      TelepathyGLib.Channel channel, Folks.Persona persona, bool is_member,
 
1902
      string? message)
 
1903
    {
 
1904
      var tp_persona = (Tpf.Persona) persona;
 
1905
 
 
1906
      if (tp_persona.contact == null)
 
1907
        {
 
1908
          warning ("Skipping Tpf.Persona %p contact list change because it " +
 
1909
              "has no attached TpContact", tp_persona);
 
1910
          return;
 
1911
        }
 
1912
 
 
1913
      try
 
1914
        {
 
1915
          FolksTpLowlevel.channel_group_change_membership (channel,
 
1916
              (Handle) tp_persona.contact.handle, is_member, message);
 
1917
        }
 
1918
      catch (GLib.Error e)
 
1919
        {
 
1920
          if (is_member == true)
 
1921
            {
 
1922
              warning (
 
1923
                  /* Translators: The first parameter is a contact identifier,
 
1924
                   * the second is a contact list identifier and the third is an
 
1925
                   * error message. */
 
1926
                  _("Failed to add Telepathy contact ‘%s’ to ‘%s’ list: %s"),
 
1927
                  tp_persona.contact.identifier, channel.get_identifier (),
 
1928
                  e.message);
 
1929
            }
 
1930
          else
 
1931
            {
 
1932
              warning (
 
1933
                  /* Translators: The first parameter is a contact identifier,
 
1934
                   * the second is a contact list identifier and the third is an
 
1935
                   * error message. */
 
1936
                  _("Failed to remove Telepathy contact ‘%s’ from ‘%s’ list: %s"),
 
1937
                  tp_persona.contact.identifier, channel.get_identifier (),
 
1938
                  e.message);
 
1939
            }
 
1940
        }
 
1941
    }
 
1942
 
 
1943
  private async Channel? _add_standard_channel (Connection conn, string name)
 
1944
    {
 
1945
      Channel? channel = null;
 
1946
 
 
1947
      debug ("Adding standard channel '%s' to connection %p for " +
 
1948
          "Tpf.PersonaStore %p ('%s').", name, conn, this, this.id);
 
1949
 
 
1950
      /* FIXME: handle the error GLib.Error from this function */
 
1951
      try
 
1952
        {
 
1953
          channel =
 
1954
              yield FolksTpLowlevel.connection_open_contact_list_channel_async (
 
1955
                  conn, name);
 
1956
        }
 
1957
      catch (GLib.Error e)
 
1958
        {
 
1959
          debug ("Failed to add channel '%s': %s", name, e.message);
 
1960
 
 
1961
          /* If the Connection doesn't support 'stored' channels we
 
1962
           * pretend we've received the stored channel members.
 
1963
           *
 
1964
           * When this happens it probably means the ConnectionManager doesn't
 
1965
           * implement the Channel.Type.ContactList interface.
 
1966
           *
 
1967
           * See: https://bugzilla.gnome.org/show_bug.cgi?id=656184 */
 
1968
          this._got_stored_channel_members = true;
 
1969
          this._notify_if_is_quiescent ();
 
1970
 
 
1971
          /* XXX: assuming there's no decent way to recover from this */
 
1972
 
 
1973
          return null;
 
1974
        }
 
1975
 
 
1976
      this._set_up_new_standard_channel (channel);
 
1977
 
 
1978
      return channel;
 
1979
    }
 
1980
 
 
1981
  /* FIXME: Array<uint> => Array<Handle>; parser bug */
 
1982
  private async void _create_personas_from_channel_handles_async (
 
1983
      Channel channel,
 
1984
      Array<uint> channel_handles)
 
1985
    {
 
1986
      uint[] contact_handles = {};
 
1987
      for (var i = 0; i < channel_handles.length; i++)
 
1988
        {
 
1989
          var channel_handle = (Handle) channel_handles.index (i);
 
1990
          var contact_handle = channel.group_get_handle_owner (channel_handle);
 
1991
          Persona? persona = this._handle_persona_map[contact_handle];
 
1992
 
 
1993
          if (persona == null)
 
1994
            {
 
1995
              contact_handles += contact_handle;
 
1996
            }
 
1997
          else
 
1998
            {
 
1999
              /* Mark the persona as having been seen in the contact list.
 
2000
               * The persona might have originally been discovered by querying
 
2001
               * the Telepathy connection's self-handle; in this case, its
 
2002
               * is-in-contact-list property will originally be false, as a
 
2003
               * contact could be exposed as the self-handle, but not actually
 
2004
               * be in the user's contact list. */
 
2005
              debug ("Setting is-in-contact-list for '%s' to true",
 
2006
                  persona.uid);
 
2007
              persona.is_in_contact_list = true;
 
2008
            }
 
2009
        }
 
2010
 
 
2011
      try
 
2012
        {
 
2013
          if (contact_handles.length < 1)
 
2014
            return;
 
2015
 
 
2016
          GLib.List<TelepathyGLib.Contact> contacts =
 
2017
              yield FolksTpLowlevel.connection_get_contacts_by_handle_async (
 
2018
                  this._conn, contact_handles, (uint[]) _contact_features);
 
2019
 
 
2020
          if (contacts == null || contacts.length () < 1)
 
2021
            return;
 
2022
 
 
2023
          var contacts_array = new TelepathyGLib.Contact[contacts.length ()];
 
2024
          var j = 0;
 
2025
          unowned GLib.List<TelepathyGLib.Contact> l = contacts;
 
2026
          for (; l != null; l = l.next)
 
2027
            {
 
2028
              contacts_array[j] = l.data;
 
2029
              j++;
 
2030
            }
 
2031
 
 
2032
          this._add_new_personas_from_contacts (contacts_array);
 
2033
        }
 
2034
      catch (GLib.Error e)
 
2035
        {
 
2036
          warning (
 
2037
              /* Translators: the first parameter is a channel identifier and
 
2038
               * the second is an error message.. */
 
2039
              _("Failed to create incoming Telepathy contacts from channel ‘%s’: %s"),
 
2040
              channel.get_identifier (), e.message);
 
2041
        }
 
2042
    }
 
2043
 
 
2044
  private async HashSet<Persona> _ensure_personas_from_contact_ids (
 
2045
      string[] contact_ids) throws GLib.Error
 
2046
    {
 
2047
      var personas = new HashSet<Persona> ();
 
2048
      var personas_added = new HashSet<Persona> ();
 
2049
 
 
2050
      if (contact_ids.length == 0)
 
2051
        return personas;
 
2052
 
 
2053
      GLib.List<TelepathyGLib.Contact> contacts =
 
2054
          yield FolksTpLowlevel.connection_get_contacts_by_id_async (
 
2055
              this._conn, contact_ids, (uint[]) _contact_features);
 
2056
 
 
2057
      unowned GLib.List<TelepathyGLib.Contact> l;
 
2058
      for (l = contacts; l != null; l = l.next)
 
2059
        {
 
2060
          var contact = l.data;
 
2061
 
 
2062
          debug ("Creating persona from contact '%s'", contact.identifier);
 
2063
 
 
2064
          bool added;
 
2065
          var persona = this._add_persona_from_contact (contact, true, out added);
 
2066
          if (added)
 
2067
            personas_added.add (persona);
 
2068
 
 
2069
          personas.add (persona);
 
2070
        }
 
2071
 
 
2072
      if (personas_added.size > 0)
 
2073
        {
 
2074
          this._emit_personas_changed (personas_added, null);
 
2075
        }
 
2076
 
 
2077
      return personas;
 
2078
    }
 
2079
 
 
2080
  private void _contact_weak_notify_cb (Object obj)
 
2081
    {
 
2082
      var c = obj as Contact;
 
2083
      if (this._weakly_referenced_contacts != null)
 
2084
        {
 
2085
          Handle handle = this._weakly_referenced_contacts.get (c);
 
2086
          this._weakly_referenced_contacts.unset (c);
 
2087
 
 
2088
          if (handle != 0)
 
2089
            {
 
2090
              this._ignore_by_handle ((!) handle, null, null,
 
2091
                  GroupDetails.ChangeReason.NONE);
 
2092
            }
 
2093
        }
 
2094
    }
 
2095
 
 
2096
  internal Tpf.Persona? _ensure_persona_from_contact (Contact contact)
 
2097
    {
 
2098
      uint handle = contact.get_handle ();
 
2099
 
 
2100
      debug ("Ensuring contact %p (handle: %u) exists in Tpf.PersonaStore " +
 
2101
          "%p ('%s').", contact, handle, this, this.id);
 
2102
 
 
2103
      if (handle == 0)
 
2104
        {
 
2105
          return null;
 
2106
        }
 
2107
 
 
2108
      /* If the persona already exists, return them. */
 
2109
      var persona = this._handle_persona_map[handle];
 
2110
 
 
2111
      if (persona != null)
 
2112
        {
 
2113
          return persona;
 
2114
        }
 
2115
 
 
2116
      /* Otherwise, add the persona to the store. See bgo#665376 for details of
 
2117
       * why this is necessary. Since the TpContact is coming from a source
 
2118
       * other than the TpChannels which are associated with this store, we only
 
2119
       * hold a weak reference to it and remove it from the store as soon as
 
2120
       * it's destroyed. */
 
2121
      bool added;
 
2122
      persona = this._add_persona_from_contact (contact, false, out added);
 
2123
 
 
2124
      if (!added)
 
2125
        {
 
2126
          return null;
 
2127
        }
 
2128
 
 
2129
      /* Weak ref. on the contact. */
 
2130
      contact.weak_ref (this._contact_weak_notify_cb);
 
2131
      this._weakly_referenced_contacts.set (contact, handle);
 
2132
 
 
2133
      /* Signal the addition of the new persona. */
 
2134
      var personas = new HashSet<Persona> ();
 
2135
      this._emit_personas_changed (personas, null);
 
2136
 
 
2137
      return persona;
 
2138
    }
 
2139
 
 
2140
  private Tpf.Persona? _add_persona_from_contact (Contact contact,
 
2141
      bool from_contact_list,
 
2142
      out bool added)
 
2143
    {
 
2144
      var h = contact.get_handle ();
 
2145
      Persona? persona = null;
 
2146
 
 
2147
      debug ("Adding persona from contact '%s'", contact.identifier);
 
2148
 
 
2149
      persona = this._handle_persona_map[h];
 
2150
      if (persona == null)
 
2151
        {
 
2152
          persona = new Tpf.Persona (contact, this);
 
2153
 
 
2154
          this._personas.set (persona.iid, persona);
 
2155
          this._persona_set.add (persona);
 
2156
          this._handle_persona_map[h] = persona;
 
2157
 
 
2158
          /* If the handle is a favourite, ensure the persona's marked
 
2159
           * as such. This deals with the case where we receive a
 
2160
           * contact _after_ we've discovered that they're a
 
2161
           * favourite. */
 
2162
          persona.is_favourite = this._favourite_handles.contains (h);
 
2163
 
 
2164
          /* Only emit this debug message in the false case to reduce debug
 
2165
           * spam (see https://bugzilla.gnome.org/show_bug.cgi?id=640901#c2). */
 
2166
          if (from_contact_list == false)
 
2167
            {
 
2168
              debug ("    Setting is-in-contact-list to false");
 
2169
            }
 
2170
 
 
2171
          persona.is_in_contact_list = from_contact_list;
 
2172
 
 
2173
          added = true;
 
2174
        }
 
2175
      else
 
2176
        {
 
2177
           debug ("    ...already exists.");
 
2178
 
 
2179
          /* Mark the persona as having been seen in the contact list.
 
2180
           * The persona might have originally been discovered by querying
 
2181
           * the Telepathy connection's self-handle; in this case, its
 
2182
           * is-in-contact-list property will originally be false, as a
 
2183
           * contact could be exposed as the self-handle, but not actually
 
2184
           * be in the user's contact list. */
 
2185
          if (persona.is_in_contact_list == false && from_contact_list == true)
 
2186
            {
 
2187
              debug ("    Setting is-in-contact-list to true");
 
2188
              persona.is_in_contact_list = true;
 
2189
            }
 
2190
 
 
2191
          added = false;
 
2192
        }
 
2193
      return persona;
 
2194
    }
 
2195
 
 
2196
  private void _add_new_personas_from_contacts (Contact[] contacts)
 
2197
    {
 
2198
      var personas = new HashSet<Persona> ();
 
2199
 
 
2200
      foreach (Contact contact in contacts)
 
2201
        {
 
2202
          bool added;
 
2203
          var persona = this._add_persona_from_contact (contact, true, out added);
 
2204
          if (added)
 
2205
            personas.add (persona);
 
2206
        }
 
2207
 
 
2208
      this._channel_groups_add_new_personas ();
 
2209
 
 
2210
      if (personas.size > 0)
 
2211
        {
 
2212
          this._emit_personas_changed (personas, null);
 
2213
        }
 
2214
    }
 
2215
 
 
2216
  private void _channel_groups_add_new_personas ()
 
2217
    {
 
2218
      foreach (var entry in this._channel_group_incoming_adds.entries)
 
2219
        {
 
2220
          var channel = (Channel) entry.key;
 
2221
          var members_added = new GLib.List<Persona> ();
 
2222
 
 
2223
          HashSet<Persona> members = this._channel_group_personas_map[channel];
 
2224
          if (members == null)
 
2225
            members = new HashSet<Persona> ();
 
2226
 
 
2227
          debug ("Adding members to channel '%s':", channel.get_identifier ());
 
2228
 
 
2229
          var contact_handles = entry.value;
 
2230
          if (contact_handles != null && contact_handles.size > 0)
 
2231
            {
 
2232
              var contact_handles_added = new HashSet<uint> ();
 
2233
              foreach (var contact_handle in contact_handles)
 
2234
                {
 
2235
                  var persona = this._handle_persona_map[contact_handle];
 
2236
                  if (persona != null)
 
2237
                    {
 
2238
                      debug ("    %s", persona.uid);
 
2239
                      members.add (persona);
 
2240
                      members_added.prepend (persona);
 
2241
                      contact_handles_added.add (contact_handle);
 
2242
                    }
 
2243
                }
 
2244
 
 
2245
              foreach (var handle in contact_handles_added)
 
2246
                contact_handles.remove (handle);
 
2247
            }
 
2248
 
 
2249
          if (members.size > 0)
 
2250
            this._channel_group_personas_map[channel] = members;
 
2251
 
 
2252
          var name = channel.get_identifier ();
 
2253
          if (this._group_is_display_group (name) &&
 
2254
              members_added.length () > 0)
 
2255
            {
 
2256
              members_added.reverse ();
 
2257
              this.group_members_changed (name, members_added, null);
 
2258
            }
 
2259
        }
 
2260
    }
 
2261
 
 
2262
  private bool _group_is_display_group (string group)
 
2263
    {
 
2264
      for (var i = 0; i < this._undisplayed_groups.length; i++)
 
2265
        {
 
2266
          if (this._undisplayed_groups[i] == group)
 
2267
            return false;
 
2268
        }
 
2269
 
 
2270
      return true;
1160
2271
    }
1161
2272
 
1162
2273
  /**
1163
2274
   * Add a new {@link Persona} to the PersonaStore.
1164
2275
   *
1165
2276
   * See {@link Folks.PersonaStore.add_persona_from_details}.
1166
 
   *
1167
 
   * @throws Folks.PersonaStoreError.INVALID_ARGUMENT if the `contact` key was
1168
 
   * not provided in `details`
1169
 
   * @throws Folks.PersonaStoreError.STORE_OFFLINE if the CM is offline
1170
 
   * @throws Folks.PersonaStoreError.CREATE_FAILED if adding the contact failed
1171
2277
   */
1172
2278
  public override async Folks.Persona? add_persona_from_details (
1173
2279
      HashTable<string, Value?> details) throws Folks.PersonaStoreError
1196
2302
              _("Cannot create a new Telepathy contact while offline."));
1197
2303
        }
1198
2304
 
 
2305
      var contact_ids = new string[1];
 
2306
      contact_ids[0] = contact_id;
 
2307
 
1199
2308
      try
1200
2309
        {
1201
 
          var persona = yield this._ensure_persona_for_id (contact_id);
1202
 
          var already_exists = persona.is_in_contact_list;
1203
 
          var tp_persona = (Tpf.Persona) persona;
1204
 
          yield tp_persona.contact.request_subscription_async (add_message);
1205
 
 
1206
 
          /* This function is supposed to return null if the persona was already
1207
 
           * in the contact list. */
1208
 
          return already_exists ? null : persona;
 
2310
          var personas = yield this._ensure_personas_from_contact_ids (
 
2311
              contact_ids);
 
2312
 
 
2313
          if (personas.size == 0)
 
2314
            {
 
2315
              /* the persona already existed */
 
2316
              return null;
 
2317
            }
 
2318
          else if (personas.size == 1)
 
2319
            {
 
2320
              /* Get the first (and only) Persona */
 
2321
              Persona persona = null;
 
2322
              foreach (var p in personas)
 
2323
                {
 
2324
                  persona = p;
 
2325
                  break;
 
2326
                }
 
2327
 
 
2328
              if (this._subscribe != null)
 
2329
                this._change_standard_contact_list_membership (this._subscribe,
 
2330
                    persona, true, add_message);
 
2331
 
 
2332
              if (this._publish != null)
 
2333
                {
 
2334
                  var flags = this._publish.group_get_flags ();
 
2335
                  if ((flags & ChannelGroupFlags.CAN_ADD) ==
 
2336
                      ChannelGroupFlags.CAN_ADD)
 
2337
                    {
 
2338
                      this._change_standard_contact_list_membership (
 
2339
                          this._publish, persona, true, add_message);
 
2340
                    }
 
2341
                }
 
2342
 
 
2343
              return persona;
 
2344
            }
 
2345
          else
 
2346
            {
 
2347
              /* We ignore the case of an empty list, as it just means the
 
2348
               * contact was already in our roster */
 
2349
              var num_personas = personas.size;
 
2350
              var message =
 
2351
                  ngettext (
 
2352
                      /* Translators: the parameter is the number of personas
 
2353
                       * which were returned. */
 
2354
                      "Requested a single persona, but got %u persona back.",
 
2355
                      "Requested a single persona, but got %u personas back.",
 
2356
                          num_personas);
 
2357
 
 
2358
              throw new PersonaStoreError.CREATE_FAILED (message, num_personas);
 
2359
            }
1209
2360
        }
1210
2361
      catch (GLib.Error e)
1211
2362
        {
1261
2412
    }
1262
2413
 
1263
2414
  internal async void change_alias (Tpf.Persona persona, string alias)
1264
 
      throws PropertyError
1265
2415
    {
1266
2416
      /* Deal with badly-behaved callers */
1267
2417
      if (alias == null)
1276
2426
          return;
1277
2427
        }
1278
2428
 
1279
 
      try
1280
 
        {
1281
 
          debug ("Changing alias of persona %s to '%s'.",
1282
 
              persona.contact.get_identifier (), alias);
1283
 
          yield FolksTpLowlevel.connection_set_contact_alias_async (this._conn,
1284
 
              (Handle) persona.contact.handle, alias);
1285
 
        }
1286
 
      catch (GLib.Error e1)
1287
 
        {
1288
 
          throw new PropertyError.UNKNOWN_ERROR (
1289
 
              /* Translators: the parameter is an error message. */
1290
 
              _("Failed to change contact's alias: %s"), e1.message);
1291
 
        }
 
2429
      debug ("Changing alias of persona %u to '%s'.", persona.contact.handle,
 
2430
          alias);
 
2431
      FolksTpLowlevel.connection_set_contact_alias (this._conn,
 
2432
          (Handle) persona.contact.handle, alias);
1292
2433
    }
1293
2434
 
1294
2435
  internal async void change_user_birthday (Tpf.Persona persona,
1378
2519
          try
1379
2520
            {
1380
2521
              success =
1381
 
                yield this._conn.set_contact_info_async (
 
2522
                yield this.account.connection.set_contact_info_async (
1382
2523
                  info_list);
1383
2524
            }
1384
2525
          catch (GLib.Error e)
1560
2701
 
1561
2702
      return store;
1562
2703
    }
1563
 
 
1564
 
  private string? _get_iid_from_event_metadata (string? uri)
1565
 
    {
1566
 
      /* Format a proper id represting a persona in the store.
1567
 
       * Zeitgeist uses x-telepathy-identifier as a prefix for telepathy, which
1568
 
       * is stored as the uri of a subject of an event. */
1569
 
      if (uri == null)
1570
 
        {
1571
 
          return null;
1572
 
        }
1573
 
      var new_uri = uri.replace ("x-telepathy-identifier:", "");
1574
 
      return this.account.protocol + ":" + new_uri;
1575
 
    }
1576
 
 
1577
 
  private void _increase_persona_counter (string? id, string? interaction_type, Event event)
1578
 
    {
1579
 
      /* Check if the persona id and interaction is valid. If so increase the
1580
 
       * appropriate interacton counter, to signify that an
1581
 
       * interaction was successfully counted. */
1582
 
      if (id != null && this._personas.has_key (id) && interaction_type != null)
1583
 
        {
1584
 
          var persona = this._personas.get (id);
1585
 
          persona._increase_counter (id, interaction_type, event);
1586
 
        }
1587
 
    }
1588
 
 
1589
 
  private void _handle_new_interaction (TimeRange timerange, ResultSet events)
1590
 
    {
1591
 
      foreach (var e in events)
1592
 
        {
1593
 
          for (var i = 1; i < e.num_subjects (); i++)
1594
 
            {
1595
 
              var id = this._get_iid_from_event_metadata (e.get_subject (i).get_uri ());
1596
 
              var interaction_type = e.get_subject (0).get_interpretation ();
1597
 
              this._increase_persona_counter (id, interaction_type, e);
1598
 
            }
1599
 
        }
1600
 
    }
1601
 
 
1602
 
  private PtrArray _get_zeitgeist_event_templates ()
1603
 
    {
1604
 
      /* To fetch events from Zeitgeist about the interaction with contacts we
1605
 
       * create templates reflecting how the telepathy-logger stores events in
1606
 
       * Zeitgeist */
1607
 
      var origin = this.id.replace (TelepathyGLib.ACCOUNT_OBJECT_PATH_BASE,
1608
 
                                    "x-telepathy-account-path:");
1609
 
 
1610
 
      Event ev1 = new Event.full ("", "", "dbus://org.freedesktop.Telepathy.Logger.service");
1611
 
      ev1.add_subject (new Subject.full ("", Zeitgeist.NMO_IMMESSAGE, "", "", "", "", ""));
1612
 
      ev1.set_origin (origin);
1613
 
 
1614
 
      Event ev2 = new Event.full ("", "", "dbus://org.freedesktop.Telepathy.Logger.service");
1615
 
      ev2.add_subject (new Subject.full ("", "", Zeitgeist.NFO_MEDIA_STREAM, "", "", "", ""));
1616
 
      ev2.set_origin (origin);
1617
 
 
1618
 
      var templates = new PtrArray ();
1619
 
      templates.add (ev1.ref ());
1620
 
      templates.add (ev2.ref ());
1621
 
      return templates;
1622
 
    }
1623
 
 
1624
 
  private async void _populate_counters ()
1625
 
    {
1626
 
      if (this._log == null)
1627
 
        {
1628
 
          this._log = new Zeitgeist.Log ();
1629
 
        }
1630
 
 
1631
 
      /* Get all events for this account from Zeitgeist and increase the
1632
 
       * the counters of the personas */
1633
 
      try
1634
 
        {
1635
 
          PtrArray events = this._get_zeitgeist_event_templates ();
1636
 
          var results = yield this._log.find_events (new TimeRange.anytime (),
1637
 
              (owned) events, StorageState.ANY, 0, ResultType.MOST_RECENT_EVENTS,
1638
 
              null);
1639
 
 
1640
 
          foreach (var persona in this._personas.values)
1641
 
            {
1642
 
              persona.freeze_notify ();
1643
 
              persona._reset_interaction ();
1644
 
            }
1645
 
          foreach (var e in results)
1646
 
            {
1647
 
              var interaction_type = e.get_subject (0).get_interpretation ();
1648
 
              for (var i = 1; i < e.num_subjects (); i++)
1649
 
                {
1650
 
                  var id = this._get_iid_from_event_metadata (e.get_subject (i).get_uri ());
1651
 
                  this._increase_persona_counter (id, interaction_type, e);
1652
 
                }
1653
 
            }
1654
 
          foreach (var persona in this.personas.values)
1655
 
            {
1656
 
              persona.thaw_notify ();
1657
 
            }
1658
 
        }
1659
 
      catch
1660
 
        {
1661
 
          debug ("Failed to fetch events from Zeitgeist");
1662
 
        }
1663
 
 
1664
 
      /* Prepare a monitor and install for this account to populate persona
1665
 
       * counters upon interaction changes.*/
1666
 
      if (this._monitor == null)
1667
 
        {
1668
 
          PtrArray monitor_events = this._get_zeitgeist_event_templates ();
1669
 
          this._monitor = new Zeitgeist.Monitor (new Zeitgeist.TimeRange.from_now (),
1670
 
              (owned) monitor_events);
1671
 
          this._monitor.events_inserted.connect (this._handle_new_interaction);
1672
 
          this._log.install_monitor (this._monitor);
1673
 
        }
1674
 
 
1675
 
      this._notify_if_is_quiescent ();
1676
 
    }
1677
2704
}