2 * Copyright (C) 2010 Collabora Ltd.
4 * This library is free software: you can redistribute it and/or modify
5 * it under the terms of the GNU Lesser General Public License as published by
6 * the Free Software Foundation, either version 2.1 of the License, or
7 * (at your option) any later version.
9 * This library is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU Lesser General Public License for more details.
14 * You should have received a copy of the GNU Lesser General Public License
15 * along with this library. If not, see <http://www.gnu.org/licenses/>.
18 * Philip Withnall <philip.withnall@collabora.co.uk>
26 public class Folks.Importers.Pidgin : Folks.Importer
28 private PersonaStore destination_store;
29 private uint persona_count = 0;
31 public override async uint import (PersonaStore destination_store,
32 string? source_filename) throws ImportError
34 this.destination_store = destination_store;
35 string filename = source_filename;
37 /* Default filename */
38 if (filename == null || filename.strip () == "")
40 filename = Path.build_filename (Environment.get_home_dir (),
41 ".purple", "blist.xml", null);
44 var file = File.new_for_path (filename);
45 if (!file.query_exists ())
47 /* Translators: the parameter is a filename. */
48 throw new ImportError.MALFORMED_INPUT (_("File %s does not exist."),
55 file_info = yield file.query_info_async (
56 FILE_ATTRIBUTE_ACCESS_CAN_READ, FileQueryInfoFlags.NONE,
61 throw new ImportError.MALFORMED_INPUT (
62 /* Translators: the first parameter is a filename, and the second
63 * is an error message. */
64 _("Failed to get information about file %s: %s"), filename,
68 if (!file_info.get_attribute_boolean (FILE_ATTRIBUTE_ACCESS_CAN_READ))
70 /* Translators: the parameter is a filename. */
71 throw new ImportError.MALFORMED_INPUT (_("File %s is not readable."),
75 Xml.Doc* xml_doc = Parser.parse_file (filename);
79 throw new ImportError.MALFORMED_INPUT (
80 /* Translators: the parameter is a filename. */
81 _("The Pidgin buddy list file '%s' could not be loaded."),
85 /* Check the root node */
86 Xml.Node *root_node = xml_doc->get_root_element ();
88 if (root_node == null || root_node->name != "purple" ||
89 root_node->get_prop ("version") != "1.0")
91 /* Free the document manually before throwing because the garbage
92 * collector can't work on pointers. */
94 throw new ImportError.MALFORMED_INPUT (
95 /* Translators: the parameter is a filename. */
96 _("The Pidgin buddy list file '%s' could not be loaded: the root element could not be found or was not recognised."),
100 /* Parse each <blist> child element */
101 for (Xml.Node *iter = root_node->children; iter != null;
104 if (iter->type != ElementType.ELEMENT_NODE || iter->name != "blist")
107 yield this.parse_blist (iter);
113 /* Translators: the first parameter is the number of buddies which were
114 * successfully imported, and the second is a filename. */
115 stdout.printf (_("Imported %u buddies from '%s'.\n"), this.persona_count,
118 /* Return the number of Personas we imported */
119 return this.persona_count;
122 private async void parse_blist (Xml.Node *blist_node)
124 for (Xml.Node *iter = blist_node->children; iter != null;
127 if (iter->type != ElementType.ELEMENT_NODE || iter->name != "group")
130 yield this.parse_group (iter);
134 private async void parse_group (Xml.Node *group_node)
136 string group_name = group_node->get_prop ("name");
138 for (Xml.Node *iter = group_node->children; iter != null;
141 if (iter->type != ElementType.ELEMENT_NODE || iter->name != "contact")
144 Persona persona = yield this.parse_contact (iter);
146 /* Skip the persona if creating them failed or if they don't support
148 if (persona == null || !(persona is Groupable))
153 Groupable groupable = (Groupable) persona;
154 yield groupable.change_group (group_name, true);
159 /* Translators: the first parameter is a persona identifier,
160 * and the second is an error message. */
161 _("Error changing group of Pidgin.Persona '%s': %s\n"),
162 persona.iid, e.message);
167 private async Persona? parse_contact (Xml.Node *contact_node)
170 HashTable<string, GenericArray<string>> im_addresses =
171 new HashTable<string, GenericArray<string>> (str_hash, str_equal);
172 string im_address_string = "";
174 /* Parse the <buddy> elements beneath <contact> */
175 for (Xml.Node *iter = contact_node->children; iter != null;
178 if (iter->type != ElementType.ELEMENT_NODE || iter->name != "buddy")
181 string blist_protocol = iter->get_prop ("proto");
182 if (blist_protocol == null)
186 this.blist_protocol_to_tp_protocol (blist_protocol);
187 if (tp_protocol == null)
190 /* Parse the <name> and <alias> elements beneath <buddy> */
191 for (Xml.Node *subiter = iter->children; subiter != null;
192 subiter = subiter->next)
194 if (subiter->type != ElementType.ELEMENT_NODE)
197 if (subiter->name == "alias")
198 alias = subiter->get_content ();
199 else if (subiter->name == "name")
201 /* The <name> element seems to give the contact ID, which
202 * we need to insert into the Persona's im-addresses property
203 * for the linking to work. */
204 string im_address = subiter->get_content ();
206 GenericArray<string> im_address_array =
207 im_addresses.lookup (tp_protocol);
208 if (im_address_array == null)
210 im_address_array = new GenericArray<string> ();
211 im_addresses.insert (tp_protocol, im_address_array);
214 im_address_array.add (im_address);
215 im_address_string += " %s\n".printf (im_address);
220 /* Don't bother if there's no alias and only one IM address */
221 if (im_addresses.size () < 2 &&
222 (alias == null || alias.strip () == "" ||
223 alias.strip () == im_address_string.strip ()))
226 /* Translators: the parameter is the buddy's IM address. */
227 _("Ignoring buddy with no alias and only one IM address:\n%s"),
232 /* Create or update the relevant Persona */
233 HashTable<string, Value?> details =
234 new HashTable<string, Value?> (str_hash, str_equal);
235 Value im_addresses_value = Value (typeof (HashTable));
236 im_addresses_value.set_boxed (im_addresses);
237 details.insert ("im-addresses", im_addresses_value);
243 yield this.destination_store.add_persona_from_details (details);
245 catch (PersonaStoreError e)
247 /* Translators: the first parameter is an alias, the second is a set
248 * of IM addresses each on a new line, and the third is an error
251 _("Failed to create new persona for buddy with alias '%s' and IM addresses:\n%s\nError: %s\n"),
252 alias, im_address_string, e.message);
256 /* Set the Persona's details */
257 if (alias != null && persona is Aliasable)
258 ((Aliasable) persona).alias = alias;
262 /* Translators: the first parameter is a persona identifier, the
263 * second is an alias for the persona, and the third is a set of IM
264 * addresses each on a new line. */
265 _("Created persona '%s' for buddy with alias '%s' and IM addresses:\n%s"),
266 persona.uid, alias, im_address_string);
267 this.persona_count++;
272 private string? blist_protocol_to_tp_protocol (string blist_protocol)
274 string tp_protocol = blist_protocol;
275 if (blist_protocol.has_prefix ("prpl-"))
276 tp_protocol = blist_protocol.substring (5);
278 /* Convert protocol names from Pidgin to Telepathy. Other protocol names
279 * should be OK now that we've taken off the "prpl-" prefix. See:
280 * http://telepathy.freedesktop.org/spec/Connection_Manager.html#Protocol
281 * and http://developer.pidgin.im/wiki/prpl_id. */
282 if (tp_protocol == "bonjour")
283 tp_protocol = "local-xmpp";
284 else if (tp_protocol == "novell")
285 tp_protocol = "groupwise";
286 else if (tp_protocol == "gg")
287 tp_protocol = "gadugadu";
288 else if (tp_protocol == "meanwhile")
289 tp_protocol = "sametime";
290 else if (tp_protocol == "simple")