eds: Clarify mapping between properties and vCard fields in EDS
[platform/upstream/folks.git] / tools / import-pidgin.vala
1 /*
2  * Copyright (C) 2010 Collabora Ltd.
3  * Copyright (C) 2013 Philip Withnall
4  *
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.
9  *
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.
14  *
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/>.
17  *
18  * Authors:
19  *       Philip Withnall <philip.withnall@collabora.co.uk>
20  */
21
22 using GLib;
23 using Gee;
24 using Xml;
25 using Folks;
26
27 public class Folks.Importers.Pidgin : Folks.Importer
28 {
29   private PersonaStore destination_store;
30   private uint persona_count = 0;
31
32   public override async uint import (PersonaStore destination_store,
33       string? source_filename) throws ImportError
34     {
35       this.destination_store = destination_store;
36       string filename = source_filename;
37
38       /* Default filename */
39       if (filename == null || filename.strip () == "")
40         {
41           filename = Path.build_filename (Environment.get_home_dir (),
42               ".purple", "blist.xml", null);
43         }
44
45       var file = File.new_for_path (filename);
46       if (!file.query_exists ())
47         {
48           /* Translators: the parameter is a filename. */
49           throw new ImportError.MALFORMED_INPUT (_("File %s does not exist."),
50               filename);
51         }
52
53       FileInfo file_info;
54       try
55         {
56           file_info = yield file.query_info_async (
57               FILE_ATTRIBUTE_ACCESS_CAN_READ, FileQueryInfoFlags.NONE,
58               Priority.DEFAULT);
59         }
60       catch (GLib.Error e)
61         {
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,
66               e.message);
67         }
68
69       if (!file_info.get_attribute_boolean (FILE_ATTRIBUTE_ACCESS_CAN_READ))
70         {
71           /* Translators: the parameter is a filename. */
72           throw new ImportError.MALFORMED_INPUT (_("File %s is not readable."),
73               filename);
74         }
75
76       Xml.Doc* xml_doc = Parser.parse_file (filename);
77
78       if (xml_doc == null)
79         {
80           throw new ImportError.MALFORMED_INPUT (
81               /* Translators: the parameter is a filename. */
82               _("The Pidgin buddy list file '%s' could not be loaded."),
83               filename);
84         }
85
86       /* Check the root node */
87       Xml.Node *root_node = xml_doc->get_root_element ();
88
89       if (root_node == null || root_node->name != "purple" ||
90           root_node->get_prop ("version") != "1.0")
91         {
92           /* Free the document manually before throwing because the garbage
93            * collector can't work on pointers. */
94           delete xml_doc;
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."),
98               filename);
99         }
100
101       /* Parse each <blist> child element */
102       for (Xml.Node *iter = root_node->children; iter != null;
103           iter = iter->next)
104         {
105           if (iter->type != ElementType.ELEMENT_NODE || iter->name != "blist")
106             continue;
107
108           yield this.parse_blist (iter);
109         }
110
111       /* Tidy up */
112       delete xml_doc;
113
114       stdout.printf (
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);
120
121       /* Return the number of Personas we imported */
122       return this.persona_count;
123     }
124
125   private async void parse_blist (Xml.Node *blist_node)
126     {
127       for (Xml.Node *iter = blist_node->children; iter != null;
128           iter = iter->next)
129         {
130           if (iter->type != ElementType.ELEMENT_NODE || iter->name != "group")
131             continue;
132
133           yield this.parse_group (iter);
134         }
135     }
136
137   private async void parse_group (Xml.Node *group_node)
138     {
139       string group_name = group_node->get_prop ("name");
140
141       for (Xml.Node *iter = group_node->children; iter != null;
142           iter = iter->next)
143         {
144           if (iter->type != ElementType.ELEMENT_NODE || iter->name != "contact")
145             continue;
146
147           Persona persona = yield this.parse_contact (iter);
148
149           /* Skip the persona if creating them failed or if they don't support
150            * groups. */
151           if (persona == null || !(persona is GroupDetails))
152             continue;
153
154           try
155             {
156               GroupDetails group_details = (GroupDetails) persona;
157               yield group_details.change_group (group_name, true);
158             }
159           catch (GLib.Error e)
160             {
161               stderr.printf (
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);
166             }
167         }
168     }
169
170   private async Persona? parse_contact (Xml.Node *contact_node)
171     {
172       string alias = null;
173       var im_addresses = new HashMultiMap<string, ImFieldDetails> ();
174       string im_address_string = "";
175
176       /* Parse the <buddy> elements beneath <contact> */
177       for (Xml.Node *iter = contact_node->children; iter != null;
178           iter = iter->next)
179         {
180           if (iter->type != ElementType.ELEMENT_NODE || iter->name != "buddy")
181             continue;
182
183           string blist_protocol = iter->get_prop ("proto");
184           if (blist_protocol == null)
185             continue;
186
187           string tp_protocol =
188               this.blist_protocol_to_tp_protocol (blist_protocol);
189           if (tp_protocol == null)
190             continue;
191
192           /* Parse the <name> and <alias> elements beneath <buddy> */
193           for (Xml.Node *subiter = iter->children; subiter != null;
194               subiter = subiter->next)
195             {
196               if (subiter->type != ElementType.ELEMENT_NODE)
197                 continue;
198
199               if (subiter->name == "alias")
200                 alias = subiter->get_content ();
201               else if (subiter->name == "name")
202                 {
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);
210                 }
211             }
212         }
213
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 ()))
218         {
219           stdout.printf (
220               /* Translators: the parameter is the buddy's IM address. */
221               _("Ignoring buddy with no alias and only one IM address:\n%s"),
222               im_address_string);
223           return null;
224         }
225
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);
231
232       Persona persona;
233       try
234         {
235           persona =
236               yield this.destination_store.add_persona_from_details (details);
237         }
238       catch (PersonaStoreError e)
239         {
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
242            * message. */
243           stderr.printf (
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);
246           return null;
247         }
248
249       /* Set the Persona's details */
250       if (alias != null && persona is AliasDetails)
251         ((AliasDetails) persona).alias = alias;
252
253       /* Print progress */
254       stdout.printf (
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++;
261
262       return persona;
263     }
264
265   private string? blist_protocol_to_tp_protocol (string blist_protocol)
266     {
267       string tp_protocol = blist_protocol;
268       if (blist_protocol.has_prefix ("prpl-"))
269         tp_protocol = blist_protocol.substring (5);
270
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")
284         tp_protocol = "sip";
285
286       return tp_protocol;
287     }
288 }