build fix: Only depend on Gee 0.8.3, since 0.8.4 isn't released yet
[platform/upstream/folks.git] / backends / key-file / kf-persona.vala
index a40dd0a..2d4b2eb 100644 (file)
  */
 
 using GLib;
+using Gee;
 using Folks;
 using Folks.Backends.Kf;
 
 /**
  * A persona subclass which represents a single persona from a simple key file.
+ *
+ * @since 0.1.13
  */
 public class Folks.Backends.Kf.Persona : Folks.Persona,
-    IMable
+    AliasDetails,
+    AntiLinkable,
+    ImDetails,
+    WebServiceDetails
 {
-  private unowned GLib.KeyFile key_file;
-  /* FIXME: As described in the IMable interface, we have to use
-   * GenericArray<string> here rather than just string[], as null-terminated
-   * arrays aren't supported as generic types. */
-  private HashTable<string, GenericArray<string>> _im_addresses;
+  private HashMultiMap<string, ImFieldDetails> _im_addresses;
+  private HashMultiMap<string, WebServiceFieldDetails> _web_service_addresses;
+  private string _alias = ""; /* must not be null */
+  private const string[] _linkable_properties =
+    {
+      "im-addresses",
+      "web-service-addresses"
+    };
+  private const string[] _writeable_properties =
+    {
+      "alias",
+      "im-addresses",
+      "web-service-addresses",
+      "anti-links"
+    };
+
+  /**
+   * {@inheritDoc}
+   */
+  public override string[] linkable_properties
+    {
+      get { return Kf.Persona._linkable_properties; }
+    }
+
+  /**
+   * {@inheritDoc}
+   *
+   * @since 0.6.0
+   */
+  public override string[] writeable_properties
+    {
+      get { return Kf.Persona._writeable_properties; }
+    }
+
+  /**
+   * {@inheritDoc}
+   *
+   * @since 0.1.15
+   */
+  [CCode (notify = false)]
+  public string alias
+    {
+      get { return this._alias; }
+      set { this.change_alias.begin (value); }
+    }
 
   /**
    * {@inheritDoc}
+   *
+   * @since 0.6.2
    */
-  public HashTable<string, GenericArray<string>> im_addresses
+  public async void change_alias (string alias) throws PropertyError
     {
-      get
-        { return this._im_addresses; }
+      /* Deal with badly-behaved callers. */
+      if (alias == null)
+        {
+          alias = "";
+        }
 
-      set
+      if (this._alias == alias)
         {
-          /* Remove the current IM addresses from the key file */
-          this._im_addresses.foreach ((k, v) =>
+          return;
+        }
+
+      debug ("Setting alias of Kf.Persona '%s' to '%s'.", this.uid, alias);
+
+      unowned KeyFile key_file = ((Kf.PersonaStore) this.store).get_key_file ();
+      key_file.set_string (this.display_id, "__alias", alias);
+      yield ((Kf.PersonaStore) this.store).save_key_file ();
+
+      this._alias = alias;
+      this.notify_property ("alias");
+    }
+
+  /**
+   * {@inheritDoc}
+   */
+  [CCode (notify = false)]
+  public MultiMap<string, ImFieldDetails> im_addresses
+    {
+      get { return this._im_addresses; }
+      set { this.change_im_addresses.begin (value); }
+    }
+
+  /**
+   * {@inheritDoc}
+   *
+   * @since 0.6.2
+   */
+  public async void change_im_addresses (
+      MultiMap<string, ImFieldDetails> im_addresses) throws PropertyError
+    {
+      unowned KeyFile key_file = ((Kf.PersonaStore) this.store).get_key_file ();
+
+      /* Remove the current IM addresses from the key file */
+      foreach (var protocol1 in this._im_addresses.get_keys ())
+        {
+          try
+            {
+              key_file.remove_key (this.display_id, protocol1);
+            }
+          catch (KeyFileError e1)
+            {
+              /* Ignore the error, since it's just a group or key not found
+               * error. */
+            }
+        }
+
+      /* Add the new IM addresses to the key file and build a normalised
+       * table of them to set as the new property value */
+      var new_im_addresses = new HashMultiMap<string, ImFieldDetails> (
+          null, null,
+          (Gee.HashDataFunc) AbstractFieldDetails<string>.hash_static,
+          (Gee.EqualDataFunc) AbstractFieldDetails<string>.equal_static);
+
+      foreach (var protocol2 in im_addresses.get_keys ())
+        {
+          var addresses = im_addresses.get (protocol2);
+          var normalised_addresses = new HashSet<string> ();
+
+          foreach (var im_fd in addresses)
             {
+              string normalised_address;
               try
                 {
-                  unowned string protocol = (string) k;
-                  this.key_file.remove_key (this.uid, protocol);
+                  normalised_address = ImDetails.normalise_im_address (
+                      im_fd.value, protocol2);
                 }
-              catch (KeyFileError e)
+               catch (ImDetailsError e2)
                 {
-                  /* Ignore the error, since it's just a group or key not found
-                   * error. */
+                  throw new PropertyError.INVALID_VALUE (
+                      /* Translators: this is an error message for if the user
+                       * provides an invalid IM address. The first parameter is
+                       * an IM address (e.g. “foo@jabber.org”), the second is
+                       * the name of a protocol (e.g. “jabber”) and the third is
+                       * an error message. */
+                      _("Invalid IM address ‘%s’ for protocol ‘%s’: %s"),
+                      im_fd.value, protocol2, e2.message);
                 }
-            });
 
-          this._im_addresses = value;
+              normalised_addresses.add (normalised_address);
+              var new_im_fd = new ImFieldDetails (normalised_address);
+              new_im_addresses.set (protocol2, new_im_fd);
+            }
+
+          string[] addrs = (string[]) normalised_addresses.to_array ();
+          addrs.length = normalised_addresses.size;
+
+          key_file.set_string_list (this.display_id, protocol2, addrs);
+        }
+
+      /* Get the PersonaStore to save the key file */
+      yield ((Kf.PersonaStore) this.store).save_key_file ();
+
+      this._im_addresses = new_im_addresses;
+      this.notify_property ("im-addresses");
+    }
+
+  /**
+   * {@inheritDoc}
+   */
+  [CCode (notify = false)]
+  public MultiMap<string, WebServiceFieldDetails> web_service_addresses
+    {
+      get { return this._web_service_addresses; }
+      set { this.change_web_service_addresses.begin (value); }
+    }
+
+  /**
+   * {@inheritDoc}
+   *
+   * @since 0.6.2
+   */
+  public async void change_web_service_addresses (
+      MultiMap<string, WebServiceFieldDetails> web_service_addresses)
+          throws PropertyError
+    {
+      unowned KeyFile key_file = ((Kf.PersonaStore) this.store).get_key_file ();
 
-          /* Add the new IM addresses to the key file */
-          this._im_addresses.foreach ((k, v) =>
+      /* Remove the current web service addresses from the key file */
+      foreach (var web_service1 in this._web_service_addresses.get_keys ())
+        {
+          try
+            {
+              key_file.remove_key (this.display_id,
+                  "web-service." + web_service1);
+            }
+          catch (KeyFileError e)
             {
-              unowned string protocol = (string) k;
-              unowned string[] addresses = (string[]) v;
-              this.key_file.set_string_list (this.uid, protocol, addresses);
-            });
+              /* Ignore the error, since it's just a group or key not found
+               * error. */
+            }
+        }
+
+      /* Add the new web service addresses to the key file and build a
+       * table of them to set as the new property value */
+      var new_web_service_addresses =
+        new HashMultiMap<string, WebServiceFieldDetails> (
+            null, null,
+            (Gee.HashDataFunc) AbstractFieldDetails<string>.hash_static,
+            (Gee.EqualDataFunc) AbstractFieldDetails<string>.equal_static);
+
+      foreach (var web_service2 in web_service_addresses.get_keys ())
+        {
+          var ws_fds = web_service_addresses.get (web_service2);
+
+          string[] addrs = new string[0];
+          foreach (var ws_fd1 in ws_fds)
+            addrs += ws_fd1.value;
+
+          key_file.set_string_list (this.display_id,
+              "web-service." + web_service2, addrs);
+
+          foreach (var ws_fd2 in ws_fds)
+            new_web_service_addresses.set (web_service2, ws_fd2);
+        }
+
+      /* Get the PersonaStore to save the key file */
+      yield ((Kf.PersonaStore) this.store).save_key_file ();
+
+      this._web_service_addresses = new_web_service_addresses;
+      this.notify_property ("web-service-addresses");
+    }
+
+  private HashSet<string> _anti_links;
+  private Set<string> _anti_links_ro;
 
