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