2d4b2ebe44050a0eeea080dedc626b8cc387a4fb
[platform/upstream/folks.git] / backends / key-file / kf-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  *       Philip Withnall <philip.withnall@collabora.co.uk>
19  */
20
21 using GLib;
22 using Gee;
23 using Folks;
24 using Folks.Backends.Kf;
25
26 /**
27  * A persona subclass which represents a single persona from a simple key file.
28  *
29  * @since 0.1.13
30  */
31 public class Folks.Backends.Kf.Persona : Folks.Persona,
32     AliasDetails,
33     AntiLinkable,
34     ImDetails,
35     WebServiceDetails
36 {
37   private HashMultiMap<string, ImFieldDetails> _im_addresses;
38   private HashMultiMap<string, WebServiceFieldDetails> _web_service_addresses;
39   private string _alias = ""; /* must not be null */
40   private const string[] _linkable_properties =
41     {
42       "im-addresses",
43       "web-service-addresses"
44     };
45   private const string[] _writeable_properties =
46     {
47       "alias",
48       "im-addresses",
49       "web-service-addresses",
50       "anti-links"
51     };
52
53   /**
54    * {@inheritDoc}
55    */
56   public override string[] linkable_properties
57     {
58       get { return Kf.Persona._linkable_properties; }
59     }
60
61   /**
62    * {@inheritDoc}
63    *
64    * @since 0.6.0
65    */
66   public override string[] writeable_properties
67     {
68       get { return Kf.Persona._writeable_properties; }
69     }
70
71   /**
72    * {@inheritDoc}
73    *
74    * @since 0.1.15
75    */
76   [CCode (notify = false)]
77   public string alias
78     {
79       get { return this._alias; }
80       set { this.change_alias.begin (value); }
81     }
82
83   /**
84    * {@inheritDoc}
85    *
86    * @since 0.6.2
87    */
88   public async void change_alias (string alias) throws PropertyError
89     {
90       /* Deal with badly-behaved callers. */
91       if (alias == null)
92         {
93           alias = "";
94         }
95
96       if (this._alias == alias)
97         {
98           return;
99         }
100
101       debug ("Setting alias of Kf.Persona '%s' to '%s'.", this.uid, alias);
102
103       unowned KeyFile key_file = ((Kf.PersonaStore) this.store).get_key_file ();
104       key_file.set_string (this.display_id, "__alias", alias);
105       yield ((Kf.PersonaStore) this.store).save_key_file ();
106
107       this._alias = alias;
108       this.notify_property ("alias");
109     }
110
111   /**
112    * {@inheritDoc}
113    */
114   [CCode (notify = false)]
115   public MultiMap<string, ImFieldDetails> im_addresses
116     {
117       get { return this._im_addresses; }
118       set { this.change_im_addresses.begin (value); }
119     }
120
121   /**
122    * {@inheritDoc}
123    *
124    * @since 0.6.2
125    */
126   public async void change_im_addresses (
127       MultiMap<string, ImFieldDetails> im_addresses) throws PropertyError
128     {
129       unowned KeyFile key_file = ((Kf.PersonaStore) this.store).get_key_file ();
130
131       /* Remove the current IM addresses from the key file */
132       foreach (var protocol1 in this._im_addresses.get_keys ())
133         {
134           try
135             {
136               key_file.remove_key (this.display_id, protocol1);
137             }
138           catch (KeyFileError e1)
139             {
140               /* Ignore the error, since it's just a group or key not found
141                * error. */
142             }
143         }
144
145       /* Add the new IM addresses to the key file and build a normalised
146        * table of them to set as the new property value */
147       var new_im_addresses = new HashMultiMap<string, ImFieldDetails> (
148           null, null,
149           (Gee.HashDataFunc) AbstractFieldDetails<string>.hash_static,
150           (Gee.EqualDataFunc) AbstractFieldDetails<string>.equal_static);
151
152       foreach (var protocol2 in im_addresses.get_keys ())
153         {
154           var addresses = im_addresses.get (protocol2);
155           var normalised_addresses = new HashSet<string> ();
156
157           foreach (var im_fd in addresses)
158             {
159               string normalised_address;
160               try
161                 {
162                   normalised_address = ImDetails.normalise_im_address (
163                       im_fd.value, protocol2);
164                 }
165                catch (ImDetailsError e2)
166                 {
167                   throw new PropertyError.INVALID_VALUE (
168                       /* Translators: this is an error message for if the user
169                        * provides an invalid IM address. The first parameter is
170                        * an IM address (e.g. “foo@jabber.org”), the second is
171                        * the name of a protocol (e.g. “jabber”) and the third is
172                        * an error message. */
173                       _("Invalid IM address ‘%s’ for protocol ‘%s’: %s"),
174                       im_fd.value, protocol2, e2.message);
175                 }
176
177               normalised_addresses.add (normalised_address);
178               var new_im_fd = new ImFieldDetails (normalised_address);
179               new_im_addresses.set (protocol2, new_im_fd);
180             }
181
182           string[] addrs = (string[]) normalised_addresses.to_array ();
183           addrs.length = normalised_addresses.size;
184
185           key_file.set_string_list (this.display_id, protocol2, addrs);
186         }
187
188       /* Get the PersonaStore to save the key file */
189       yield ((Kf.PersonaStore) this.store).save_key_file ();
190
191       this._im_addresses = new_im_addresses;
192       this.notify_property ("im-addresses");
193     }
194
195   /**
196    * {@inheritDoc}
197    */
198   [CCode (notify = false)]
199   public MultiMap<string, WebServiceFieldDetails> web_service_addresses
200     {
201       get { return this._web_service_addresses; }
202       set { this.change_web_service_addresses.begin (value); }
203     }
204
205   /**
206    * {@inheritDoc}
207    *
208    * @since 0.6.2
209    */
210   public async void change_web_service_addresses (
211       MultiMap<string, WebServiceFieldDetails> web_service_addresses)
212           throws PropertyError
213     {
214       unowned KeyFile key_file = ((Kf.PersonaStore) this.store).get_key_file ();
215
216       /* Remove the current web service addresses from the key file */
217       foreach (var web_service1 in this._web_service_addresses.get_keys ())
218         {
219           try
220             {
221               key_file.remove_key (this.display_id,
222                   "web-service." + web_service1);
223             }
224           catch (KeyFileError e)
225             {
226               /* Ignore the error, since it's just a group or key not found
227                * error. */
228             }
229         }
230
231       /* Add the new web service addresses to the key file and build a
232        * table of them to set as the new property value */
233       var new_web_service_addresses =
234         new HashMultiMap<string, WebServiceFieldDetails> (
235             null, null,
236             (Gee.HashDataFunc) AbstractFieldDetails<string>.hash_static,
237             (Gee.EqualDataFunc) AbstractFieldDetails<string>.equal_static);
238
239       foreach (var web_service2 in web_service_addresses.get_keys ())
240         {
241           var ws_fds = web_service_addresses.get (web_service2);
242
243           string[] addrs = new string[0];
244           foreach (var ws_fd1 in ws_fds)
245             addrs += ws_fd1.value;
246
247           key_file.set_string_list (this.display_id,
248               "web-service." + web_service2, addrs);
249
250           foreach (var ws_fd2 in ws_fds)
251             new_web_service_addresses.set (web_service2, ws_fd2);
252         }
253
254       /* Get the PersonaStore to save the key file */
255       yield ((Kf.PersonaStore) this.store).save_key_file ();
256
257       this._web_service_addresses = new_web_service_addresses;
258       this.notify_property ("web-service-addresses");
259     }
260
261   private HashSet<string> _anti_links;
262   private Set<string> _anti_links_ro;
263
264   /**
265    * {@inheritDoc}
266    *
267    * @since 0.7.3
268    */
269   [CCode (notify = false)]
270   public Set<string> anti_links
271     {
272       get { return this._anti_links_ro; }
273       set { this.change_anti_links.begin (value); }
274     }
275
276   /**
277    * {@inheritDoc}
278    *
279    * @since 0.7.3
280    */
281   public async void change_anti_links (Set<string> anti_links)
282       throws PropertyError
283     {
284       if (Folks.Internal.equal_sets<string> (anti_links, this.anti_links))
285         {
286           return;
287         }
288
289       unowned KeyFile key_file = ((Kf.PersonaStore) this.store).get_key_file ();
290
291       /* Skip the persona's UID; don't allow reflexive anti-links. */
292       anti_links.remove (this.uid);
293
294       key_file.set_string_list (this.display_id,
295           Kf.PersonaStore.anti_links_key_name, anti_links.to_array ());
296
297       /* Get the PersonaStore to save the key file */
298       yield ((Kf.PersonaStore) this.store).save_key_file ();
299
300       /* Update the stored anti-links. */
301       this._anti_links.clear ();
302       this._anti_links.add_all (anti_links);
303       this.notify_property ("anti-links");
304     }
305
306   /**
307    * Create a new persona.
308    *
309    * Create a new persona for the {@link PersonaStore} ``store``, representing
310    * the Persona given by the group ``uid`` in the key file ``key_file``.
311    */
312   public Persona (string id, Folks.PersonaStore store)
313     {
314       var iid = store.id + ":" + id;
315       var uid = Folks.Persona.build_uid ("key-file", store.id, id);
316
317       Object (display_id: id,
318               iid: iid,
319               uid: uid,
320               store: store,
321               is_user: false);
322     }
323
324   construct
325     {
326       debug ("Adding key-file Persona '%s' (IID '%s', group '%s')", this.uid,
327           this.iid, this.display_id);
328
329       this._im_addresses = new HashMultiMap<string, ImFieldDetails> (
330           null, null,
331           (Gee.HashDataFunc) AbstractFieldDetails<string>.hash_static,
332           (Gee.EqualDataFunc) AbstractFieldDetails<string>.equal_static);
333       this._web_service_addresses =
334         new HashMultiMap<string, WebServiceFieldDetails> (
335           null, null,
336           (Gee.HashDataFunc) AbstractFieldDetails<string>.hash_static,
337           (Gee.EqualDataFunc) AbstractFieldDetails<string>.equal_static);
338       this._anti_links = new HashSet<string> ();
339       this._anti_links_ro = this._anti_links.read_only_view;
340
341       /* Load the IM addresses from the key file */
342       unowned KeyFile key_file = ((Kf.PersonaStore) this.store).get_key_file ();
343
344       try
345         {
346           var keys = key_file.get_keys (this.display_id);
347           foreach (unowned string key in keys)
348             {
349               /* Alias */
350               if (key == "__alias")
351                 {
352                   this._alias = key_file.get_string (this.display_id, key);
353
354                   if (this._alias == null)
355                     {
356                       this._alias = "";
357                     }
358
359                   debug ("    Loaded alias '%s'.", this._alias);
360                   continue;
361                 }
362
363               /* Anti-links. */
364               if (key == Kf.PersonaStore.anti_links_key_name)
365                 {
366                   var anti_link_array =
367                       key_file.get_string_list (this.display_id, key);
368
369                   if (anti_link_array != null)
370                     {
371                       foreach (var anti_link in anti_link_array)
372                         {
373                           this._anti_links.add (anti_link);
374                         }
375
376                       debug ("    Loaded %u anti-links.",
377                           anti_link_array.length);
378                       continue;
379                     }
380                 }
381
382               /* Web service addresses */
383               var decomposed_key = key.split(".", 2);
384               if (decomposed_key.length == 2 &&
385                   decomposed_key[0] == "web-service")
386                 {
387                   unowned string web_service = decomposed_key[1];
388                   var web_service_addresses = key_file.get_string_list (
389                       this.display_id, web_service);
390
391                   foreach (var web_service_address in web_service_addresses)
392                     {
393                       this._web_service_addresses.set (web_service,
394                           new WebServiceFieldDetails (web_service_address));
395                     }
396
397                   continue;
398                 }
399
400               /* IM addresses */
401               unowned string protocol = key;
402               var im_addresses = key_file.get_string_list (
403                   this.display_id, protocol);
404
405               foreach (var im_address in im_addresses)
406                 {
407                   string address;
408                   try
409                     {
410                       address = ImDetails.normalise_im_address (im_address,
411                           protocol);
412                     }
413                   catch (ImDetailsError e)
414                     {
415                       /* Warn of and ignore any invalid IM addresses */
416                       warning (e.message);
417                       continue;
418                     }
419
420                   var im_fd = new ImFieldDetails (address);
421                   this._im_addresses.set (protocol, im_fd);
422                 }
423             }
424         }
425       catch (KeyFileError e)
426         {
427           /* We get a GROUP_NOT_FOUND exception if we're creating a new
428            * Persona, since it doesn't yet exist in the key file. We shouldn't
429            * get any other exceptions, since we're iterating through a list of
430            * keys we've just retrieved. */
431           if (!(e is KeyFileError.GROUP_NOT_FOUND))
432             {
433               /* Translators: the parameter is an error message. */
434               warning (_("Couldn't load data from key file: %s"), e.message);
435             }
436         }
437     }
438
439   /**
440    * {@inheritDoc}
441    */
442   public override void linkable_property_to_links (string prop_name,
443       Folks.Persona.LinkablePropertyCallback callback)
444     {
445       if (prop_name == "im-addresses")
446         {
447           foreach (var protocol in this._im_addresses.get_keys ())
448             {
449               var im_addresses = this._im_addresses.get (protocol);
450
451               foreach (var im_fd in im_addresses)
452                   callback (protocol + ":" + im_fd.value);
453             }
454         }
455       else if (prop_name == "web-service-addresses")
456         {
457           foreach (var web_service in this.web_service_addresses.get_keys ())
458             {
459               var web_service_addresses =
460                   this._web_service_addresses.get (web_service);
461
462               foreach (var ws_fd in web_service_addresses)
463                   callback (web_service + ":" + ws_fd.value);
464             }
465         }
466       else
467         {
468           /* Chain up */
469           base.linkable_property_to_links (prop_name, callback);
470         }
471     }
472 }