--- /dev/null
+/*
+ * 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;
+ }
+}
--- /dev/null
+/*
+ * 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;
+}