955b2e0fbaa0c6931a5d554d44868b491196ca38
[platform/upstream/folks.git] / backends / telepathy / tpf-persona.vala
1 /*
2  * Copyright (C) 2010 Collabora Ltd.
3  *
4  * This library is free software: you can redistribute it and/or modify
5  * it under the terms of the GNU Lesser General Public License as published by
6  * the Free Software Foundation, either version 2.1 of the License, or
7  * (at your option) any later version.
8  *
9  * This library is distributed in the hope that it will be useful,
10  * but WITHOUT ANY WARRANTY; without even the implied warranty of
11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12  * GNU Lesser General Public License for more details.
13  *
14  * You should have received a copy of the GNU Lesser General Public License
15  * along with this library.  If not, see <http://www.gnu.org/licenses/>.
16  *
17  * Authors:
18  *       Travis Reitter <travis.reitter@collabora.co.uk>
19  */
20
21 using Gee;
22 using GLib;
23 using TelepathyGLib;
24 using Folks;
25
26 public errordomain Tpf.PersonaError
27 {
28   INVALID_ARGUMENT
29 }
30
31 /**
32  * A persona subclass which represents a single instant messaging contact from
33  * Telepathy.
34  */
35 public class Tpf.Persona : Folks.Persona,
36     Alias,
37     Avatar,
38     Favourite,
39     Groups,
40     IMable,
41     Presence
42 {
43   private HashTable<string, bool> _groups;
44   private bool _is_favourite;
45   private string _alias;
46   private HashTable<string, GenericArray<string>> _im_addresses;
47
48   /* Whether we've finished being constructed; this is used to prevent
49    * unnecessary trips to the Telepathy service to tell it about properties
50    * being set which are actually just being set from data it's just given us.
51    */
52   private bool is_constructed = false;
53
54   /**
55    * {@inheritDoc}
56    */
57   public File avatar { get; set; }
58
59   /**
60    * {@inheritDoc}
61    */
62   public Folks.PresenceType presence_type { get; private set; }
63
64   /**
65    * {@inheritDoc}
66    */
67   public string presence_message { get; private set; }
68
69   /**
70    * {@inheritDoc}
71    */
72   public string alias
73     {
74       get { return this._alias; }
75
76       set
77         {
78           if (this._alias == value)
79             return;
80
81           if (this.is_constructed)
82             ((Tpf.PersonaStore) this.store).change_alias (this, value);
83           this._alias = value;
84         }
85     }
86
87   /**
88    * {@inheritDoc}
89    */
90   public bool is_favourite
91     {
92       get { return this._is_favourite; }
93
94       set
95         {
96           if (this._is_favourite == value)
97             return;
98
99           if (this.is_constructed)
100             ((Tpf.PersonaStore) this.store).change_is_favourite (this, value);
101           this._is_favourite = value;
102         }
103     }
104
105   /**
106    * {@inheritDoc}
107    */
108   public HashTable<string, GenericArray<string>> im_addresses
109     {
110       get { return this._im_addresses; }
111       private set {}
112     }
113
114   /**
115    * {@inheritDoc}
116    */
117   public HashTable<string, bool> groups
118     {
119       get { return this._groups; }
120
121       set
122         {
123           value.foreach ((k, v) =>
124             {
125               var group = (string) k;
126               if (this._groups.lookup (group) == false)
127                 this._change_group (group, true);
128             });
129
130           this._groups.foreach ((k, v) =>
131             {
132               var group = (string) k;
133               if (value.lookup (group) == false)
134                 this._change_group (group, true);
135             });
136         }
137     }
138
139   /**
140    * {@inheritDoc}
141    */
142   public async void change_group (string group, bool is_member)
143     {
144       if (_change_group (group, is_member))
145         {
146           ((Tpf.PersonaStore) this.store).change_group_membership (this, group,
147             is_member);
148
149           this.group_changed (group, is_member);
150         }
151     }
152
153   private bool _change_group (string group, bool is_member)
154     {
155       bool changed = false;
156
157       if (is_member)
158         {
159           if (this._groups.lookup (group) != true)
160             {
161               this._groups.insert (group, true);
162               changed = true;
163             }
164         }
165       else
166         changed = this._groups.remove (group);
167
168       return changed;
169     }
170
171   /**
172    * The Telepathy contact represented by this persona.
173    */
174   public Contact contact { get; construct; }
175
176   /**
177    * Create a new persona.
178    *
179    * Create a new persona for the {@link PersonaStore} `store`, representing
180    * the Telepathy contact given by `contact`.
181    */
182   public Persona (Contact contact, PersonaStore store) throws Tpf.PersonaError
183     {
184       string[] linkable_properties = { "im-addresses" };
185
186       /* FIXME: There is the possibility of a crash in the error condition below
187        * due to bgo#604299, where the C self variable isn't initialised until we
188        * chain up to the Object constructor, below. */
189       unowned string id = contact.get_identifier ();
190       if (id == null || id == "")
191         throw new Tpf.PersonaError.INVALID_ARGUMENT ("contact has an " +
192             "invalid ID");
193
194       var account = account_for_connection (contact.get_connection ());
195       string uid = this.build_uid ("telepathy", account.get_protocol (), id);
196
197       var alias = contact.get_alias ();
198       var display_id = id;
199
200       /* If the alias is empty, fall back to the display ID */
201       if (alias == null || alias.strip () == "")
202         alias = display_id;
203
204       Object (alias: alias,
205               contact: contact,
206               display_id: display_id,
207               /* FIXME: This IID format should be moved out to the IMable
208                * interface along with the code in
209                * Kf.Persona.linkable_property_to_links(), but that depends on
210                * bgo#624842 being fixed. */
211               iid: account.get_protocol () + ":" + id,
212               uid: uid,
213               store: store,
214               linkable_properties: linkable_properties);
215
216       debug ("Creating new Tpf.Persona '%s' for service-specific UID '%s': %p",
217           uid, id, this);
218       this.is_constructed = true;
219
220       /* Set our single IM address */
221       GenericArray<string> im_address_array = new GenericArray<string> ();
222       im_address_array.add (id);
223
224       this._im_addresses =
225           new HashTable<string, GenericArray<string>> (str_hash, str_equal);
226       this._im_addresses.insert (account.get_protocol (), im_address_array);
227
228       /* Groups */
229       this._groups = new HashTable<string, bool> (str_hash, str_equal);
230
231       contact.notify["avatar-file"].connect ((s, p) =>
232         {
233           this.contact_notify_avatar ();
234         });
235       this.contact_notify_avatar ();
236
237       contact.notify["presence-message"].connect ((s, p) =>
238         {
239           this.contact_notify_presence_message ();
240         });
241       contact.notify["presence-type"].connect ((s, p) =>
242         {
243           this.contact_notify_presence_type ();
244         });
245       this.contact_notify_presence_message ();
246       this.contact_notify_presence_type ();
247
248       ((Tpf.PersonaStore) this.store).group_members_changed.connect (
249           (s, group, added, removed) =>
250             {
251               if (added.find (this) != null)
252                 this._change_group (group, true);
253
254               if (removed.find (this) != null)
255                 this._change_group (group, false);
256             });
257
258       ((Tpf.PersonaStore) this.store).group_removed.connect (
259           (s, group, error) =>
260             {
261               /* FIXME: Can't use
262                * !(error is TelepathyGLib.DBusError.OBJECT_REMOVED) because the
263                * GIR bindings don't annotate errors */
264               if (error != null &&
265                   (error.domain != TelepathyGLib.dbus_errors_quark () ||
266                    error.code != TelepathyGLib.DBusError.OBJECT_REMOVED))
267                 {
268                   debug ("Group invalidated: %s", error.message);
269                 }
270
271               this._change_group (group, false);
272             });
273     }
274
275   ~Persona ()
276     {
277       debug ("Destroying Tpf.Persona '%s': %p", this.uid, this);
278     }
279
280   private static Account? account_for_connection (Connection conn)
281     {
282       var manager = AccountManager.dup ();
283       GLib.List<unowned Account> accounts = manager.get_valid_accounts ();
284
285       Account account_found = null;
286       accounts.foreach ((l) =>
287         {
288           unowned Account account = (Account) l;
289           if (account.get_connection () == conn)
290             {
291               account_found = account;
292               return;
293             }
294         });
295
296       return account_found;
297     }
298
299   private void contact_notify_presence_message ()
300     {
301       this.presence_message = this.contact.get_presence_message ();
302     }
303
304   private void contact_notify_presence_type ()
305     {
306       this.presence_type = folks_presence_type_from_tp (
307           this.contact.get_presence_type ());
308     }
309
310   private static PresenceType folks_presence_type_from_tp (
311       TelepathyGLib.ConnectionPresenceType type)
312     {
313       switch (type)
314         {
315           case TelepathyGLib.ConnectionPresenceType.AVAILABLE:
316             return PresenceType.AVAILABLE;
317           case TelepathyGLib.ConnectionPresenceType.AWAY:
318             return PresenceType.AWAY;
319           case TelepathyGLib.ConnectionPresenceType.BUSY:
320             return PresenceType.BUSY;
321           case TelepathyGLib.ConnectionPresenceType.ERROR:
322             return PresenceType.ERROR;
323           case TelepathyGLib.ConnectionPresenceType.EXTENDED_AWAY:
324             return PresenceType.EXTENDED_AWAY;
325           case TelepathyGLib.ConnectionPresenceType.HIDDEN:
326             return PresenceType.HIDDEN;
327           case TelepathyGLib.ConnectionPresenceType.OFFLINE:
328             return PresenceType.OFFLINE;
329           case TelepathyGLib.ConnectionPresenceType.UNKNOWN:
330             return PresenceType.UNKNOWN;
331           case TelepathyGLib.ConnectionPresenceType.UNSET:
332             return PresenceType.UNSET;
333           default:
334             return PresenceType.UNKNOWN;
335         }
336     }
337
338   private void contact_notify_avatar ()
339     {
340       var file = this.contact.get_avatar_file ();
341       if (this.avatar != file)
342         this.avatar = file;
343     }
344 }