tpf-persona.vala 18.1 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
    FavouriteDetails,
34
    GroupDetails,
Travis Reitter's avatar
Travis Reitter committed
35
    ImDetails,
36
    PhoneDetails,
37
    PresenceDetails
38
{
39
  private HashSet<string> _groups;
40
  private Set<string> _groups_ro;
Philip Withnall's avatar
Philip Withnall committed
41
  private bool _is_favourite;
42
  private string _alias; /* must never be null */
43
  private HashMultiMap<string, ImFieldDetails> _im_addresses;
44
  private const string[] _linkable_properties = { "im-addresses" };
45
46
47
48
49
50
  private const string[] _writeable_properties =
    {
      "alias",
      "is-favourite",
      "groups"
    };
51

52
53
54
55
  /* 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.
   */
56
  private bool _is_constructed = false;
57

58
59
60
61
62
63
64
65
  /**
   * 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.
   *
66
   * @since 0.3.5
67
68
69
   */
  public bool is_in_contact_list { get; set; }

70
71
  private LoadableIcon? _avatar = null;

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

86
  /**
87
88
   * The Persona's presence type.
   *
89
   * See {@link Folks.PresenceDetails.presence_type}.
90
   */
91
  public Folks.PresenceType presence_type { get; private set; }
92

93
94
95
96
97
  /**
   * The Persona's presence status.
   *
   * See {@link Folks.PresenceDetails.presence_status}.
   *
Travis Reitter's avatar
Travis Reitter committed
98
   * @since 0.6.0
99
100
101
   */
  public string presence_status { get; private set; }

102
  /**
103
104
   * The Persona's presence message.
   *
105
   * See {@link Folks.PresenceDetails.presence_message}.
106
   */
107
  public string presence_message { get; private set; }
Travis Reitter's avatar
Travis Reitter committed
108

109
110
111
112
113
114
115
116
117
118
  /**
   * The names of the Persona's linkable properties.
   *
   * See {@link Folks.Persona.linkable_properties}.
   */
  public override string[] linkable_properties
    {
      get { return this._linkable_properties; }
    }

119
120
121
  /**
   * {@inheritDoc}
   *
Travis Reitter's avatar
Travis Reitter committed
122
   * @since 0.6.0
123
124
125
126
127
128
   */
  public override string[] writeable_properties
    {
      get { return this._writeable_properties; }
    }

129
  /**
130
131
   * An alias for the Persona.
   *
132
   * See {@link Folks.AliasDetails.alias}.
133
   */
134
  [CCode (notify = false)]
135
136
137
  public string alias
    {
      get { return this._alias; }
138
139
      set { this.change_alias.begin (value); }
    }
140

141
142
143
  /**
   * {@inheritDoc}
   *
Raul Gutierrez Segales's avatar
Raul Gutierrez Segales committed
144
   * @since 0.6.2
145
146
147
148
   */
  public async void change_alias (string alias) throws PropertyError
    {
      if (this._alias == alias)
149
        {
150
151
          return;
        }
152

153
154
155
      if (this._is_constructed)
        {
          yield ((Tpf.PersonaStore) this.store).change_alias (this, alias);
156
        }
157
158
159

      this._alias = alias;
      this.notify_property ("alias");
160
161
    }

162
  /**
163
164
   * Whether this Persona is a user-defined favourite.
   *
165
   * See {@link Folks.FavouriteDetails.is_favourite}.
166
   */
167
  [CCode (notify = false)]
168
  public bool is_favourite
Philip Withnall's avatar
Philip Withnall committed
169
170
    {
      get { return this._is_favourite; }
171
172
      set { this.change_is_favourite.begin (value); }
    }
Philip Withnall's avatar
Philip Withnall committed
173

174
175
176
  /**
   * {@inheritDoc}
   *
Raul Gutierrez Segales's avatar
Raul Gutierrez Segales committed
177
   * @since 0.6.2
178
179
180
181
   */
  public async void change_is_favourite (bool is_favourite) throws PropertyError
    {
      if (this._is_favourite == is_favourite)
Philip Withnall's avatar
Philip Withnall committed
182
        {
183
184
          return;
        }
Philip Withnall's avatar
Philip Withnall committed
185

186
187
188
189
      if (this._is_constructed)
        {
          yield ((Tpf.PersonaStore) this.store).change_is_favourite (this,
              is_favourite);
Philip Withnall's avatar
Philip Withnall committed
190
        }
191
192
193

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

196
  /**
197
   * A mapping of IM protocol to an (unordered) set of IM addresses.
198
   *
Travis Reitter's avatar
Travis Reitter committed
199
   * See {@link Folks.ImDetails.im_addresses}.
200
   */
201
  [CCode (notify = false)]
202
  public MultiMap<string, ImFieldDetails> im_addresses
203
204
    {
      get { return this._im_addresses; }
205
      set { this.change_im_addresses.begin (value); }
206
207
    }

208
  /**
209
210
   * A mapping of group ID to whether the contact is a member.
   *
211
   * See {@link Folks.GroupDetails.groups}.
212
   */
213
  [CCode (notify = false)]
214
  public Set<string> groups
215
    {
216
      get { return this._groups_ro; }
217
      set { this.change_groups.begin (value); }
218
219
    }

220
  /**
221
222
   * Add or remove the Persona from the specified group.
   *
223
   * See {@link Folks.GroupDetails.change_group}.
224
   */
225
  public async void change_group (string group, bool is_member)
226
    {
227
      if (this._change_group (group, is_member))
228
        {
229
          Tpf.PersonaStore store = (Tpf.PersonaStore) this.store;
230
          yield store._change_group_membership (this, group, is_member);
231
232
233
234
        }
    }

  private bool _change_group (string group, bool is_member)
235
    {
236
      var changed = false;
237
238
239

      if (is_member)
        {
240
          if (!this._groups.contains (group))
241
            {
242
              this._groups.add (group);
243
244
245
246
247
248
              changed = true;
            }
        }
      else
        changed = this._groups.remove (group);

249
250
251
      if (changed == true)
        this.group_changed (group, is_member);

252
      return changed;
253
254
    }

255
256
257
  /**
   * {@inheritDoc}
   *
Raul Gutierrez Segales's avatar
Raul Gutierrez Segales committed
258
   * @since 0.6.2
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
   */
  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");
    }

279
280
  /**
   * The Telepathy contact represented by this persona.
281
282
283
284
285
286
   *
   * 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).
287
   */
288
  public Contact? contact { get; construct; }
289

290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
  private HashSet<PhoneFieldDetails> _phone_numbers;
  private Set<PhoneFieldDetails> _phone_numbers_ro;

  /**
   * {@inheritDoc}
   *
   * @since UNRELEASED
   */
  [CCode (notify = false)]
  public Set<PhoneFieldDetails> phone_numbers
    {
      get { return this._phone_numbers_ro; }
      set { this.change_phone_numbers.begin (value); }
    }

305
306
307
308
309
  /**
   * Create a new persona.
   *
   * Create a new persona for the {@link PersonaStore} `store`, representing
   * the Telepathy contact given by `contact`.
310
311
312
   *
   * @param contact the Telepathy contact being represented by the persona
   * @param store the persona store to place the persona in
313
   */
314
  public Persona (Contact contact, PersonaStore store)
315
    {
316
      unowned string id = contact.get_identifier ();
317
      var connection = contact.connection;
318
      var account = this._account_for_connection (connection);
319
      var uid = this.build_uid (store.type_id, store.id, id);
320

321
      Object (alias: contact.get_alias (),
322
              contact: contact,
323
              display_id: id,
Travis Reitter's avatar
Travis Reitter committed
324
              /* FIXME: This IID format should be moved out to the ImDetails
325
326
327
328
               * 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
329
              uid: uid,
330
              store: store,
331
              is_user: contact.handle == connection.self_handle);
332

333
334
      contact.notify["alias"].connect ((s, p) =>
          {
335
336
337
            /* Tp guarantees that aliases are always non-null. */
            assert (this.contact.alias != null);

338
            if (this._alias != this.contact.alias)
339
              {
340
                this._alias = this.contact.alias;
341
342
343
344
                this.notify_property ("alias");
              }
          });

345
346
      debug ("Creating new Tpf.Persona '%s' for service-specific UID '%s': %p",
          uid, id, this);
347
      this._is_constructed = true;
348

349
      /* Set our single IM address */
350
351
352
353
      this._im_addresses = new HashMultiMap<string, ImFieldDetails> (
          null, null,
          (GLib.HashFunc) ImFieldDetails.hash,
          (GLib.EqualFunc) ImFieldDetails.equal);
354

355
356
      try
        {
357
358
359
360
          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);
361
        }
