/*
* 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 .
*
* Authors:
* Philip Withnall
*/
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);
}
var file = File.new_for_path (filename);
if (!file.query_exists ())
{
/* Translators: the parameter is a filename. */
throw new ImportError.MALFORMED_INPUT (_("File %s does not exist."),
filename);
}
FileInfo file_info;
try
{
file_info = yield file.query_info_async (
FILE_ATTRIBUTE_ACCESS_CAN_READ, FileQueryInfoFlags.NONE,
Priority.DEFAULT);
}
catch (GLib.Error e)
{
throw new ImportError.MALFORMED_INPUT (
/* Translators: the first parameter is a filename, and the second
* is an error message. */
_("Failed to get information about file %s: %s"), filename,
e.message);
}
if (!file_info.get_attribute_boolean (FILE_ATTRIBUTE_ACCESS_CAN_READ))
{
/* Translators: the parameter is a filename. */
throw new ImportError.MALFORMED_INPUT (_("File %s is not readable."),
filename);
}
Xml.Doc* xml_doc = Parser.parse_file (filename);
if (xml_doc == null)
{
throw new ImportError.MALFORMED_INPUT (
/* Translators: the parameter is a filename. */
_("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 (
/* Translators: the parameter is a filename. */
_("The Pidgin buddy list file '%s' could not be loaded: the root element could not be found or was not recognised."),
filename);
}
/* Parse each 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;
/* Translators: the first parameter is the number of buddies which were
* successfully imported, and the second is a filename. */
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 GroupDetails))
continue;
try
{
GroupDetails group_details = (GroupDetails) persona;
yield group_details.change_group (group_name, true);
}
catch (GLib.Error e)
{
stderr.printf (
/* Translators: the first parameter is a persona identifier,
* and the second is an error message. */
_("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;
var im_addresses = new HashMultiMap ();
string im_address_string = "";
/* Parse the elements beneath */
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 and elements beneath */
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 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 ();
im_addresses.set (tp_protocol, 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 (
/* Translators: the parameter is the buddy's IM address. */
_("Ignoring buddy with no alias and only one IM address:\n%s"),
im_address_string);
return null;
}
/* Create or update the relevant Persona */
HashTable details =
new HashTable (str_hash, str_equal);
Value im_addresses_value = Value (typeof (MultiMap));
im_addresses_value.set_object (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)
{
/* Translators: the first parameter is an alias, the second is a set
* of IM addresses each on a new line, and the third is an error
* message. */
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 AliasDetails)
((AliasDetails) persona).alias = alias;
/* Print progress */
stdout.printf (
/* Translators: the first parameter is a persona identifier, the
* second is an alias for the persona, and the third is a set of IM
* addresses each on a new line. */
_("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;
}
}