Revert "Port folks to libgee 0.8."
[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 this._linkable_properties; }
59     }
60
61   /**
62    * {@inheritDoc}
63    *
64    * @since 0.6.0
65    */
66   public override string[] writeable_properties
67     {
68       get { return this._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           (GLib.HashFunc) ImFieldDetails.hash,
150           (GLib.EqualFunc) ImFieldDetails.equal);
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             (GLib.HashFunc) WebServiceFieldDetails.hash,
237             (GLib.EqualFunc) WebServiceFieldDetails.equal);
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 UNRELEASED
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 UNRELEASED
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 = this.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, ImFieldDetails.hash, (EqualFunc) ImFieldDetails.equal);
331       this._web_service_addresses =
332         new HashMultiMap<string, WebServiceFieldDetails> (
333             null, null,
334             (GLib.HashFunc) WebServiceFieldDetails.hash,
335             (GLib.EqualFunc) WebServiceFieldDetails.equal);
336       this._anti_links = new HashSet<string> ();
337       this._anti_links_ro = this._anti_links.read_only_view;
338
339       /* Load the IM addresses from the key file */
340       unowned KeyFile key_file = ((Kf.PersonaStore) this.store).get_key_file ();
341
342       try
343         {
344           var keys = key_file.get_keys (this.display_id);
345           foreach (unowned string key in keys)
346             {
347               /* Alias */
348               if (key == "__alias")
349                 {
350                   this._alias = key_file.get_string (this.display_id, key);
351
352                   if (this._alias == null)
353                     {
354                       this._alias = "";
355                     }
356
357                   debug ("    Loaded alias '%s'.", this._alias);
358                   continue;
359                 }
360
361               /* Anti-links. */
362               if (key == Kf.PersonaStore.anti_links_key_name)
363                 {
364                   var anti_link_array =
365                       key_file.get_string_list (this.display_id, key);
366
367                   if (anti_link_array != null)
368                     {
369                       foreach (var anti_link in anti_link_array)
370                         {
371                           this._anti_links.add (anti_link);
372                         }
373
374                       debug ("    Loaded %u anti-links.",
375                           anti_link_array.length);
376                       continue;
377                     }
378                 }
379
380               /* Web service addresses */
381               var decomposed_key = key.split(".", 2);
382               if (decomposed_key.length == 2 &&
383                   decomposed_key[0] == "web-service")
384                 {
385                   unowned string web_service = decomposed_key[1];
386                   var web_service_addresses = key_file.get_string_list (
387                       this.display_id, web_service);
388
389                   foreach (var web_service_address in web_service_addresses)
390                     {
391                       this._web_service_addresses.set (web_service,
392                           new WebServiceFieldDetails (web_service_address));
393                     }
394
395                   continue;
396                 }
397
398               /* IM addresses */
399               unowned string protocol = key;
400               var im_addresses = key_file.get_string_list (
401                   this.display_id, protocol);
402
403               foreach (var im_address in im_addresses)
404                 {
405                   string address;
406                   try
407                     {
408                       address = ImDetails.normalise_im_address (im_address,
409                           protocol);
410                     }
411                   catch (ImDetailsError e)
412                     {
413                       /* Warn of and ignore any invalid IM addresses */
414                       warning (e.message);
415                       continue;
416                     }
417
418                   var im_fd = new ImFieldDetails (address);
419                   this._im_addresses.set (protocol, im_fd);
420                 }
421             }
422         }
423       catch (KeyFileError e)
424         {
425           /* We get a GROUP_NOT_FOUND exception if we're creating a new
426            * Persona, since it doesn't yet exist in the key file. We shouldn't
427            * get any other exceptions, since we're iterating through a list of
428            * keys we've just retrieved. */
429           if (!(e is KeyFileError.GROUP_NOT_FOUND))
430             {
431               /* Translators: the parameter is an error message. */
432               warning (_("Couldn't load data from key file: %s"), e.message);
433             }
434         }
435     }
436
437   /**
438    * {@inheritDoc}
439    */
440   public override void linkable_property_to_links (string prop_name,
441       Folks.Persona.LinkablePropertyCallback callback)
442     {
443       if (prop_name == "im-addresses")
444         {
445           foreach (var protocol in this._im_addresses.get_keys ())
446             {
447               var im_addresses = this._im_addresses.get (protocol);
448
449               foreach (var im_fd in im_addresses)
450                   callback (protocol + ":" + im_fd.value);
451             }
452         }
453       else if (prop_name == "web-service-addresses")
454         {
455           foreach (var web_service in this.web_service_addresses.get_keys ())
456             {
457               var web_service_addresses =
458                   this._web_service_addresses.get (web_service);
459
460               foreach (var ws_fd in web_service_addresses)
461                   callback (web_service + ":" + ws_fd.value);
462             }
463         }
464       else
465         {
466           /* Chain up */
467           base.linkable_property_to_links (prop_name, callback);
468         }
469     }
470 }