Travis Reitter's avatar
Travis Reitter committed
362
      catch (ImDetailsError e)
363
364
365
366
        {
          /* This should never happen…but if it does, warn of it and continue */
          warning (e.message);
        }
367
368

      /* Groups */
369
      this._groups = new HashSet<string> ();
370
      this._groups_ro = this._groups.read_only_view;
371

372
373
374
375
376
      this._phone_numbers = new HashSet<PhoneFieldDetails> (
          (GLib.HashFunc) PhoneFieldDetails.hash,
          (GLib.EqualFunc) PhoneFieldDetails.equal);
      this._phone_numbers_ro = this._phone_numbers.read_only_view;

377
378
      contact.notify["avatar-file"].connect ((s, p) =>
        {
379
          this._contact_notify_avatar ();
380
        });
381
      this._contact_notify_avatar ();
382

383
384
      contact.notify["presence-message"].connect ((s, p) =>
        {
385
          this._contact_notify_presence_message ();
386
387
388
        });
      contact.notify["presence-type"].connect ((s, p) =>
        {
389
          this._contact_notify_presence_type ();
390
        });
391
392
393
394
      contact.notify["presence-status"].connect ((s, p) =>
        {
          this._contact_notify_presence_status ();
        });
395
396
      this._contact_notify_presence_message ();
      this._contact_notify_presence_type ();
397
      this._contact_notify_presence_status ();
398

