Rename Aliasable -> AliasDetails
[platform/upstream/folks.git] / tools / import-pidgin.vala
1 /*
2  * Copyright (C) 2010 Collabora Ltd.
3  *
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.
8  *
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.
13  *
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/>.
16  *
17  * Authors:
18  *       Philip Withnall <philip.withnall@collabora.co.uk>
19  */
20
21 using GLib;
22 using Gee;
23 using Xml;
24 using Folks;
25
26 public class Folks.Importers.Pidgin : Folks.Importer
27 {
28   private PersonaStore destination_store;
29   private uint persona_count = 0;
30
31   public override async uint import (PersonaStore destination_store,
32       string? source_filename) throws ImportError
33     {
34       this.destination_store = destination_store;
35       string filename = source_filename;
36
37       /* Default filename */
38       if (filename == null || filename.strip () == "")
39         {
40           filename = Path.build_filename (Environment.get_home_dir (),
41               ".purple", "blist.xml", null);
42         }
43
44       var file = File.new_for_path (filename);
45       if (!file.query_exists ())
46         {
47           /* Translators: the parameter is a filename. */
48           throw new ImportError.MALFORMED_INPUT (_("File %s does not exist."),
49               filename);
50         }
51
52       FileInfo file_info;
53       try
54         {
55           file_info = yield file.query_info_async (
56               FILE_ATTRIBUTE_ACCESS_CAN_READ, FileQueryInfoFlags.NONE,
57               Priority.DEFAULT);
58         }
59       catch (GLib.Error e)
60         {
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,
65               e.message);
66         }
67
68       if (!file_info.get_attribute_boolean (FILE_ATTRIBUTE_ACCESS_CAN_READ))
69         {
70           /* Translators: the parameter is a filename. */
71           throw new ImportError.MALFORMED_INPUT (_("File %s is not readable."),
72               filename);
73         }
74
75       Xml.Doc* xml_doc = Parser.parse_file (filename);
76
77       if (xml_doc == null)
78         {
79           throw new ImportError.MALFORMED_INPUT (
80               /* Translators: the parameter is a filename. */
81               _("The Pidgin buddy list file '%s' could not be loaded."),
82               filename);
83         }
84
85       /* Check the root node */
86       Xml.Node *root_node = xml_doc->get_root_element ();
87
88       if (root_node == null || root_node->name != "purple" ||
89           root_node->get_prop ("version") != "1.0")
90         {
91           /* Free the document manually before throwing because the garbage
92            * collector can't work on pointers. */
93           delete xml_doc;
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."),
97               filename);
98         }
99
100       /* Parse each <blist> child element */
101       for (Xml.Node *iter = root_node->children; iter != null;
102           iter = iter->next)
103         {
104           if (iter->type != ElementType.ELEMENT_NODE || iter->name != "blist")
105             continue;
106
107           yield this.parse_blist (iter);
108         }
109
110       /* Tidy up */
111       delete xml_doc;
112
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,
116           filename);
117
118       /* Return the number of Personas we imported */
119       return this.persona_count;
120     }
121
122   private async void parse_blist (Xml.Node *blist_node)
123     {
124       for (Xml.Node *iter = blist_node->children; iter != null;
125           iter = iter->next)
126         {
127           if (iter->type != ElementType.ELEMENT_NODE || iter->name != "group")
128             continue;
129
130           yield this.parse_group (iter);
131         }
132     }
133
134   private async void parse_group (Xml.Node *group_node)
135     {
136       string group_name = group_node->get_prop ("name");
137
138       for (Xml.Node *iter = group_node->children; iter != null;
139           iter = iter->next)
140         {
141           if (iter->type != ElementType.ELEMENT_NODE || iter->name != "contact")
142             continue;
143
144           Persona persona = yield this.parse_contact (iter);
145
146           /* Skip the persona if creating them failed or if they don't support
147            * groups. */
148           if (persona == null || !(persona is Groupable))
149             continue;
150
151           try
152             {
153               Groupable groupable = (Groupable) persona;
154               yield groupable.change_group (group_name, true);
155             }
156           catch (GLib.Error e)
157             {
158               stderr.printf (
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);
163             }
164         }
165     }
166
167   private async Persona? parse_contact (Xml.Node *contact_node)
168     {
169       string alias = null;
170       HashTable<string, GenericArray<string>> im_addresses =
171           new HashTable<string, GenericArray<string>> (str_hash, str_equal);
172       string im_address_string = "";
173
174       /* Parse the <buddy> elements beneath <contact> */
175       for (Xml.Node *iter = contact_node->children; iter != null;
176           iter = iter->next)
177         {
178           if (iter->type != ElementType.ELEMENT_NODE || iter->name != "buddy")
179             continue;
180
181           string blist_protocol = iter->get_prop ("proto");
182           if (blist_protocol == null)
183             continue;
184
185           string tp_protocol =
186               this.blist_protocol_to_tp_protocol (blist_protocol);
187           if (tp_protocol == null)
188             continue;
189
190           /* Parse the <name> and <alias> elements beneath <buddy> */
191           for (Xml.Node *subiter = iter->children; subiter != null;
192               subiter = subiter->next)
193             {
194               if (subiter->type != ElementType.ELEMENT_NODE)
195                 continue;
196
197               if (subiter->name == "alias")
198                 alias = subiter->get_content ();
199               else if (subiter->name == "name")
200                 {
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 ();
205
206                   GenericArray<string> im_address_array =
207                       im_addresses.lookup (tp_protocol);
208                   if (im_address_array == null)
209                     {
210                       im_address_array = new GenericArray<string> ();
211                       im_addresses.insert (tp_protocol, im_address_array);
212                     }
213
214                   im_address_array.add (im_address);
215                   im_address_string += "    %s\n".printf (im_address);
216                 }
217             }
218         }
219
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 ()))
224         {
225           stdout.printf (
226               /* Translators: the parameter is the buddy's IM address. */
227               _("Ignoring buddy with no alias and only one IM address:\n%s"),
228               im_address_string);
229           return null;
230         }
231
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);
238
239       Persona persona;
240       try
241         {
242           persona =
243               yield this.destination_store.add_persona_from_details (details);
244         }
245       catch (PersonaStoreError e)
246         {
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
249            * message. */
250           stderr.printf (
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);
253           return null;
254         }
255
256       /* Set the Persona's details */
257       if (alias != null && persona is AliasDetails)
258         ((AliasDetails) persona).alias = alias;
259
260       /* Print progress */
261       stdout.printf (
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++;
268
269       return persona;
270     }
271
272   private string? blist_protocol_to_tp_protocol (string blist_protocol)
273     {
274       string tp_protocol = blist_protocol;
275       if (blist_protocol.has_prefix ("prpl-"))
276         tp_protocol = blist_protocol.substring (5);
277
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")
291         tp_protocol = "sip";
292
293       return tp_protocol;
294     }
295 }