-          /* Get the PersonaStore to save the key file */
-          ((Kf.PersonaStore) this.store).save_key_file.begin ();
+  /**
+   * {@inheritDoc}
+   *
+   * @since 0.7.3
+   */
+  [CCode (notify = false)]
+  public Set<string> anti_links
+    {
+      get { return this._anti_links_ro; }
+      set { this.change_anti_links.begin (value); }
+    }
+
+  /**
+   * {@inheritDoc}
+   *
+   * @since 0.7.3
+   */
+  public async void change_anti_links (Set<string> anti_links)
+      throws PropertyError
+    {
+      if (Folks.Internal.equal_sets<string> (anti_links, this.anti_links))
+        {
+          return;
         }
+
+      unowned KeyFile key_file = ((Kf.PersonaStore) this.store).get_key_file ();
+
+      /* Skip the persona's UID; don't allow reflexive anti-links. */
+      anti_links.remove (this.uid);
+
+      key_file.set_string_list (this.display_id,
+          Kf.PersonaStore.anti_links_key_name, anti_links.to_array ());
+
+      /* Get the PersonaStore to save the key file */
+      yield ((Kf.PersonaStore) this.store).save_key_file ();
+
+      /* Update the stored anti-links. */
+      this._anti_links.clear ();
+      this._anti_links.add_all (anti_links);
+      this.notify_property ("anti-links");
     }
 
   /**
    * Create a new persona.
    *
-   * Create a new persona for the {@link PersonaStore} `store`, representing
-   * the Persona given by the group `uid` in the key file `key_file`.
+   * Create a new persona for the {@link PersonaStore} ``store``, representing
+   * the Persona given by the group ``uid`` in the key file ``key_file``.
    */