399
400
401
402
403
404
      contact.notify["contact-info"].connect ((s, p) =>
        {
          this._contact_notify_phones ();
        });
      this._contact_notify_phones ();

405
406
407
408
409
      ((Tpf.PersonaStore) this.store).group_members_changed.connect (
          (s, group, added, removed) =>
            {
              if (added.find (this) != null)
                this._change_group (group, true);
410

411
412
413
              if (removed.find (this) != null)
                this._change_group (group, false);
            });
414

415
416
417
      ((Tpf.PersonaStore) this.store).group_removed.connect (
          (s, group, error) =>
            {
418
419
420
421
422
423
424
              /* 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))
                {
425
                  debug ("Group invalidated: %s", error.message);
426
                  this._change_group (group, false);
427
                }
428
            });
429
430
    }

431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
  private void _contact_notify_phones ()
    {
      var new_phone_numbers = new HashSet<PhoneFieldDetails> (
          (GLib.HashFunc) PhoneFieldDetails.hash,
          (GLib.EqualFunc) PhoneFieldDetails.equal);

      var contact_info = this.contact.get_contact_info ();
      foreach (var info in contact_info)
        {
          if (info.field_name != "tel")
            continue;

          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);
            }
        }

      if (!Folks.PersonaStore.equal_sets<PhoneFieldDetails> (new_phone_numbers,
              this._phone_numbers))
        {
          this._phone_numbers = new_phone_numbers;
          this._phone_numbers_ro = new_phone_numbers.read_only_view;
          this.notify_property ("phone-numbers");
        }
    }

  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;
    }

481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
  /**
   * 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.
   * @return A new {@link Tpf.Persona} representing the cached persona.
   *
Travis Reitter's avatar
Travis Reitter committed
501
   * @since 0.6.0
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
   */
  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,
      LoadableIcon? avatar)
    {
      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
518
519
520
521
522
523
      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);
524
525
526
527
528
529

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

      // Other properties
530
531
532
533
534
535
      if (alias == null)
        {
          /* Deal with badly-behaved callers */
          alias = "";
        }

536
537
538
      this._alias = alias;
      this._is_favourite = is_favourite;
      this.is_in_contact_list = is_in_contact_list;
539
      this._avatar = avatar;
540
541
542
543
544
545

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

546
547
548
549
550
  ~Persona ()
    {
      debug ("Destroying Tpf.Persona '%s': %p", this.uid, this);
    }

551
  private static Account? _account_for_connection (Connection conn)
552
553
    {
      var manager = AccountManager.dup ();
554
      var accounts = manager.get_valid_accounts ();
555
556
557
558

      Account account_found = null;
      accounts.foreach ((l) =>
        {
559
          unowned Account account = (Account) l;
560
          if (account.connection == conn)
561
562
563
564
565
566
567
568
569
            {
              account_found = account;
              return;
            }
        });

      return account_found;
    }

570
  private void _contact_notify_presence_message ()
571
    {
572
      this.presence_message = this.contact.get_presence_message ();
573
574
    }

575
  private void _contact_notify_presence_type ()
576
    {
577
      this.presence_type = Tpf.Persona._folks_presence_type_from_tp (
578
          this.contact.get_presence_type ());
579
580
    }

581
582
583
584
585
  private void _contact_notify_presence_status ()
    {
      this.presence_status = this.contact.get_presence_status ();
    }

586
  private static PresenceType _folks_presence_type_from_tp (
587
      TelepathyGLib.ConnectionPresenceType type)
588
589
590
    {
      switch (type)
        {
591
          case TelepathyGLib.ConnectionPresenceType.AVAILABLE:
592
            return PresenceType.AVAILABLE;
593
          case TelepathyGLib.ConnectionPresenceType.AWAY:
594
            return PresenceType.AWAY;
595
          case TelepathyGLib.ConnectionPresenceType.BUSY:
596
            return PresenceType.BUSY;
597
          case TelepathyGLib.ConnectionPresenceType.ERROR:
598
            return PresenceType.ERROR;
599
          case TelepathyGLib.ConnectionPresenceType.EXTENDED_AWAY:
600
            return PresenceType.EXTENDED_AWAY;
601
          case TelepathyGLib.ConnectionPresenceType.HIDDEN:
602
            return PresenceType.HIDDEN;
603
          case TelepathyGLib.ConnectionPresenceType.OFFLINE:
604
            return PresenceType.OFFLINE;
605
          case TelepathyGLib.ConnectionPresenceType.UNKNOWN:
606
            return PresenceType.UNKNOWN;
607
          case TelepathyGLib.ConnectionPresenceType.UNSET:
608
609
610
611
            return PresenceType.UNSET;
          default:
            return PresenceType.UNKNOWN;
        }
612
    }
613

614
  private void _contact_notify_avatar ()
615
    {
616
      var file = this.contact.avatar_file;
617
618
619
620
621
      Icon? icon = null;

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

622
623
624
625
626
      if (this._avatar == null || icon == null || !this._avatar.equal (icon))
        {
          this._avatar = (LoadableIcon) icon;
          this.notify_property ("avatar");
        }
627
    }
628
}