tpf-persona.vala 29.5 KB
Newer Older
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/*
 * Copyright (C) 2010 Collabora Ltd.
 *
 * This library is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 2.1 of the License, or
 * (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this library.  If not, see <http://www.gnu.org/licenses/>.
 *
 * Authors:
 *       Travis Reitter <travis.reitter@collabora.co.uk>
 */

21
using Gee;
22
using GLib;
23
using TelepathyGLib;
24
using Folks;
25

26
27
28
29
/**
 * A persona subclass which represents a single instant messaging contact from
 * Telepathy.
 */
30
public class Tpf.Persona : Folks.Persona,
31
    AliasDetails,
32
    AvatarDetails,
33
    BirthdayDetails,
34
    EmailDetails,
35
    FavouriteDetails,
36
    GroupDetails,
Travis Reitter's avatar
Travis Reitter committed
37
    ImDetails,
38
    NameDetails,
39
    PhoneDetails,
40
41
    PresenceDetails,
    UrlDetails
42
{
43
  private HashSet<string> _groups;
44
  private Set<string> _groups_ro;
Philip Withnall's avatar
Philip Withnall committed
45
  private bool _is_favourite;
46
  private string _alias; /* must never be null */
47
  private string _full_name; /* must never be null */
48
  private HashMultiMap<string, ImFieldDetails> _im_addresses;
49
  private const string[] _linkable_properties = { "im-addresses" };
50
  private const string[] _always_writeable_properties =
51
52
53
54
55
    {
      "alias",
      "is-favourite",
      "groups"
    };
56
  private string[] _writeable_properties = null;
57

58
59
60
61
  /* Whether we've finished being constructed; this is used to prevent
   * unnecessary trips to the Telepathy service to tell it about properties
   * being set which are actually just being set from data it's just given us.
   */
62
  private bool _is_constructed = false;
63

64
65
66
67
68
69
70
71
  /**
   * Whether the Persona is in the user's contact list.
   *
   * This will be true for most {@link Folks.Persona}s, but may not be true for
   * personas where {@link Folks.Persona.is_user} is true. If it's false in
   * this case, it means that the persona has been retrieved from the Telepathy
   * connection, but has not been added to the user's contact list.
   *
72
   * @since 0.3.5
73
74
75
   */
  public bool is_in_contact_list { get; set; }

76
77
  private LoadableIcon? _avatar = null;

78
  /**
79
80
   * An avatar for the Persona.
   *
81
   * See {@link Folks.AvatarDetails.avatar}.
82
   *
Travis Reitter's avatar
Travis Reitter committed
83
   * @since 0.6.0
84
   */
85
86
87
88
89
90
  [CCode (notify = false)]
  public LoadableIcon? avatar
    {
      get { return this._avatar; }
      set { this.change_avatar.begin (value); } /* not writeable */
    }
91

92
93
94
  /**
   * {@inheritDoc}
   *
Travis Reitter's avatar
Travis Reitter committed
95
   * @since 0.6.4
96
97
98
99
100
101
102
103
104
105
106
   */
  [CCode (notify = false)]
  public StructuredName? structured_name
    {
      get { return null; }
      set { this.change_structured_name.begin (value); } /* not writeable */
    }

  /**
   * {@inheritDoc}
   *
Travis Reitter's avatar
Travis Reitter committed
107
   * @since 0.6.4
108
109
110
111
112
113
114
115
116
117
118
   */
  [CCode (notify = false)]
  public string full_name
    {
      get { return this._full_name; }
      set { this.change_full_name.begin (value); }
    }

  /**
   * {@inheritDoc}
   *
Travis Reitter's avatar
Travis Reitter committed
119
   * @since 0.6.4
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
   */
  public async void change_full_name (string full_name) throws PropertyError
    {
      var tpf_store = this.store as Tpf.PersonaStore;

      if (full_name == this._full_name)
        return;

      if (this._is_constructed)
        {
          try
            {
              yield tpf_store.change_user_full_name (this, full_name);
            }
          catch (PersonaStoreError.INVALID_ARGUMENT e1)
            {
              throw new PropertyError.NOT_WRITEABLE (e1.message);
            }
          catch (PersonaStoreError.STORE_OFFLINE e2)
            {
              throw new PropertyError.UNKNOWN_ERROR (e2.message);
            }
          catch (PersonaStoreError e3)
            {
              throw new PropertyError.UNKNOWN_ERROR (e3.message);
            }
        }

      /* the change will be notified when we receive changes to
       * contact.contact_info */
    }

  /**
   * {@inheritDoc}
   *
Travis Reitter's avatar
Travis Reitter committed
155
   * @since 0.6.4
156
157
158
159
160
161
162
163
   */
  [CCode (notify = false)]
  public string nickname
    {
      get { return ""; }
      set { this.change_nickname.begin (value); } /* not writeable */
    }

164
165
166
167
168
  /**
   * {@inheritDoc}
   *
   * ContactInfo has no equivalent field, so this is unsupported.
   *
Travis Reitter's avatar
Travis Reitter committed
169
   * @since 0.6.4
170
171
172
173
174
175
176
177
178
179
180
181
   */
  [CCode (notify = false)]
  public string? calendar_event_id
    {
      get { return null; } /* unsupported */
      set { this.change_calendar_event_id.begin (value); } /* not writeable */
    }

  private DateTime? _birthday = null;
  /**
   * {@inheritDoc}
   *
Travis Reitter's avatar
Travis Reitter committed
182
   * @since 0.6.4
183
184
185
186
187
188
189
190
191
192
193
   */
  [CCode (notify = false)]
  public DateTime? birthday
    {
      get { return this._birthday; }
      set { this.change_birthday.begin (value); }
    }

  /**
   * {@inheritDoc}
   *
Travis Reitter's avatar
Travis Reitter committed
194
   * @since 0.6.4
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
   */
  public async void change_birthday (DateTime? birthday) throws PropertyError
    {
      var tpf_store = this.store as Tpf.PersonaStore;

      if (birthday != null && this._birthday != null &&
          birthday.equal (this._birthday))
        {
          return;
        }

      if (this._is_constructed)
        {
          try
            {
              yield tpf_store.change_user_birthday (this, birthday);
            }
          catch (PersonaStoreError.INVALID_ARGUMENT e1)
            {
              throw new PropertyError.NOT_WRITEABLE (e1.message);
            }
          catch (PersonaStoreError.STORE_OFFLINE e2)
            {
              throw new PropertyError.UNKNOWN_ERROR (e2.message);
            }
          catch (PersonaStoreError e3)
            {
              throw new PropertyError.UNKNOWN_ERROR (e3.message);
            }
        }

      /* the change will be notified when we receive changes to
       * contact.contact_info */
    }

230
  /**
231
232
   * The Persona's presence type.
   *
233
   * See {@link Folks.PresenceDetails.presence_type}.
234
   */
235
  public Folks.PresenceType presence_type { get; private set; }
236

237
238
239
240
241
  /**
   * The Persona's presence status.
   *
   * See {@link Folks.PresenceDetails.presence_status}.
   *
Travis Reitter's avatar
Travis Reitter committed
242
   * @since 0.6.0
243
244
245
   */
  public string presence_status { get; private set; }

246
  /**
247
248
   * The Persona's presence message.
   *
249
   * See {@link Folks.PresenceDetails.presence_message}.
250
   */
251
  public string presence_message { get; private set; }
Travis Reitter's avatar
Travis Reitter committed
252

253
254
255
256
257
258
259
260
261
262
  /**
   * The names of the Persona's linkable properties.
   *
   * See {@link Folks.Persona.linkable_properties}.
   */
  public override string[] linkable_properties
    {
      get { return this._linkable_properties; }
    }

263
264
265
  /**
   * {@inheritDoc}
   *
Travis Reitter's avatar
Travis Reitter committed
266
   * @since 0.6.0
267
268
269
   */
  public override string[] writeable_properties
    {
270
271
272
      get
        {
          if (this.is_user)
273
            return this._writeable_properties;
274
275
276

          return this._always_writeable_properties;
        }
277
278
    }

279
  /**
280
281
   * An alias for the Persona.
   *
282
   * See {@link Folks.AliasDetails.alias}.
283
   */
284
  [CCode (notify = false)]
285
286
287
  public string alias
    {
      get { return this._alias; }
288
289
      set { this.change_alias.begin (value); }
    }
290

291
292
293
  /**
   * {@inheritDoc}
   *
Raul Gutierrez Segales's avatar
Raul Gutierrez Segales committed
294
   * @since 0.6.2
295
296
297
298
   */
  public async void change_alias (string alias) throws PropertyError
    {
      if (this._alias == alias)
299
        {
300
301
          return;
        }
302

303
304
305
      if (this._is_constructed)
        {
          yield ((Tpf.PersonaStore) this.store).change_alias (this, alias);
306
        }
307
308
309

      this._alias = alias;
      this.notify_property ("alias");
310
311
    }

312
  /**
313
314
   * Whether this Persona is a user-defined favourite.
   *
315
   * See {@link Folks.FavouriteDetails.is_favourite}.
316
   */
317
  [CCode (notify = false)]
318
  public bool is_favourite
Philip Withnall's avatar
Philip Withnall committed
319
320
    {
      get { return this._is_favourite; }
321
322
      set { this.change_is_favourite.begin (value); }
    }
Philip Withnall's avatar
Philip Withnall committed
323

324
325
326
  /**
   * {@inheritDoc}
   *
Raul Gutierrez Segales's avatar
Raul Gutierrez Segales committed
327
   * @since 0.6.2
328
329
330
331
   */
  public async void change_is_favourite (bool is_favourite) throws PropertyError
    {
      if (this._is_favourite == is_favourite)
Philip Withnall's avatar
Philip Withnall committed
332
        {
333
334
          return;
        }
Philip Withnall's avatar
Philip Withnall committed
335

336
337
338
339
      if (this._is_constructed)
        {
          yield ((Tpf.PersonaStore) this.store).change_is_favourite (this,
              is_favourite);
Philip Withnall's avatar
Philip Withnall committed
340
        }
341
342
343

      this._is_favourite = is_favourite;
      this.notify_property ("is-favourite");
Philip Withnall's avatar
Philip Withnall committed
344
345
    }

346
347
348
349
350
351
  private HashSet<EmailFieldDetails> _email_addresses;
  private Set<EmailFieldDetails> _email_addresses_ro;

  /**
   * {@inheritDoc}
   *
Travis Reitter's avatar
Travis Reitter committed
352
   * @since 0.6.4
353
354
355
356
357
358
359
360
361
362
363
   */
  [CCode (notify = false)]
  public Set<EmailFieldDetails> email_addresses
    {
      get { return this._email_addresses_ro; }
      set { this.change_email_addresses.begin (value); }
    }

  /**
   * {@inheritDoc}
   *
Travis Reitter's avatar
Travis Reitter committed
364
   * @since 0.6.4
365
366
367
368
369
370
371
372
   */
  public async void change_email_addresses (
      Set<EmailFieldDetails> email_addresses) throws PropertyError
    {
      yield this._change_details<EmailFieldDetails> (email_addresses,
          this._email_addresses, "email");
    }

373
  /**
374
   * A mapping of IM protocol to an (unordered) set of IM addresses.
375
   *
Travis Reitter's avatar
Travis Reitter committed
376
   * See {@link Folks.ImDetails.im_addresses}.
377
   */
378
  [CCode (notify = false)]
379
  public MultiMap<string, ImFieldDetails> im_addresses
380
381
    {
      get { return this._im_addresses; }
382
      set { this.change_im_addresses.begin (value); }
383
384
    }

385
  /**
386
387
   * A mapping of group ID to whether the contact is a member.
   *
388
   * See {@link Folks.GroupDetails.groups}.
389
   */
390
  [CCode (notify = false)]
391
  public Set<string> groups
392
    {
393
      get { return this._groups_ro; }
394
      set { this.change_groups.begin (value); }
395
396
    }

397
  /**
398
399
   * Add or remove the Persona from the specified group.
   *
400
   * See {@link Folks.GroupDetails.change_group}.
401
   */
402
  public async void change_group (string group, bool is_member)
403
    {
404
      if (this._change_group (group, is_member))
405
        {
406
          Tpf.PersonaStore store = (Tpf.PersonaStore) this.store;
407
          yield store._change_group_membership (this, group, is_member);
408
409
410
411
        }
    }

  private bool _change_group (string group, bool is_member)
412
    {
413
      var changed = false;
414
415
416

      if (is_member)
        {
417
          if (!this._groups.contains (group))
418
            {
419
              this._groups.add (group);
420
421
422
423
424
425
              changed = true;
            }
        }
      else
        changed = this._groups.remove (group);

426
427
428
      if (changed == true)
        this.group_changed (group, is_member);

429
      return changed;
430
431
    }

432
433
434
  /**
   * {@inheritDoc}
   *
Raul Gutierrez Segales's avatar
Raul Gutierrez Segales committed
435
   * @since 0.6.2
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
   */
  public async void change_groups (Set<string> groups) throws PropertyError
    {
      Tpf.PersonaStore store = (Tpf.PersonaStore) this.store;

      foreach (var group1 in groups)
        {
          if (this._groups.contains (group1) == false)
            yield store._change_group_membership (this, group1, true);
        }

      foreach (var group2 in this._groups)
        {
          if (groups.contains (group2) == false)
            yield store._change_group_membership (this, group2, false);
        }

      this.notify_property ("groups");
    }

456
457
  /**
   * The Telepathy contact represented by this persona.
458
459
460
461
462
463
   *
   * Note that this may be `null` if the {@link PersonaStore} providing this
   * {@link Persona} isn't currently available (e.g. due to not being connected
   * to the network). In this case, most other properties of the {@link Persona}
   * are being retrieved from a cache and may not be current (though there's no
   * way to tell this).
464
   */
465
  public Contact? contact { get; construct; }
466

467
468
469
470
471
472
  private HashSet<PhoneFieldDetails> _phone_numbers;
  private Set<PhoneFieldDetails> _phone_numbers_ro;

  /**
   * {@inheritDoc}
   *
Travis Reitter's avatar
Travis Reitter committed
473
   * @since 0.6.4
474
475
476
477
478
479
480
481
   */
  [CCode (notify = false)]
  public Set<PhoneFieldDetails> phone_numbers
    {
      get { return this._phone_numbers_ro; }
      set { this.change_phone_numbers.begin (value); }
    }

482
483
484
  /**
   * {@inheritDoc}
   *
Travis Reitter's avatar
Travis Reitter committed
485
   * @since 0.6.4
486
487
488
489
490
491
492
493
   */
  public async void change_phone_numbers (
      Set<PhoneFieldDetails> phone_numbers) throws PropertyError
    {
      yield this._change_details<PhoneFieldDetails> (phone_numbers,
          this._phone_numbers, "tel");
    }

494
495
496
497
498
499
  private HashSet<UrlFieldDetails> _urls;
  private Set<UrlFieldDetails> _urls_ro;

  /**
   * {@inheritDoc}
   *
Travis Reitter's avatar
Travis Reitter committed
500
   * @since 0.6.4
501
502
503
504
505
506
507
508
509
510
511
   */
  [CCode (notify = false)]
  public Set<UrlFieldDetails> urls
    {
      get { return this._urls_ro; }
      set { this.change_urls.begin (value); }
    }

  /**
   * {@inheritDoc}
   *
Travis Reitter's avatar
Travis Reitter committed
512
   * @since 0.6.4
513
514
515
516
517
518
519
   */
  public async void change_urls (Set<UrlFieldDetails> urls) throws PropertyError
    {
      yield this._change_details<UrlFieldDetails> (urls,
          this._urls, "url");
    }

520
521
522
523
524
525
526
527
  private async void _change_details<T> (
      Set<AbstractFieldDetails<string>> details,
      Set<AbstractFieldDetails<string>> member_set,
      string field_name)
        throws PropertyError
    {
      var tpf_store = this.store as Tpf.PersonaStore;

528
      if (Folks.Internal.equal_sets<T> (details, member_set))
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
        {
          return;
        }

      if (this._is_constructed)
        {
          try
            {
              yield tpf_store._change_user_details (this, details, field_name);
            }
          catch (PersonaStoreError.INVALID_ARGUMENT e1)
            {
              throw new PropertyError.NOT_WRITEABLE (e1.message);
            }
          catch (PersonaStoreError.STORE_OFFLINE e2)
            {
              throw new PropertyError.UNKNOWN_ERROR (e2.message);
            }
          catch (PersonaStoreError e3)
            {
              throw new PropertyError.UNKNOWN_ERROR (e3.message);
            }
        }

      /* the change will be notified when we receive changes to
       * contact.contact_info */
    }

557
558
559
560
561
  /**
   * Create a new persona.
   *
   * Create a new persona for the {@link PersonaStore} `store`, representing
   * the Telepathy contact given by `contact`.
562
563
564
   *
   * @param contact the Telepathy contact being represented by the persona
   * @param store the persona store to place the persona in
565
   */
566
  public Persona (Contact contact, PersonaStore store)
567
    {
568
      unowned string id = contact.get_identifier ();
569
      var connection = contact.connection;
570
      var account = this._account_for_connection (connection);
571
      var uid = this.build_uid (store.type_id, store.id, id);
572

573
      Object (alias: contact.get_alias (),
574
              contact: contact,
575
              display_id: id,
Travis Reitter's avatar
Travis Reitter committed
576
              /* FIXME: This IID format should be moved out to the ImDetails
577
578
579
580
               * interface along with the code in
               * Kf.Persona.linkable_property_to_links(), but that depends on
               * bgo#624842 being fixed. */
              iid: account.get_protocol () + ":" + id,
Travis Reitter's avatar
Travis Reitter committed
581
              uid: uid,
582
              store: store,
583
              is_user: contact.handle == connection.self_handle);
584

585

586
587
      this._full_name = "";

588
589
      contact.notify["alias"].connect ((s, p) =>
          {
590
591
592
            /* Tp guarantees that aliases are always non-null. */
            assert (this.contact.alias != null);

593
            if (this._alias != this.contact.alias)
594
              {
595
                this._alias = this.contact.alias;
596
597
598
599
                this.notify_property ("alias");
              }
          });

600
601
      debug ("Creating new Tpf.Persona '%s' for service-specific UID '%s': %p",
          uid, id, this);
602
      this._is_constructed = true;
603

604
      /* Set our single IM address */
605
606
607
608
      this._im_addresses = new HashMultiMap<string, ImFieldDetails> (
          null, null,
          (GLib.HashFunc) ImFieldDetails.hash,
          (GLib.EqualFunc) ImFieldDetails.equal);
609

610
611
      try
        {
612
613
614
615
          var im_addr = ImDetails.normalise_im_address (id,
              account.get_protocol ());
          var im_fd = new ImFieldDetails (im_addr);
          this._im_addresses.set (account.get_protocol (), im_fd);
616
        }
Travis Reitter's avatar
Travis Reitter committed
617
      catch (ImDetailsError e)
618
619
620
621
        {
          /* This should never happen…but if it does, warn of it and continue */
          warning (e.message);
        }
622
623

      /* Groups */
624
      this._groups = new HashSet<string> ();
625
      this._groups_ro = this._groups.read_only_view;
626

627
628
629
630
      this._email_addresses = new HashSet<EmailFieldDetails> (
          (GLib.HashFunc) EmailFieldDetails.hash,
          (GLib.EqualFunc) EmailFieldDetails.equal);
      this._email_addresses_ro = this._email_addresses.read_only_view;
631
632
633
634
      this._phone_numbers = new HashSet<PhoneFieldDetails> (
          (GLib.HashFunc) PhoneFieldDetails.hash,
          (GLib.EqualFunc) PhoneFieldDetails.equal);
      this._phone_numbers_ro = this._phone_numbers.read_only_view;
635
636
637
638
      this._urls = new HashSet<UrlFieldDetails> (
          (GLib.HashFunc) UrlFieldDetails.hash,
          (GLib.EqualFunc) UrlFieldDetails.equal);
      this._urls_ro = this._urls.read_only_view;
639

640
641
      contact.notify["avatar-file"].connect ((s, p) =>
        {
642
          this._contact_notify_avatar ();
643
        });
644
      this._contact_notify_avatar ();
645

646
647
      contact.notify["presence-message"].connect ((s, p) =>
        {
648
          this._contact_notify_presence_message ();
649
650
651
        });
      contact.notify["presence-type"].connect ((s, p) =>
        {
652
          this._contact_notify_presence_type ();
653
        });
654
655
656
657
      contact.notify["presence-status"].connect ((s, p) =>
        {
          this._contact_notify_presence_status ();
        });
658
659
      this._contact_notify_presence_message ();
      this._contact_notify_presence_type ();
660
      this._contact_notify_presence_status ();
661

662
663
      contact.notify["contact-info"].connect ((s, p) =>
        {
664
          this._contact_notify_contact_info ();
665
        });
666
      this._contact_notify_contact_info ();
667

668
669
670
671
672
      ((Tpf.PersonaStore) this.store).group_members_changed.connect (
          (s, group, added, removed) =>
            {
              if (added.find (this) != null)
                this._change_group (group, true);
673

674
675
676
              if (removed.find (this) != null)
                this._change_group (group, false);
            });
677

678
679
680
      ((Tpf.PersonaStore) this.store).group_removed.connect (
          (s, group, error) =>
            {
681
682
683
684
685
686
687
              /* FIXME: Can't use
               * !(error is TelepathyGLib.DBusError.OBJECT_REMOVED) because the
               * GIR bindings don't annotate errors */
              if (error != null &&
                  (error.domain != TelepathyGLib.dbus_errors_quark () ||
                   error.code != TelepathyGLib.DBusError.OBJECT_REMOVED))
                {
688
                  debug ("Group invalidated: %s", error.message);
689
                  this._change_group (group, false);
690
                }
691
            });
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708

      if (this.is_user)
        {
          ((Tpf.PersonaStore) this.store).notify["supported-fields"].connect (
            (s, p) =>
              {
                this._store_notify_supported_fields ();
              });
          this._store_notify_supported_fields ();
        }
    }

  private void _store_notify_supported_fields ()
    {
      var tpf_store = this.store as Tpf.PersonaStore;
      this._writeable_properties = this._always_writeable_properties;

709
710
      if ("bday" in tpf_store.supported_fields)
        this._writeable_properties += "birthday";
711
712
      if ("email" in tpf_store.supported_fields)
        this._writeable_properties += "email-addresses";
713
714
715
716
      if ("fn" in tpf_store.supported_fields)
        this._writeable_properties += "full-name";
      if ("tel" in tpf_store.supported_fields)
        this._writeable_properties += "phone-numbers";
717
718
      if ("url" in tpf_store.supported_fields)
        this._writeable_properties += "urls";
719
720
    }

721
  private void _contact_notify_contact_info ()
722
    {
723
      var new_birthday_str = "";
724
      var new_full_name = "";
725
726
727
      var new_email_addresses = new HashSet<EmailFieldDetails> (
          (GLib.HashFunc) EmailFieldDetails.hash,
          (GLib.EqualFunc) EmailFieldDetails.equal);
728
729
730
      var new_phone_numbers = new HashSet<PhoneFieldDetails> (
          (GLib.HashFunc) PhoneFieldDetails.hash,
          (GLib.EqualFunc) PhoneFieldDetails.equal);
731
732
733
      var new_urls = new HashSet<UrlFieldDetails> (
          (GLib.HashFunc) UrlFieldDetails.hash,
          (GLib.EqualFunc) UrlFieldDetails.equal);
734
735
736
737

      var contact_info = this.contact.get_contact_info ();
      foreach (var info in contact_info)
        {
738
          if (info.field_name == "") {}
739
740
741
742
          else if (info.field_name == "bday")
            {
              new_birthday_str = info.field_value[0];
            }
743
744
745
746
747
748
749
750
751
          else if (info.field_name == "email")
            {
              foreach (var email_addr in info.field_value)
                {
                  var parameters = this._afd_params_from_strv (info.parameters);
                  var email_fd = new EmailFieldDetails (email_addr, parameters);
                  new_email_addresses.add (email_fd);
                }
            }
752
753
754
755
756
          else if (info.field_name == "fn")
            {
              new_full_name = info.field_value[0];
            }
          else if (info.field_name == "tel")
757
            {
758
759
760
761
762
763
              foreach (var phone_num in info.field_value)
                {
                  var parameters = this._afd_params_from_strv (info.parameters);
                  var phone_fd = new PhoneFieldDetails (phone_num, parameters);
                  new_phone_numbers.add (phone_fd);
                }
764
            }
765
766
767
768
769
770
771
772
773
          else if (info.field_name == "url")
            {
              foreach (var url in info.field_value)
                {
                  var parameters = this._afd_params_from_strv (info.parameters);
                  var url_fd = new UrlFieldDetails (url, parameters);
                  new_urls.add (url_fd);
                }
            }
774
775
        }

776
777
778
      if (new_birthday_str != "")
        {
          var timeval = TimeVal ();
779
          if (timeval.from_iso8601 (new_birthday_str))
780
            {
781
782
783
784
785
786
787
788
789
790
791
792
793
              var d = new DateTime.from_timeval_utc (timeval);
              if (this._birthday == null ||
                  (this._birthday != null &&
                    !this._birthday.equal (d.to_utc ())))
                {
                  this._birthday = d.to_utc ();
                  this.notify_property ("birthday");
                }
            }
          else
            {
              warning ("Failed to parse new birthday string '%s'",
                  new_birthday_str);
794
795
796
797
798
799
800
801
802
803
804
            }
        }
      else
        {
          if (this._birthday != null)
            {
              this._birthday = null;
              this.notify_property ("birthday");
            }
        }

805
806
807
808
809
810
811
812
      if (!Folks.Internal.equal_sets<EmailFieldDetails> (new_email_addresses,
              this._email_addresses))
        {
          this._email_addresses = new_email_addresses;
          this._email_addresses_ro = new_email_addresses.read_only_view;
          this.notify_property ("email-addresses");
        }

813
814
815
816
817
818
      if (new_full_name != this._full_name)
        {
          this._full_name = new_full_name;
          this.notify_property ("full-name");
        }

819
      if (!Folks.Internal.equal_sets<PhoneFieldDetails> (new_phone_numbers,
820
821
822
823
824
825
              this._phone_numbers))
        {
          this._phone_numbers = new_phone_numbers;
          this._phone_numbers_ro = new_phone_numbers.read_only_view;
          this.notify_property ("phone-numbers");
        }
826
827
828
829
830
831
832

      if (!Folks.Internal.equal_sets<UrlFieldDetails> (new_urls, this._urls))
        {
          this._urls = new_urls;
          this._urls_ro = new_urls.read_only_view;
          this.notify_property ("urls");
        }
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
    }

  private MultiMap<string, string> _afd_params_from_strv (string[] parameters)
    {
      var retval = new HashMultiMap<string, string> ();

      foreach (var entry in parameters)
        {
          var tokens = entry.split ("=", 2);
          if (tokens.length == 2)
            {
              retval.set (tokens[0], tokens[1]);
            }
          else
            {
              warning ("Failed to parse vCard parameter from string '%s'",
                  entry);
            }
        }

      return retval;
    }

856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
  /**
   * Create a new persona for the {@link PersonaStore} `store`, representing
   * a cached contact for which we currently have no Telepathy contact.
   *
   * @param store The persona store to place the persona in.
   * @param uid The cached UID of the persona.
   * @param iid The cached IID of the persona.
   * @param im_address The cached IM address of the persona (excluding
   * protocol).
   * @param protocol The cached protocol of the persona.
   * @param groups The cached set of groups the persona is in.
   * @param is_favourite Whether the persona is a favourite.
   * @param alias The cached alias for the persona.
   * @param is_in_contact_list Whether the persona is in the user's contact
   * list.
   * @param is_user Whether the persona is the user.
   * @param avatar The icon for the persona's cached avatar, or `null` if they
   * have no avatar.
874
875
876
877
878
879
880
881
882
883
   * @param birthday The date/time of birth of the persona, or `null` if it's
   * unknown.
   * @param full_name The persona's full name, or the empty string if it's
   * unknown.
   * @param email_addresses A set of the persona's e-mail addresses, which may
   * be empty (but may not be `null`).
   * @param phone_numbers A set of the persona's phone numbers, which may be
   * empty (but may not be `null`).
   * @param urls A set of the persona's URLs, which may be empty (but may not be
   * `null`).
884
885
   * @return A new {@link Tpf.Persona} representing the cached persona.
   *
Travis Reitter's avatar
Travis Reitter committed
886
   * @since 0.6.0
887
888
889
890
   */
  internal Persona.from_cache (PersonaStore store, string uid, string iid,
      string im_address, string protocol, HashSet<string> groups,
      bool is_favourite, string alias, bool is_in_contact_list, bool is_user,
891
892
893
      LoadableIcon? avatar, DateTime? birthday, string full_name,
      HashSet<EmailFieldDetails> email_addresses,
      HashSet<PhoneFieldDetails> phone_numbers, HashSet<UrlFieldDetails> urls)
894
895
896
897
898
899
900
901
902
903
904
    {
      Object (contact: null,
              display_id: im_address,
              iid: iid,
              uid: uid,
              store: store,
              is_user: is_user);

      debug ("Creating new Tpf.Persona '%s' from cache: %p", uid, this);

      // IM addresses
905
906
907
908
909
910
      this._im_addresses = new HashMultiMap<string, ImFieldDetails> (null, null,
          (GLib.HashFunc) ImFieldDetails.hash,
          (GLib.EqualFunc) ImFieldDetails.equal);

      var im_fd = new ImFieldDetails (im_address);
      this._im_addresses.set (protocol, im_fd);
911
912
913
914
915

      // Groups
      this._groups = groups;
      this._groups_ro = this._groups.read_only_view;

916
917
918
919
920
921
922
923
924
925
926
927
      // E-mail addresses
      this._email_addresses = email_addresses;
      this._email_addresses_ro = this._email_addresses.read_only_view;

      // Phone numbers
      this._phone_numbers = phone_numbers;
      this._phone_numbers_ro = this._phone_numbers.read_only_view;

      // URLs
      this._urls = urls;
      this._urls_ro = this._urls.read_only_view;

928
      // Other properties
929
930
931
932
933
934
      if (alias == null)
        {
          /* Deal with badly-behaved callers */
          alias = "";
        }

935
936
937
938
939
940
      if (full_name == null)
        {
          /* Deal with badly-behaved callers */
          full_name = "";
        }

941
942
943
      this._alias = alias;
      this._is_favourite = is_favourite;
      this.is_in_contact_list = is_in_contact_list;
944
      this._avatar = avatar;
945
946
      this._birthday = birthday;
      this._full_name = full_name;
947
948
949
950
951
952

      // Make the persona appear offline
      this.presence_type = PresenceType.OFFLINE;
      this.presence_message = "";
    }

953
954
955
956
957
  ~Persona ()
    {
      debug ("Destroying Tpf.Persona '%s': %p", this.uid, this);
    }

958
  private static Account? _account_for_connection (Connection conn)
959
960
    {
      var manager = AccountManager.dup ();
961
      var accounts = manager.get_valid_accounts ();
962
963
964
965

      Account account_found = null;
      accounts.foreach ((l) =>
        {
966
          unowned Account account = (Account) l;
967
          if (account.connection == conn)
968
969
970
971
972
973
974
975
976
            {
              account_found = account;
              return;
            }
        });

      return account_found;
    }

977
  private void _contact_notify_presence_message ()
978
    {
979
      this.presence_message = this.contact.get_presence_message ();
980
981
    }

982
  private void _contact_notify_presence_type ()
983
    {
984
      this.presence_type = Tpf.Persona._folks_presence_type_from_tp (
985
          this.contact.get_presence_type ());
986
987
    }

988
989
990
991
992
  private void _contact_notify_presence_status ()
    {
      this.presence_status = this.contact.get_presence_status ();
    }

993
  private static PresenceType _folks_presence_type_from_tp (
994
      TelepathyGLib.ConnectionPresenceType type)
995
996
997
    {
      switch (type)
        {
998
          case TelepathyGLib.ConnectionPresenceType.AVAILABLE:
999
            return PresenceType.AVAILABLE;
1000
          case TelepathyGLib.ConnectionPresenceType.AWAY:
1001
            return PresenceType.AWAY;
1002
          case TelepathyGLib.ConnectionPresenceType.BUSY:
1003
            return PresenceType.BUSY;
1004
          case TelepathyGLib.ConnectionPresenceType.ERROR:
1005
            return PresenceType.ERROR;
1006
          case TelepathyGLib.ConnectionPresenceType.EXTENDED_AWAY:
1007
            return PresenceType.EXTENDED_AWAY;
1008
          case TelepathyGLib.ConnectionPresenceType.HIDDEN:
1009
            return PresenceType.HIDDEN;
1010
          case TelepathyGLib.ConnectionPresenceType.OFFLINE:
1011
            return PresenceType.OFFLINE;
1012
          case TelepathyGLib.ConnectionPresenceType.UNKNOWN:
1013
            return PresenceType.UNKNOWN;
1014
          case TelepathyGLib.ConnectionPresenceType.UNSET:
1015
1016
1017
1018
            return PresenceType.UNSET;
          default:
            return PresenceType.UNKNOWN;
        }
1019
    }
1020

1021
  private void _contact_notify_avatar ()
1022
    {
1023
      var file = this.contact.avatar_file;
1024
1025
1026
1027
1028
      Icon? icon = null;

      if (file != null)
        icon = new FileIcon (file);

1029
1030
1031
1032
1033
      if (this._avatar == null || icon == null || !this._avatar.equal (icon))
        {
          this._avatar = (LoadableIcon) icon;
          this.notify_property ("avatar");
        }
1034
    }
1035
}