-  public Persona (KeyFile key_file, string uid, Folks.PersonaStore store)
+  public Persona (string id, Folks.PersonaStore store)
     {
-      string iid = "key-file:" + uid;
-      string[] linkable_properties = { "im-addresses" };
+      var iid = store.id + ":" + id;
+      var uid = Folks.Persona.build_uid ("key-file", store.id, id);
 
-      Object (iid: iid,
+      Object (display_id: id,
+              iid: iid,
               uid: uid,
               store: store,
-              linkable_properties: linkable_properties);
+              is_user: false);
+    }
+
+  construct
+    {
+      debug ("Adding key-file Persona '%s' (IID '%s', group '%s')", this.uid,
+          this.iid, this.display_id);
 
-      this.key_file = key_file;
-      this._im_addresses = new HashTable<string, GenericArray<string>> (
-          str_hash, str_equal);
+      this._im_addresses = new HashMultiMap<string, ImFieldDetails> (
+          null, null,
+          (Gee.HashDataFunc) AbstractFieldDetails<string>.hash_static,
+          (Gee.EqualDataFunc) AbstractFieldDetails<string>.equal_static);
+      this._web_service_addresses =
+        new HashMultiMap<string, WebServiceFieldDetails> (
+          null, null,
+          (Gee.HashDataFunc) AbstractFieldDetails<string>.hash_static,
+          (Gee.EqualDataFunc) AbstractFieldDetails<string>.equal_static);
+      this._anti_links = new HashSet<string> ();
+      this._anti_links_ro = this._anti_links.read_only_view;
 
       /* Load the IM addresses from the key file */
+      unowned KeyFile key_file = ((Kf.PersonaStore) this.store).get_key_file ();
+
       try
         {
-          string[] keys = this.key_file.get_keys (uid);
-          foreach (string protocol in keys)
+          var keys = key_file.get_keys (this.display_id);
+          foreach (unowned string key in keys)
             {
-              string[] im_addresses = this.key_file.get_string_list (uid,
-                  protocol);
-
-              /* FIXME: We have to convert our nice efficient string[] to a
-               * GenericArray<string> because Vala doesn't like null-terminated
-               * arrays as generic types. */
-              GenericArray<string> im_address_array =
-                  new GenericArray<string> ();
-              foreach (string address in im_addresses)
-                im_address_array.add (address);
-
-              this._im_addresses.insert (protocol, im_address_array);
+              /* Alias */
+              if (key == "__alias")
+                {
+                  this._alias = key_file.get_string (this.display_id, key);
+
+                  if (this._alias == null)
+                    {
+                      this._alias = "";
+                    }
+
+                  debug ("    Loaded alias '%s'.", this._alias);
+                  continue;
+                }
+
+              /* Anti-links. */
+              if (key == Kf.PersonaStore.anti_links_key_name)
+                {
+                  var anti_link_array =
+                      key_file.get_string_list (this.display_id, key);
+
+                  if (anti_link_array != null)
+                    {
+                      foreach (var anti_link in anti_link_array)
+                        {
+                          this._anti_links.add (anti_link);
+                        }
+
+                      debug ("    Loaded %u anti-links.",
+                          anti_link_array.length);
+                      continue;
+                    }
+                }
+
+              /* Web service addresses */
+              var decomposed_key = key.split(".", 2);
+              if (decomposed_key.length == 2 &&
+                  decomposed_key[0] == "web-service")
+                {
+                  unowned string web_service = decomposed_key[1];
+                  var web_service_addresses = key_file.get_string_list (
+                      this.display_id, web_service);
+
+                  foreach (var web_service_address in web_service_addresses)
+                    {
+                      this._web_service_addresses.set (web_service,
+                          new WebServiceFieldDetails (web_service_address));
+                    }
+
+                  continue;
+                }
+
+              /* IM addresses */
+              unowned string protocol = key;
+              var im_addresses = key_file.get_string_list (
+                  this.display_id, protocol);
+
+              foreach (var im_address in im_addresses)
+                {
+                  string address;
+                  try
+                    {
+                      address = ImDetails.normalise_im_address (im_address,
+                          protocol);
+                    }
+                  catch (ImDetailsError e)
+                    {
+                      /* Warn of and ignore any invalid IM addresses */
+                      warning (e.message);
+                      continue;
+                    }
+
+                  var im_fd = new ImFieldDetails (address);
+                  this._im_addresses.set (protocol, im_fd);
+                }
             }
         }
       catch (KeyFileError e)
         {
-          /* This should never be reached, as we're listing the keys then
-           * iterating through the list. */
-          GLib.assert_not_reached ();
+          /* We get a GROUP_NOT_FOUND exception if we're creating a new
+           * Persona, since it doesn't yet exist in the key file. We shouldn't
+           * get any other exceptions, since we're iterating through a list of
+           * keys we've just retrieved. */
+          if (!(e is KeyFileError.GROUP_NOT_FOUND))
+            {
+              /* Translators: the parameter is an error message. */
+              warning (_("Couldn't load data from key file: %s"), e.message);
+            }
         }
     }
 
+  /**
+   * {@inheritDoc}
+   */
   public override void linkable_property_to_links (string prop_name,
       Folks.Persona.LinkablePropertyCallback callback)
     {
       if (prop_name == "im-addresses")
         {
-          this.im_addresses.foreach ((k, v) =>
+          foreach (var protocol in this._im_addresses.get_keys ())
             {
-              unowned string protocol = (string) k;
-              unowned GenericArray<string> im_addresses =
-                  (GenericArray<string>) v;
+              var im_addresses = this._im_addresses.get (protocol);
 
-              im_addresses.foreach ((v) =>
-                {
-                  unowned string address = (string) v;
-                  callback (protocol + ":" + address);
-                });
-            });
+              foreach (var im_fd in im_addresses)
+                  callback (protocol + ":" + im_fd.value);
+            }
+        }
+      else if (prop_name == "web-service-addresses")
+        {
+          foreach (var web_service in this.web_service_addresses.get_keys ())
+            {
+              var web_service_addresses =
+                  this._web_service_addresses.get (web_service);
+
+              foreach (var ws_fd in web_service_addresses)
+                  callback (web_service + ":" + ws_fd.value);
+            }
         }
       else
         {