1 /*
2 * Routines to copy information from a Gaim buddy list into an
3 * Evolution addressbook.
4 *
5 * I currently copy IM account names and buddy icons, provided you
6 * don't already have a buddy icon defined for a person.
7 *
8 * This works today (25 October 2004), but is pretty sure to break
9 * later on as the Gaim buddylist file format shifts.
10 *
11 * This program is free software; you can redistribute it and/or modify it
12 * under the terms of the GNU Lesser General Public License as published by
13 * the Free Software Foundation.
14 *
15 * This program is distributed in the hope that it will be useful, but
16 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
17 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
18 * for more details.
19 *
20 * You should have received a copy of the GNU Lesser General Public License
21 * along with this program; if not, see <http://www.gnu.org/licenses/>.
22 *
23 *
24 * Authors:
25 * Nat Friedman <nat@novell.com>
26 *
27 * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com)
28 *
29 */
30
31 #include "evolution-config.h"
32
33 #include <libxml/tree.h>
34 #include <libxml/parser.h>
35 #include <libxml/xmlmemory.h>
36
37 #include <gtk/gtk.h>
38 #include <glib/gi18n.h>
39 #include <string.h>
40
41 #include <sys/time.h>
42 #include <sys/stat.h>
43
44 #include <e-util/e-util.h>
45
46 #include "bbdb.h"
47
48 typedef struct {
49 gchar *account_name;
50 gchar *proto;
51 gchar *alias;
52 gchar *icon;
53 } GaimBuddy;
54
55 /* Forward declarations for this file. */
56 static gboolean bbdb_merge_buddy_to_contact (EBookClient *client,
57 GaimBuddy *buddy,
58 EContact *contact);
59 static void bbdb_get_gaim_buddy_list (GQueue *out_buddies);
60 static gchar * get_node_text (xmlNodePtr node);
61 static gchar * get_buddy_icon_from_setting (xmlNodePtr setting);
62 static void parse_buddy_group (xmlNodePtr group,
63 GQueue *out_buddies,
64 GSList *blocked);
65 static EContactField
66 proto_to_contact_field (const gchar *proto);
67
68 static void
free_gaim_body(GaimBuddy * gb)69 free_gaim_body (GaimBuddy *gb)
70 {
71 if (gb != NULL) {
72 g_free (gb->icon);
73 g_free (gb->alias);
74 g_free (gb->account_name);
75 g_free (gb->proto);
76 g_free (gb);
77 }
78 }
79
80 static gchar *
get_buddy_filename(void)81 get_buddy_filename (void)
82 {
83 return g_build_filename (
84 g_get_home_dir (), ".purple", "blist.xml", NULL);
85 }
86
87 static gchar *
get_md5_as_string(const gchar * filename)88 get_md5_as_string (const gchar *filename)
89 {
90 GMappedFile *mapped_file;
91 const gchar *contents;
92 gchar *digest;
93 gsize length;
94 GError *error = NULL;
95
96 g_return_val_if_fail (filename != NULL, NULL);
97
98 mapped_file = g_mapped_file_new (filename, FALSE, &error);
99 if (mapped_file == NULL) {
100 g_warning ("%s", error->message);
101 return NULL;
102 }
103
104 contents = g_mapped_file_get_contents (mapped_file);
105 length = g_mapped_file_get_length (mapped_file);
106
107 digest = g_compute_checksum_for_data (
108 G_CHECKSUM_MD5, (guchar *) contents, length);
109
110 g_mapped_file_unref (mapped_file);
111
112 return digest;
113 }
114
115 void
bbdb_sync_buddy_list_check(void)116 bbdb_sync_buddy_list_check (void)
117 {
118 struct stat statbuf;
119 time_t last_sync_time;
120 gchar *md5;
121 gchar *blist_path;
122 gchar *last_sync_str;
123 GSettings *settings = e_util_ref_settings (CONF_SCHEMA);
124
125 blist_path = get_buddy_filename ();
126 if (stat (blist_path, &statbuf) < 0) {
127 g_free (blist_path);
128 return;
129 }
130
131 /* Reprocess the buddy list if it's been updated. */
132 last_sync_str = g_settings_get_string (settings, CONF_KEY_GAIM_LAST_SYNC_TIME);
133 if (last_sync_str == NULL || !strcmp ((const gchar *) last_sync_str, ""))
134 last_sync_time = (time_t) 0;
135 else
136 last_sync_time = (time_t) g_ascii_strtoull (last_sync_str, NULL, 10);
137
138 g_free (last_sync_str);
139
140 if (statbuf.st_mtime <= last_sync_time) {
141 g_object_unref (G_OBJECT (settings));
142 g_free (blist_path);
143 return;
144 }
145
146 last_sync_str = g_settings_get_string (
147 settings, CONF_KEY_GAIM_LAST_SYNC_MD5);
148
149 g_object_unref (settings);
150
151 md5 = get_md5_as_string (blist_path);
152
153 if (!last_sync_str || !*last_sync_str || !g_str_equal (md5, last_sync_str)) {
154 fprintf (stderr, "bbdb: Buddy list has changed since last sync.\n");
155
156 bbdb_sync_buddy_list ();
157 }
158
159 g_free (last_sync_str);
160 g_free (blist_path);
161 g_free (md5);
162 }
163
164 static gboolean
store_last_sync_idle_cb(gpointer data)165 store_last_sync_idle_cb (gpointer data)
166 {
167 GSettings *settings;
168 gchar *md5;
169 gchar *blist_path = get_buddy_filename ();
170 time_t last_sync;
171 gchar *last_sync_time;
172
173 time (&last_sync);
174 last_sync_time = g_strdup_printf ("%ld", (glong) last_sync);
175
176 md5 = get_md5_as_string (blist_path);
177
178 settings = e_util_ref_settings (CONF_SCHEMA);
179 g_settings_set_string (
180 settings, CONF_KEY_GAIM_LAST_SYNC_TIME, last_sync_time);
181 g_settings_set_string (
182 settings, CONF_KEY_GAIM_LAST_SYNC_MD5, md5);
183
184 g_object_unref (G_OBJECT (settings));
185
186 g_free (last_sync_time);
187 g_free (blist_path);
188 g_free (md5);
189
190 return FALSE;
191 }
192
193 static gboolean syncing = FALSE;
194 G_LOCK_DEFINE_STATIC (syncing);
195
196 static gpointer
bbdb_sync_buddy_list_in_thread(gpointer data)197 bbdb_sync_buddy_list_in_thread (gpointer data)
198 {
199 EBookClient *client;
200 GQueue *buddies = data;
201 GList *head, *link;
202 GError *error = NULL;
203
204 g_return_val_if_fail (buddies != NULL, NULL);
205
206 client = bbdb_create_book_client (GAIM_ADDRESSBOOK, NULL, &error);
207 if (error != NULL) {
208 g_warning (
209 "bbdb: Failed to get addressbook: %s",
210 error->message);
211 g_error_free (error);
212 goto exit;
213 }
214
215 printf ("bbdb: Synchronizing buddy list to contacts...\n");
216
217 /* Walk the buddy list */
218
219 head = g_queue_peek_head_link (buddies);
220
221 for (link = head; link != NULL; link = g_list_next (link)) {
222 GaimBuddy *b = link->data;
223 EBookQuery *query;
224 gchar *query_string;
225 GSList *contacts = NULL;
226 EContact *c;
227
228 if (b->alias == NULL || strlen (b->alias) == 0) {
229 g_free (b->alias);
230 b->alias = g_strdup (b->account_name);
231 }
232
233 /* Look for an exact match full name == buddy alias */
234 query = e_book_query_field_test (
235 E_CONTACT_FULL_NAME, E_BOOK_QUERY_IS, b->alias);
236 query_string = e_book_query_to_string (query);
237 e_book_query_unref (query);
238 if (!e_book_client_get_contacts_sync (
239 client, query_string, &contacts, NULL, NULL)) {
240 g_free (query_string);
241 continue;
242 }
243
244 g_free (query_string);
245
246 if (contacts != NULL) {
247
248 /* FIXME: If there's more than one contact with this
249 * name, just give up; we're not smart enough for
250 * this. */
251 if (contacts->next != NULL) {
252 g_slist_free_full (
253 contacts,
254 (GDestroyNotify) g_object_unref);
255 continue;
256 }
257
258 c = E_CONTACT (contacts->data);
259
260 if (!bbdb_merge_buddy_to_contact (client, b, c)) {
261 g_slist_free_full (
262 contacts,
263 (GDestroyNotify) g_object_unref);
264 continue;
265 }
266
267 /* Write it out to the addressbook */
268 e_book_client_modify_contact_sync (
269 client, c, E_BOOK_OPERATION_FLAG_NONE, NULL, &error);
270
271 if (error != NULL) {
272 g_warning (
273 "bbdb: Could not modify contact: %s",
274 error->message);
275 g_clear_error (&error);
276 }
277
278 g_slist_free_full (
279 contacts,
280 (GDestroyNotify) g_object_unref);
281 continue;
282 }
283
284 /* Otherwise, create a new contact. */
285 c = e_contact_new ();
286 e_contact_set (c, E_CONTACT_FULL_NAME, (gpointer) b->alias);
287 if (!bbdb_merge_buddy_to_contact (client, b, c)) {
288 g_object_unref (c);
289 continue;
290 }
291
292 e_book_client_add_contact_sync (client, c, E_BOOK_OPERATION_FLAG_NONE, NULL, NULL, &error);
293
294 if (error != NULL) {
295 g_warning (
296 "bbdb: Failed to add new contact: %s",
297 error->message);
298 g_clear_error (&error);
299 goto exit;
300 }
301
302 g_object_unref (c);
303 }
304
305 g_idle_add (store_last_sync_idle_cb, NULL);
306
307 exit:
308 printf ("bbdb: Done syncing buddy list to contacts.\n");
309
310 g_clear_object (&client);
311
312 g_queue_free_full (buddies, (GDestroyNotify) free_gaim_body);
313
314 G_LOCK (syncing);
315 syncing = FALSE;
316 G_UNLOCK (syncing);
317
318 return NULL;
319 }
320
321 void
bbdb_sync_buddy_list(void)322 bbdb_sync_buddy_list (void)
323 {
324 GQueue *buddies;
325
326 G_LOCK (syncing);
327 if (syncing) {
328 G_UNLOCK (syncing);
329 printf ("bbdb: Already syncing buddy list, skipping this call\n");
330 return;
331 }
332
333 buddies = g_queue_new ();
334 bbdb_get_gaim_buddy_list (buddies);
335
336 if (g_queue_is_empty (buddies)) {
337 g_queue_free (buddies);
338 } else {
339 GThread *thread;
340
341 syncing = TRUE;
342
343 thread = g_thread_new (
344 NULL, bbdb_sync_buddy_list_in_thread, buddies);
345 g_thread_unref (thread);
346 }
347
348 G_UNLOCK (syncing);
349 }
350
351 static gboolean
im_list_contains_buddy(GList * ims,GaimBuddy * b)352 im_list_contains_buddy (GList *ims,
353 GaimBuddy *b)
354 {
355 GList *l;
356
357 for (l = ims; l != NULL; l = l->next) {
358 gchar *im = (gchar *) l->data;
359
360 if (!strcmp (im, b->account_name))
361 return TRUE;
362 }
363
364 return FALSE;
365 }
366
367 static gboolean
bbdb_merge_buddy_to_contact(EBookClient * client,GaimBuddy * b,EContact * c)368 bbdb_merge_buddy_to_contact (EBookClient *client,
369 GaimBuddy *b,
370 EContact *c)
371 {
372 EContactField field;
373 GList *ims;
374 gboolean dirty = FALSE;
375 EContactPhoto *photo = NULL;
376 GError *error = NULL;
377
378 /* Set the IM account */
379 field = proto_to_contact_field (b->proto);
380 ims = e_contact_get (c, field);
381 if (!im_list_contains_buddy (ims, b)) {
382 ims = g_list_append (ims, g_strdup (b->account_name));
383 e_contact_set (c, field, (gpointer) ims);
384 dirty = TRUE;
385 }
386
387 g_list_foreach (ims, (GFunc) g_free, NULL);
388 g_list_free (ims);
389 ims = NULL;
390
391 /* Set the photo if it's not set */
392 if (b->icon != NULL) {
393 photo = e_contact_get (c, E_CONTACT_PHOTO);
394 if (photo == NULL) {
395 gchar *contents = NULL;
396
397 photo = e_contact_photo_new ();
398 photo->type = E_CONTACT_PHOTO_TYPE_INLINED;
399
400 if (!g_file_get_contents (
401 b->icon, &contents,
402 &photo->data.inlined.length, &error)) {
403 g_warning (
404 "bbdb: Could not read buddy icon: "
405 "%s\n", error->message);
406 g_error_free (error);
407 e_contact_photo_free (photo);
408 return dirty;
409 }
410
411 photo->data.inlined.data = (guchar *) contents;
412 e_contact_set (c, E_CONTACT_PHOTO, (gpointer) photo);
413 dirty = TRUE;
414 }
415 }
416
417 /* Clean up */
418 if (photo != NULL)
419 e_contact_photo_free (photo);
420
421 return dirty;
422 }
423
424 static EContactField
proto_to_contact_field(const gchar * proto)425 proto_to_contact_field (const gchar *proto)
426 {
427 if (!strcmp (proto, "prpl-oscar"))
428 return E_CONTACT_IM_AIM;
429 if (!strcmp (proto, "prpl-novell"))
430 return E_CONTACT_IM_GROUPWISE;
431 if (!strcmp (proto, "prpl-msn"))
432 return E_CONTACT_IM_MSN;
433 if (!strcmp (proto, "prpl-icq"))
434 return E_CONTACT_IM_ICQ;
435 if (!strcmp (proto, "prpl-yahoo"))
436 return E_CONTACT_IM_YAHOO;
437 if (!strcmp (proto, "prpl-jabber"))
438 return E_CONTACT_IM_JABBER;
439 if (!strcmp (proto, "prpl-gg"))
440 return E_CONTACT_IM_GADUGADU;
441 if (!strcmp (proto, "prpl-matrix"))
442 return E_CONTACT_IM_MATRIX;
443
444 return E_CONTACT_IM_AIM;
445 }
446
447 static void
get_all_blocked(xmlNodePtr node,GSList ** blocked)448 get_all_blocked (xmlNodePtr node,
449 GSList **blocked)
450 {
451 xmlNodePtr child;
452
453 if (!node || !blocked)
454 return;
455
456 for (child = node->children; child; child = child->next) {
457 if (child->children)
458 get_all_blocked (child, blocked);
459
460 if (!strcmp ((const gchar *) child->name, "block")) {
461 gchar *name = get_node_text (child);
462
463 if (name)
464 *blocked = g_slist_prepend (*blocked, name);
465 }
466 }
467 }
468
469 static void
bbdb_get_gaim_buddy_list(GQueue * out_buddies)470 bbdb_get_gaim_buddy_list (GQueue *out_buddies)
471 {
472 gchar *blist_path;
473 xmlDocPtr buddy_xml;
474 xmlNodePtr root, child, blist;
475 GSList *blocked = NULL;
476
477 blist_path = get_buddy_filename ();
478
479 buddy_xml = xmlParseFile (blist_path);
480 g_free (blist_path);
481 if (!buddy_xml) {
482 fprintf (stderr, "bbdb: Could not open Pidgin buddy list.\n");
483 return;
484 }
485
486 root = xmlDocGetRootElement (buddy_xml);
487 if (strcmp ((const gchar *) root->name, "purple")) {
488 fprintf (stderr, "bbdb: Could not parse Pidgin buddy list.\n");
489 xmlFreeDoc (buddy_xml);
490 return;
491 }
492
493 for (child = root->children; child != NULL; child = child->next) {
494 if (!strcmp ((const gchar *) child->name, "privacy")) {
495 get_all_blocked (child, &blocked);
496 break;
497 }
498 }
499
500 blist = NULL;
501 for (child = root->children; child != NULL; child = child->next) {
502 if (!strcmp ((const gchar *) child->name, "blist")) {
503 blist = child;
504 break;
505 }
506 }
507 if (blist == NULL) {
508 fprintf (
509 stderr, "bbdb: Could not find 'blist' "
510 "element in Pidgin buddy list.\n");
511 xmlFreeDoc (buddy_xml);
512 return;
513 }
514
515 for (child = blist->children; child != NULL; child = child->next) {
516 if (!strcmp ((const gchar *) child->name, "group"))
517 parse_buddy_group (child, out_buddies, blocked);
518 }
519
520 xmlFreeDoc (buddy_xml);
521
522 g_slist_foreach (blocked, (GFunc) g_free, NULL);
523 g_slist_free (blocked);
524 }
525
526 static gchar *
get_node_text(xmlNodePtr node)527 get_node_text (xmlNodePtr node)
528 {
529 if (node->children == NULL || node->children->content == NULL ||
530 strcmp ((gchar *) node->children->name, "text"))
531 return NULL;
532
533 return g_strdup ((gchar *) node->children->content);
534 }
535
536 static gchar *
get_buddy_icon_from_setting(xmlNodePtr setting)537 get_buddy_icon_from_setting (xmlNodePtr setting)
538 {
539 gchar *icon = NULL;
540
541 icon = get_node_text (setting);
542 if (icon[0] != '/') {
543 gchar *path;
544
545 path = g_build_path ("/", g_get_home_dir (), ".purple/icons", icon, NULL);
546 g_free (icon);
547 icon = path;
548 }
549
550 return icon;
551 }
552
553 static void
parse_contact(xmlNodePtr contact,GQueue * out_buddies,GSList * blocked)554 parse_contact (xmlNodePtr contact,
555 GQueue *out_buddies,
556 GSList *blocked)
557 {
558 xmlNodePtr child;
559 xmlNodePtr buddy = NULL;
560 GaimBuddy *gb;
561 gboolean is_blocked = FALSE;
562
563 for (child = contact->children; child != NULL; child = child->next) {
564 if (!strcmp ((const gchar *) child->name, "buddy")) {
565 buddy = child;
566 break;
567 }
568 }
569
570 if (buddy == NULL) {
571 fprintf (
572 stderr, "bbdb: Could not find buddy in contact. "
573 "Malformed Pidgin buddy list file.\n");
574 return;
575 }
576
577 gb = g_new0 (GaimBuddy, 1);
578
579 gb->proto = e_xml_get_string_prop_by_name (buddy, (const guchar *)"proto");
580
581 for (child = buddy->children; child != NULL && !is_blocked; child = child->next) {
582 if (!strcmp ((const gchar *) child->name, "setting")) {
583 gchar *setting_type;
584
585 setting_type = e_xml_get_string_prop_by_name (
586 child, (const guchar *)"name");
587
588 if (!strcmp ((const gchar *) setting_type, "buddy_icon"))
589 gb->icon = get_buddy_icon_from_setting (child);
590
591 g_free (setting_type);
592 } else if (!strcmp ((const gchar *) child->name, "name")) {
593 gb->account_name = get_node_text (child);
594 is_blocked = g_slist_find_custom (
595 blocked, gb->account_name,
596 (GCompareFunc) strcmp) != NULL;
597 } else if (!strcmp ((const gchar *) child->name, "alias"))
598 gb->alias = get_node_text (child);
599
600 }
601
602 if (is_blocked)
603 free_gaim_body (gb);
604 else
605 g_queue_push_tail (out_buddies, gb);
606 }
607
608 static void
parse_buddy_group(xmlNodePtr group,GQueue * out_buddies,GSList * blocked)609 parse_buddy_group (xmlNodePtr group,
610 GQueue *out_buddies,
611 GSList *blocked)
612 {
613 xmlNodePtr child;
614
615 for (child = group->children; child != NULL; child = child->next) {
616 if (strcmp ((const gchar *) child->name, "contact"))
617 continue;
618
619 parse_contact (child, out_buddies, blocked);
620 }
621 }
622