2 * Copyright (C) 2010 Collabora Ltd.
3 * Copyright (C) 2013 Philip Withnall
5 * This library is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU Lesser General Public License as published by
7 * the Free Software Foundation, either version 2.1 of the License, or
8 * (at your option) any later version.
10 * This library is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU Lesser General Public License for more details.
15 * You should have received a copy of the GNU Lesser General Public License
16 * along with this library. If not, see <http://www.gnu.org/licenses/>.
19 * Philip Withnall <philip.withnall@collabora.co.uk>
27 public class Folks.Importers.Pidgin : Folks.Importer
29 private PersonaStore destination_store;
30 private uint persona_count = 0;
32 public override async uint import (PersonaStore destination_store,
33 string? source_filename) throws ImportError
35 this.destination_store = destination_store;
36 string filename = source_filename;
38 /* Default filename */
39 if (filename == null || filename.strip () == "")
41 filename = Path.build_filename (Environment.get_home_dir (),
42 ".purple", "blist.xml", null);
45 var file = File.new_for_path (filename);
46 if (!file.query_exists ())
48 /* Translators: the parameter is a filename. */
49 throw new ImportError.MALFORMED_INPUT (_("File %s does not exist."),
56 file_info = yield file.query_info_async (
57 FILE_ATTRIBUTE_ACCESS_CAN_READ, FileQueryInfoFlags.NONE,
62 throw new ImportError.MALFORMED_INPUT (
63 /* Translators: the first parameter is a filename, and the second
64 * is an error message. */
65 _("Failed to get information about file %s: %s"), filename,
69 if (!file_info.get_attribute_boolean (FILE_ATTRIBUTE_ACCESS_CAN_READ))
71 /* Translators: the parameter is a filename. */
72 throw new ImportError.MALFORMED_INPUT (_("File %s is not readable."),
76 Xml.Doc* xml_doc = Parser.parse_file (filename);
80 throw new ImportError.MALFORMED_INPUT (
81 /* Translators: the parameter is a filename. */
82 _("The Pidgin buddy list file '%s' could not be loaded."),
86 /* Check the root node */
87 Xml.Node *root_node = xml_doc->get_root_element ();
89 if (root_node == null || root_node->name != "purple" ||
90 root_node->get_prop ("version") != "1.0")
92 /* Free the document manually before throwing because the garbage
93 * collector can't work on pointers. */
95 throw new ImportError.MALFORMED_INPUT (
96 /* Translators: the parameter is a filename. */
97 _("The Pidgin buddy list file ‘%s’ could not be loaded: the root element could not be found or was not recognized."),
101 /* Parse each <blist> child element */
102 for (Xml.Node *iter = root_node->children; iter != null;
105 if (iter->type != ElementType.ELEMENT_NODE || iter->name != "blist")
108 yield this.parse_blist (iter);
115 /* Translators: the first parameter is the number of buddies which
116 * were successfully imported, and the second is a filename. */
117 ngettext ("Imported %u buddy from '%s'.",
118 "Imported %u buddies from '%s'.", this.persona_count) + "\n",
119 this.persona_count, filename);
121 /* Return the number of Personas we imported */
122 return this.persona_count;
125 private async void parse_blist (Xml.Node *blist_node)
127 for (Xml.Node *iter = blist_node->children; iter != null;
130 if (iter->type != ElementType.ELEMENT_NODE || iter->name != "group")
133 yield this.parse_group (iter);
137 private async void parse_group (Xml.Node *group_node)
139 string group_name = group_node->get_prop ("name");
141 for (Xml.Node *iter = group_node->children; iter != null;
144 if (iter->type != ElementType.ELEMENT_NODE || iter->name != "contact")
147 Persona persona = yield this.parse_contact (iter);
149 /* Skip the persona if creating them failed or if they don't support
151 if (persona == null || !(persona is GroupDetails))
156 GroupDetails group_details = (GroupDetails) persona;
157 yield group_details.change_group (group_name, true);
162 /* Translators: the first parameter is a persona identifier,
163 * and the second is an error message. */
164 _("Error changing group of contact ‘%s’: %s") + "\n",
165 persona.iid, e.message);
170 private async Persona? parse_contact (Xml.Node *contact_node)
173 var im_addresses = new HashMultiMap<string, ImFieldDetails> ();
174 string im_address_string = "";
176 /* Parse the <buddy> elements beneath <contact> */
177 for (Xml.Node *iter = contact_node->children; iter != null;
180 if (iter->type != ElementType.ELEMENT_NODE || iter->name != "buddy")
183 string blist_protocol = iter->get_prop ("proto");
184 if (blist_protocol == null)
188 this.blist_protocol_to_tp_protocol (blist_protocol);
189 if (tp_protocol == null)
192 /* Parse the <name> and <alias> elements beneath <buddy> */
193 for (Xml.Node *subiter = iter->children; subiter != null;
194 subiter = subiter->next)
196 if (subiter->type != ElementType.ELEMENT_NODE)
199 if (subiter->name == "alias")
200 alias = subiter->get_content ();
201 else if (subiter->name == "name")
203 /* The <name> element seems to give the contact ID, which
204 * we need to insert into the Persona's im-addresses property
205 * for the linking to work. */
206 string im_address = subiter->get_content ();
207 im_addresses.set (tp_protocol,
208 new ImFieldDetails (im_address));
209 im_address_string += " %s\n".printf (im_address);
214 /* Don't bother if there's no alias and only one IM address */
215 if (im_addresses.size < 2 &&
216 (alias == null || alias.strip () == "" ||
217 alias.strip () == im_address_string.strip ()))
220 /* Translators: the parameter is the buddy's IM address. */
221 _("Ignoring buddy with no alias and only one IM address:\n%s"),
226 /* Create or update the relevant Persona */
227 var details = new GLib.HashTable<string, Value?> (str_hash, str_equal);
228 Value im_addresses_value = Value (typeof (MultiMap));
229 im_addresses_value.set_object (im_addresses);
230 details.insert ("im-addresses", im_addresses_value);
236 yield this.destination_store.add_persona_from_details (details);
238 catch (PersonaStoreError e)
240 /* Translators: the first parameter is an alias, the second is a set
241 * of IM addresses each on a new line, and the third is an error
244 _("Failed to create new contact for buddy with alias ‘%s’ and IM addresses:\n%s\nError: %s\n"),
245 alias, im_address_string, e.message);
249 /* Set the Persona's details */
250 if (alias != null && persona is AliasDetails)
251 ((AliasDetails) persona).alias = alias;
255 /* Translators: the first parameter is a persona identifier, the
256 * second is an alias for the persona, and the third is a set of IM
257 * addresses each on a new line. */
258 _("Created contact ‘%s’ for buddy with alias ‘%s’ and IM addresses:\n%s"),
259 persona.uid, alias, im_address_string);
260 this.persona_count++;
265 private string? blist_protocol_to_tp_protocol (string blist_protocol)
267 string tp_protocol = blist_protocol;
268 if (blist_protocol.has_prefix ("prpl-"))
269 tp_protocol = blist_protocol.substring (5);
271 /* Convert protocol names from Pidgin to Telepathy. Other protocol names
272 * should be OK now that we've taken off the "prpl-" prefix. See:
273 * http://telepathy.freedesktop.org/spec/Connection_Manager.html#Protocol
274 * and http://developer.pidgin.im/wiki/prpl_id. */
275 if (tp_protocol == "bonjour")
276 tp_protocol = "local-xmpp";
277 else if (tp_protocol == "novell")
278 tp_protocol = "groupwise";
279 else if (tp_protocol == "gg")
280 tp_protocol = "gadugadu";
281 else if (tp_protocol == "meanwhile")
282 tp_protocol = "sametime";
283 else if (tp_protocol == "simple")