[folks] Bug 629084 — Add a folks-import tool



commit e41c4a953bf36bd13ebabb0fb78bd1389b17ef56
Author: Philip Withnall <philip withnall collabora co uk>
Date:   Wed Sep 8 19:36:04 2010 +0100

    Bug 629084 â?? Add a folks-import tool
    
    Add a folks-import tool which allows importing of Pidgin meta-contact
    information to libfolks' key file. Closes: bgo#629084

 Makefile.am              |    1 +
 configure.ac             |    9 ++
 folks/backend-store.vala |    2 +-
 tools/Makefile.am        |   34 +++++++
 tools/import-pidgin.vala |  247 ++++++++++++++++++++++++++++++++++++++++++++++
 tools/import.vala        |  189 +++++++++++++++++++++++++++++++++++
 6 files changed, 481 insertions(+), 1 deletions(-)
---
diff --git a/Makefile.am b/Makefile.am
index 5fcd882..5f0a32a 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -4,6 +4,7 @@ SUBDIRS = \
 	folks \
 	backends \
 	tests \
+	tools \
 	$(NULL)
 
 if ENABLE_DOCS
diff --git a/configure.ac b/configure.ac
index c890fd6..ffb05ab 100644
--- a/configure.ac
+++ b/configure.ac
@@ -127,6 +127,13 @@ PKG_CHECK_MODULES([TP_GLIB_FOR_TESTS],
 AM_CONDITIONAL([HAVE_TP_GLIB_FOR_TESTS], [$have_tp_glib_for_tests])
 
 # -----------------------------------------------------------
+# Tools
+# -----------------------------------------------------------
+
+PKG_CHECK_MODULES(LIBXML, libxml-2.0, [have_libxml=yes], [have_libxml=no])
+AM_CONDITIONAL([HAVE_LIBXML], [test "$have_libxml" = "yes"])
+
+# -----------------------------------------------------------
 # Documentation
 # -----------------------------------------------------------
 
@@ -212,6 +219,7 @@ AC_CONFIG_FILES([
     tests/lib/telepathy/contactlist/Makefile
     tests/lib/telepathy/contactlist/session.conf
     tests/tools/Makefile
+    tools/Makefile
 ])
 
 AC_OUTPUT
@@ -225,6 +233,7 @@ Configure summary:
         Bugreporting URL............:  ${PACKAGE_BUGREPORT}
         Documentation...............:  ${enable_docs}
         Tests.......................:  ${have_tp_glib_for_tests}
+        Import tool.................:  ${have_libxml}
 
 
 "
diff --git a/folks/backend-store.vala b/folks/backend-store.vala
index 86d6607..b6aff32 100644
--- a/folks/backend-store.vala
+++ b/folks/backend-store.vala
@@ -172,7 +172,7 @@ public class Folks.BackendStore : Object {
 
           if (file_type == FileType.DIRECTORY)
             {
-              this.load_modules_from_dir.begin (file);
+              yield this.load_modules_from_dir (file);
             }
           else if (mime == "application/x-sharedlib" && !is_symlink)
             {
diff --git a/tools/Makefile.am b/tools/Makefile.am
new file mode 100644
index 0000000..1a0e961
--- /dev/null
+++ b/tools/Makefile.am
@@ -0,0 +1,34 @@
+if HAVE_LIBXML
+bin_PROGRAMS = folks-import
+endif
+
+VALAFLAGS = \
+	--vapidir=$(top_builddir)/folks \
+	--pkg=gee-1.0 \
+	--pkg=libxml-2.0 \
+	--pkg=folks \
+	$(NULL)
+
+folks_import_SOURCES = \
+	import.vala \
+	import-pidgin.vala \
+	$(NULL)
+folks_import_CFLAGS = \
+	$(GLIB_CFLAGS) \
+	$(GEE_CFLAGS) \
+	$(LIBXML_CFLAGS) \
+	-I$(top_srcdir)/folks \
+	$(NULL)
+folks_import_LDADD = \
+	$(GLIB_LIBS) \
+	$(GEE_LIBS) \
+	$(LIBXML_LIBS) \
+	$(top_builddir)/folks/libfolks.la \
+	$(NULL)
+
+GITIGNOREFILES = \
+	folks_import_vala.stamp \
+	$(folks_import_SOURCES:.vala=.c) \
+	$(NULL)
+
+-include $(top_srcdir)/git.mk
diff --git a/tools/import-pidgin.vala b/tools/import-pidgin.vala
new file mode 100644
index 0000000..22790a4
--- /dev/null
+++ b/tools/import-pidgin.vala
@@ -0,0 +1,247 @@
+/*
+ * Copyright (C) 2010 Collabora Ltd.
+ *
+ * This library is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this library.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * Authors:
+ *       Philip Withnall <philip withnall collabora co uk>
+ */
+
+using GLib;
+using Gee;
+using Xml;
+using Folks;
+
+public class Folks.Importers.Pidgin : Folks.Importer
+{
+  private PersonaStore destination_store;
+  private uint persona_count = 0;
+
+  public override async uint import (PersonaStore destination_store,
+      string? source_filename) throws ImportError
+    {
+      this.destination_store = destination_store;
+      string filename = source_filename;
+
+      /* Default filename */
+      if (filename == null || filename.strip () == "")
+        {
+          filename = Path.build_filename (Environment.get_home_dir (),
+              ".purple", "blist.xml", null);
+        }
+
+      Xml.Doc* xml_doc = Parser.parse_file (filename);
+
+      if (xml_doc == null)
+        {
+          throw new ImportError.MALFORMED_INPUT ("The Pidgin buddy list file " +
+              "'%s' could not be loaded.", filename);
+        }
+
+      /* Check the root node */
+      Xml.Node *root_node = xml_doc->get_root_element ();
+
+      if (root_node == null || root_node->name != "purple" ||
+          root_node->get_prop ("version") != "1.0")
+        {
+          /* Free the document manually before throwing because the garbage
+           * collector can't work on pointers. */
+          delete xml_doc;
+          throw new ImportError.MALFORMED_INPUT ("The Pidgin buddy list file " +
+              "'%s' could not be loaded: the root element could not be found " +
+              "or was not recognised.", filename);
+        }
+
+      /* Parse each <blist> child element */
+      for (Xml.Node *iter = root_node->children; iter != null;
+          iter = iter->next)
+        {
+          if (iter->type != ElementType.ELEMENT_NODE || iter->name != "blist")
+            continue;
+
+          yield this.parse_blist (iter);
+        }
+
+      /* Tidy up */
+      delete xml_doc;
+
+      stdout.printf ("Imported %u buddies from '%s'.\n", this.persona_count,
+          filename);
+
+      /* Return the number of Personas we imported */
+      return this.persona_count;
+    }
+
+  private async void parse_blist (Xml.Node *blist_node)
+    {
+      for (Xml.Node *iter = blist_node->children; iter != null;
+          iter = iter->next)
+        {
+          if (iter->type != ElementType.ELEMENT_NODE || iter->name != "group")
+            continue;
+
+          yield this.parse_group (iter);
+        }
+    }
+
+  private async void parse_group (Xml.Node *group_node)
+    {
+      string group_name = group_node->get_prop ("name");
+
+      for (Xml.Node *iter = group_node->children; iter != null;
+          iter = iter->next)
+        {
+          if (iter->type != ElementType.ELEMENT_NODE || iter->name != "contact")
+            continue;
+
+          Persona persona = yield this.parse_contact (iter);
+
+          /* Skip the persona if creating them failed or if they don't support
+           * groups. */
+          if (persona == null || !(persona is Groups))
+            continue;
+
+          try
+            {
+              Groups groupable = (Groups) persona;
+              yield groupable.change_group (group_name, true);
+            }
+          catch (GLib.Error e)
+            {
+              stderr.printf ("Error changing group of Pidgin.Persona " +
+                  "'%s': %s\n", persona.iid, e.message);
+            }
+        }
+    }
+
+  private async Persona? parse_contact (Xml.Node *contact_node)
+    {
+      string alias = null;
+      HashTable<string, GenericArray<string>> im_addresses =
+          new HashTable<string, GenericArray<string>> (str_hash, str_equal);
+      string im_address_string = "";
+
+      /* Parse the <buddy> elements beneath <contact> */
+      for (Xml.Node *iter = contact_node->children; iter != null;
+          iter = iter->next)
+        {
+          if (iter->type != ElementType.ELEMENT_NODE || iter->name != "buddy")
+            continue;
+
+          string blist_protocol = iter->get_prop ("proto");
+          if (blist_protocol == null)
+            continue;
+
+          string tp_protocol =
+              this.blist_protocol_to_tp_protocol (blist_protocol);
+          if (tp_protocol == null)
+            continue;
+
+          /* Parse the <name> and <alias> elements beneath <buddy> */
+          for (Xml.Node *subiter = iter->children; subiter != null;
+              subiter = subiter->next)
+            {
+              if (subiter->type != ElementType.ELEMENT_NODE)
+                continue;
+
+              if (subiter->name == "alias")
+                alias = subiter->get_content ();
+              else if (subiter->name == "name")
+                {
+                  /* The <name> element seems to give the contact ID, which
+                   * we need to insert into the Persona's im-addresses property
+                   * for the linking to work. */
+                  string im_address = subiter->get_content ();
+
+                  GenericArray<string> im_address_array =
+                      im_addresses.lookup (tp_protocol);
+                  if (im_address_array == null)
+                    {
+                      im_address_array = new GenericArray<string> ();
+                      im_addresses.insert (tp_protocol, im_address_array);
+                    }
+
+                  im_address_array.add (im_address);
+                  im_address_string += "    %s\n".printf (im_address);
+                }
+            }
+        }
+
+      /* Don't bother if there's no alias and only one IM address */
+      if (im_addresses.size () < 2 &&
+          (alias == null || alias.strip () == "" ||
+           alias.strip () == im_address_string.strip ()))
+        {
+          stdout.printf ("Ignoring buddy with no alias and only one IM " +
+              "address:\n%s", im_address_string);
+          return null;
+        }
+
+      /* Create or update the relevant Persona */
+      HashTable<string, Value?> details =
+          new HashTable<string, Value?> (str_hash, str_equal);
+      Value im_addresses_value = Value (typeof (HashTable));
+      im_addresses_value.set_boxed (im_addresses);
+      details.insert ("im-addresses", im_addresses_value);
+
+      Persona persona;
+      try
+        {
+          persona =
+              yield this.destination_store.add_persona_from_details (details);
+        }
+      catch (PersonaStoreError e)
+        {
+          stderr.printf ("Failed to create new persona for buddy with alias " +
+              "'%s' and IM addresses:\n%s\nError: %s\n", alias,
+              im_address_string, e.message);
+          return null;
+        }
+
+      /* Set the Persona's details */
+      if (alias != null && persona is Alias)
+        ((Alias) persona).alias = alias;
+
+      /* Print progress */
+      stdout.printf ("Created persona '%s' for buddy with alias '%s' and IM " +
+          "addresses:\n%s", persona.uid, alias, im_address_string);
+      this.persona_count++;
+
+      return persona;
+    }
+
+  private string? blist_protocol_to_tp_protocol (string blist_protocol)
+    {
+      string tp_protocol = blist_protocol;
+      if (blist_protocol.has_prefix ("prpl-"))
+        tp_protocol = blist_protocol.substring (5);
+
+      /* Convert protocol names from Pidgin to Telepathy. Other protocol names
+       * should be OK now that we've taken off the "prpl-" prefix. See:
+       * http://telepathy.freedesktop.org/spec/Connection_Manager.html#Protocol
+       * and http://developer.pidgin.im/wiki/prpl_id. */
+      if (tp_protocol == "bonjour")
+        tp_protocol = "local-xmpp";
+      else if (tp_protocol == "novell")
+        tp_protocol = "groupwise";
+      else if (tp_protocol == "gg")
+        tp_protocol = "gadugadu";
+      else if (tp_protocol == "meanwhile")
+        tp_protocol = "sametime";
+      else if (tp_protocol == "simple")
+        tp_protocol = "sip";
+
+      return tp_protocol;
+    }
+}
diff --git a/tools/import.vala b/tools/import.vala
new file mode 100644
index 0000000..aca2a97
--- /dev/null
+++ b/tools/import.vala
@@ -0,0 +1,189 @@
+/*
+ * Copyright (C) 2010 Collabora Ltd.
+ *
+ * This library is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this library.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * Authors:
+ *       Philip Withnall <philip withnall collabora co uk>
+ */
+
+using GLib;
+using Gee;
+using Xml;
+using Folks;
+
+/*
+ * Command line application to import meta-contact information from various
+ * places into libfolks' key file backend.
+ *
+ * Used as follows:
+ *   folks-import [--source=pidgin] [--source-filename=~/.purple/blist.xml]
+ */
+
+public class Folks.ImportTool : Object
+{
+  private static string source;
+  private static string source_filename;
+
+  private static const OptionEntry[] options =
+    {
+      { "source", 's', 0, OptionArg.STRING, ref ImportTool.source,
+          "Source backend name (default: 'pidgin')", "name" },
+      { "source-filename", 0, 0, OptionArg.FILENAME,
+          ref ImportTool.source_filename,
+          "Source filename (default: specific to source backend)", null },
+      { null }
+    };
+
+  public static int main (string[] args)
+    {
+      OptionContext context = new OptionContext ("â?? import meta-contact " +
+          "information to libfolks");
+      context.add_main_entries (ImportTool.options, "folks");
+
+      try
+        {
+          context.parse (ref args);
+        }
+      catch (OptionError e)
+        {
+          stderr.printf ("Couldn't parse command line options: %s\n",
+              e.message);
+          return 1;
+        }
+
+      /* We only support importing from Pidgin at the moment */
+      if (source == null || source.strip () == "")
+        source = "pidgin";
+
+      /* FIXME: We need to create this, even though we don't use it, to prevent
+       * debug message spew, as its constructor initialises the log handling.
+       * bgo#629096 */
+      IndividualAggregator aggregator = new IndividualAggregator ();
+      aggregator = null;
+
+      /* Create a main loop and start importing */
+      MainLoop main_loop = new MainLoop ();
+
+      bool success = false;
+      ImportTool.import.begin ((o, r) =>
+        {
+          success = ImportTool.import.end (r);
+          main_loop.quit ();
+        });
+
+      main_loop.run ();
+
+      return success ? 0 : 1;
+    }
+
+  private static async bool import ()
+    {
+      BackendStore backend_store = new BackendStore ();
+
+      try
+        {
+          yield backend_store.load_backends ();
+        }
+      catch (GLib.Error e1)
+        {
+          stderr.printf ("Couldn't load the backends: %s\n", e1.message);
+          return false;
+        }
+
+      /* Get the key-file backend */
+      Backend kf_backend = backend_store.get_backend_by_name ("key-file");
+
+      if (kf_backend == null)
+        {
+          stderr.printf ("Couldn't load the 'key-file' backend.\n");
+          return false;
+        }
+
+      try
+        {
+          yield kf_backend.prepare ();
+        }
+      catch (GLib.Error e2)
+        {
+          stderr.printf ("Couldn't prepare the 'key-file' backend: %s\n",
+              e2.message);
+          return false;
+        }
+
+      /* Get its only PersonaStore */
+      PersonaStore destination_store;
+      GLib.List<unowned PersonaStore> stores =
+          kf_backend.persona_stores.get_values ();
+
+      if (stores == null)
+        {
+          stderr.printf ("Couldn't load the 'key-file' backend's persona " +
+              "store.\n");
+          return false;
+        }
+
+      try
+        {
+          destination_store = stores.data;
+          yield destination_store.prepare ();
+        }
+      catch (GLib.Error e3)
+        {
+          stderr.printf ("Couldn't prepare the 'key-file' backend's persona " +
+              "store: %s\n", e3.message);
+          return false;
+        }
+
+      if (source == "pidgin")
+        {
+          Importer importer = new Importers.Pidgin ();
+
+          try
+            {
+              /* Import! */
+              yield importer.import (destination_store,
+                  ImportTool.source_filename);
+            }
+          catch (ImportError e)
+            {
+              stderr.printf ("Error: %s\n", e.message);
+              return false;
+            }
+
+          /* Wait for the PersonaStore to finish writing its changes to disk */
+          yield destination_store.flush ();
+
+          return true;
+        }
+      else
+        {
+          stderr.printf ("Unrecognised source backend name '%s'. " +
+              "'pidgin' is currently the only supported source backend.\n",
+              source);
+          return false;
+        }
+    }
+}
+
+public errordomain Folks.ImportError
+{
+  MALFORMED_INPUT,
+}
+
+public abstract class Folks.Importer : Object
+{
+  public abstract async uint import (PersonaStore destination_store,
+      string? source_filename) throws ImportError;
+}



[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]