These changes are reuired to make the Windows installer build.
[pidgin-git.git] / libpurple / plugins / log_reader.c
blobd7090a015d5b8d98495c6c5fca55732f77cd9b35
1 #include "internal.h"
3 #include <stdio.h>
5 #include "debug.h"
6 #include "log.h"
7 #include "plugin.h"
8 #include "pluginpref.h"
9 #include "prefs.h"
10 #include "stringref.h"
11 #include "util.h"
12 #include "version.h"
13 #include "xmlnode.h"
15 /* This must be the last Purple header included. */
16 #ifdef _WIN32
17 #include "win32dep.h"
18 #endif
20 /* Where is the Windows partition mounted? */
21 #ifndef PURPLE_LOG_READER_WINDOWS_MOUNT_POINT
22 #define PURPLE_LOG_READER_WINDOWS_MOUNT_POINT "/mnt/windows"
23 #endif
25 enum name_guesses {
26 NAME_GUESS_UNKNOWN,
27 NAME_GUESS_ME,
28 NAME_GUESS_THEM
31 /* Some common functions. */
32 static int get_month(const char *month)
34 int iter;
35 const char *months[] = {"Jan", "Feb", "Mar", "Apr", "May", "Jun",
36 "Jul", "Aug", "Sep", "Oct", "Nov", "Dec", NULL};
37 for (iter = 0; months[iter]; iter++) {
38 if (strcmp(month, months[iter]) == 0)
39 break;
41 return iter;
45 /*****************************************************************************
46 * Adium Logger *
47 *****************************************************************************/
49 /* The adium logger doesn't write logs, only reads them. This is to include
50 * Adium logs in the log viewer transparently.
53 static PurpleLogLogger *adium_logger;
55 enum adium_log_type {
56 ADIUM_HTML,
57 ADIUM_TEXT,
60 struct adium_logger_data {
61 char *path;
62 enum adium_log_type type;
65 static GList *adium_logger_list(PurpleLogType type, const char *sn, PurpleAccount *account)
67 GList *list = NULL;
68 const char *logdir;
69 PurplePlugin *plugin;
70 PurplePluginProtocolInfo *prpl_info;
71 char *prpl_name;
72 char *temp;
73 char *path;
74 GDir *dir;
76 g_return_val_if_fail(sn != NULL, NULL);
77 g_return_val_if_fail(account != NULL, NULL);
79 logdir = purple_prefs_get_string("/plugins/core/log_reader/adium/log_directory");
81 /* By clearing the log directory path, this logger can be (effectively) disabled. */
82 if (!logdir || !*logdir)
83 return NULL;
85 plugin = purple_find_prpl(purple_account_get_protocol_id(account));
86 if (!plugin)
87 return NULL;
89 prpl_info = PURPLE_PLUGIN_PROTOCOL_INFO(plugin);
90 if (!prpl_info->list_icon)
91 return NULL;
93 prpl_name = g_ascii_strup(prpl_info->list_icon(account, NULL), -1);
95 temp = g_strdup_printf("%s.%s", prpl_name, account->username);
96 path = g_build_filename(logdir, temp, sn, NULL);
97 g_free(temp);
99 dir = g_dir_open(path, 0, NULL);
100 if (dir) {
101 const gchar *file;
103 while ((file = g_dir_read_name(dir))) {
104 if (!purple_str_has_prefix(file, sn))
105 continue;
106 if (purple_str_has_suffix(file, ".html") || purple_str_has_suffix(file, ".AdiumHTMLLog")) {
107 struct tm tm;
108 const char *date = file;
110 date += strlen(sn) + 2;
111 if (sscanf(date, "%u|%u|%u",
112 &tm.tm_year, &tm.tm_mon, &tm.tm_mday) != 3) {
114 purple_debug_error("Adium log parse",
115 "Filename timestamp parsing error\n");
116 } else {
117 char *filename = g_build_filename(path, file, NULL);
118 FILE *handle = g_fopen(filename, "rb");
119 char contents[57]; /* XXX: This is really inflexible. */
120 char *contents2;
121 struct adium_logger_data *data;
122 size_t rd;
123 PurpleLog *log;
125 if (!handle) {
126 g_free(filename);
127 continue;
130 rd = fread(contents, 1, 56, handle) == 0;
131 fclose(handle);
132 contents[rd] = '\0';
134 /* XXX: This is fairly inflexible. */
135 contents2 = contents;
136 while (*contents2 && *contents2 != '>')
137 contents2++;
138 if (*contents2)
139 contents2++;
140 while (*contents2 && *contents2 != '>')
141 contents2++;
142 if (*contents2)
143 contents2++;
145 if (sscanf(contents2, "%u.%u.%u",
146 &tm.tm_hour, &tm.tm_min, &tm.tm_sec) != 3) {
148 purple_debug_error("Adium log parse",
149 "Contents timestamp parsing error\n");
150 g_free(filename);
151 continue;
154 data = g_new0(struct adium_logger_data, 1);
155 data->path = filename;
156 data->type = ADIUM_HTML;
158 tm.tm_year -= 1900;
159 tm.tm_mon -= 1;
161 /* XXX: Look into this later... Should we pass in a struct tm? */
162 log = purple_log_new(PURPLE_LOG_IM, sn, account, NULL, mktime(&tm), NULL);
163 log->logger = adium_logger;
164 log->logger_data = data;
166 list = g_list_prepend(list, log);
168 } else if (purple_str_has_suffix(file, ".adiumLog")) {
169 struct tm tm;
170 const char *date = file;
172 date += strlen(sn) + 2;
173 if (sscanf(date, "%u|%u|%u",
174 &tm.tm_year, &tm.tm_mon, &tm.tm_mday) != 3) {
176 purple_debug_error("Adium log parse",
177 "Filename timestamp parsing error\n");
178 } else {
179 char *filename = g_build_filename(path, file, NULL);
180 FILE *handle = g_fopen(filename, "rb");
181 char contents[14]; /* XXX: This is really inflexible. */
182 char *contents2;
183 struct adium_logger_data *data;
184 PurpleLog *log;
185 size_t rd;
187 if (!handle) {
188 g_free(filename);
189 continue;
192 rd = fread(contents, 1, 13, handle);
193 fclose(handle);
194 contents[rd] = '\0';
196 contents2 = contents;
197 while (*contents2 && *contents2 != '(')
198 contents2++;
199 if (*contents2)
200 contents2++;
202 if (sscanf(contents2, "%u.%u.%u",
203 &tm.tm_hour, &tm.tm_min, &tm.tm_sec) != 3) {
205 purple_debug_error("Adium log parse",
206 "Contents timestamp parsing error\n");
207 g_free(filename);
208 continue;
211 tm.tm_year -= 1900;
212 tm.tm_mon -= 1;
214 data = g_new0(struct adium_logger_data, 1);
215 data->path = filename;
216 data->type = ADIUM_TEXT;
218 /* XXX: Look into this later... Should we pass in a struct tm? */
219 log = purple_log_new(PURPLE_LOG_IM, sn, account, NULL, mktime(&tm), NULL);
220 log->logger = adium_logger;
221 log->logger_data = data;
223 list = g_list_prepend(list, log);
227 g_dir_close(dir);
230 g_free(prpl_name);
231 g_free(path);
233 return list;
236 static char *adium_logger_read (PurpleLog *log, PurpleLogReadFlags *flags)
238 struct adium_logger_data *data;
239 GError *error = NULL;
240 gchar *read = NULL;
242 /* XXX: TODO: We probably want to set PURPLE_LOG_READ_NO_NEWLINE
243 * XXX: TODO: for HTML logs. */
244 if (flags != NULL)
245 *flags = 0;
247 g_return_val_if_fail(log != NULL, g_strdup(""));
249 data = log->logger_data;
251 g_return_val_if_fail(data->path != NULL, g_strdup(""));
253 purple_debug_info("Adium log read", "Reading %s\n", data->path);
254 if (!g_file_get_contents(data->path, &read, NULL, &error)) {
255 purple_debug_error("Adium log read", "Error reading log: %s\n",
256 (error && error->message) ? error->message : "Unknown error");
257 if (error)
258 g_error_free(error);
259 return g_strdup("");
262 if (data->type != ADIUM_HTML) {
263 char *escaped = g_markup_escape_text(read, -1);
264 g_free(read);
265 read = escaped;
268 #ifdef WIN32
269 /* This problem only seems to show up on Windows.
270 * The BOM is displaying as a space at the beginning of the log.
272 if (purple_str_has_prefix(read, "\xef\xbb\xbf"))
274 /* FIXME: This feels so wrong... */
275 char *temp = g_strdup(&(read[3]));
276 g_free(read);
277 read = temp;
279 #endif
281 /* TODO: Apply formatting.
282 * Replace the above hack with something better, since we'll
283 * be looping over the entire log file contents anyway.
286 return read;
289 static int adium_logger_size (PurpleLog *log)
291 struct adium_logger_data *data;
292 char *text;
293 size_t size;
295 g_return_val_if_fail(log != NULL, 0);
297 data = log->logger_data;
299 if (purple_prefs_get_bool("/plugins/core/log_reader/fast_sizes")) {
300 struct stat st;
302 if (!data->path || stat(data->path, &st))
303 st.st_size = 0;
305 return st.st_size;
308 text = adium_logger_read(log, NULL);
309 size = strlen(text);
310 g_free(text);
312 return size;
315 static void adium_logger_finalize(PurpleLog *log)
317 struct adium_logger_data *data;
319 g_return_if_fail(log != NULL);
321 data = log->logger_data;
323 g_free(data->path);
324 g_free(data);
328 /*****************************************************************************
329 * Fire Logger *
330 *****************************************************************************/
332 #if 0
333 /* The fire logger doesn't write logs, only reads them. This is to include
334 * Fire logs in the log viewer transparently.
337 static PurpleLogLogger *fire_logger;
339 struct fire_logger_data {
342 static GList *fire_logger_list(PurpleLogType type, const char *sn, PurpleAccount *account)
344 /* TODO: Do something here. */
345 return NULL;
348 static char * fire_logger_read (PurpleLog *log, PurpleLogReadFlags *flags)
350 struct fire_logger_data *data;
352 g_return_val_if_fail(log != NULL, g_strdup(""));
354 data = log->logger_data;
356 /* TODO: Do something here. */
357 return g_strdup("");
360 static int fire_logger_size (PurpleLog *log)
362 g_return_val_if_fail(log != NULL, 0);
364 if (purple_prefs_get_bool("/plugins/core/log_reader/fast_sizes"))
365 return 0;
367 /* TODO: Do something here. */
368 return 0;
371 static void fire_logger_finalize(PurpleLog *log)
373 g_return_if_fail(log != NULL);
375 /* TODO: Do something here. */
377 #endif
380 /*****************************************************************************
381 * Messenger Plus! Logger *
382 *****************************************************************************/
384 #if 0
385 /* The messenger_plus logger doesn't write logs, only reads them. This is to include
386 * Messenger Plus! logs in the log viewer transparently.
389 static PurpleLogLogger *messenger_plus_logger;
391 struct messenger_plus_logger_data {
394 static GList *messenger_plus_logger_list(PurpleLogType type, const char *sn, PurpleAccount *account)
396 /* TODO: Do something here. */
397 return NULL;
400 static char * messenger_plus_logger_read (PurpleLog *log, PurpleLogReadFlags *flags)
402 struct messenger_plus_logger_data *data = log->logger_data;
404 g_return_val_if_fail(log != NULL, g_strdup(""));
406 data = log->logger_data;
408 /* TODO: Do something here. */
409 return g_strdup("");
412 static int messenger_plus_logger_size (PurpleLog *log)
414 g_return_val_if_fail(log != NULL, 0);
416 if (purple_prefs_get_bool("/plugins/core/log_reader/fast_sizes"))
417 return 0;
419 /* TODO: Do something here. */
420 return 0;
423 static void messenger_plus_logger_finalize(PurpleLog *log)
425 g_return_if_fail(log != NULL);
427 /* TODO: Do something here. */
429 #endif
432 /*****************************************************************************
433 * MSN Messenger Logger *
434 *****************************************************************************/
436 /* The msn logger doesn't write logs, only reads them. This is to include
437 * MSN Messenger message histories in the log viewer transparently.
440 static PurpleLogLogger *msn_logger;
442 struct msn_logger_data {
443 xmlnode *root;
444 xmlnode *message;
445 const char *session_id;
446 int last_log;
447 GString *text;
450 /* This function is really confusing. It makes baby rlaager cry...
451 In other news: "You lost a lot of blood but we found most of it."
453 static time_t msn_logger_parse_timestamp(xmlnode *message, struct tm **tm_out)
455 const char *datetime;
456 static struct tm tm2;
457 time_t stamp;
458 const char *date;
459 const char *time;
460 int month;
461 int day;
462 int year;
463 int hour;
464 int min;
465 int sec;
466 char am_pm;
467 char *str;
468 static struct tm tm;
469 time_t t;
470 time_t diff;
472 #ifndef G_DISABLE_CHECKS
473 if (message != NULL)
475 *tm_out = NULL;
477 /* Trigger the usual warning. */
478 g_return_val_if_fail(message != NULL, (time_t)0);
480 #endif
482 datetime = xmlnode_get_attrib(message, "DateTime");
483 if (!(datetime && *datetime))
485 purple_debug_error("MSN log timestamp parse",
486 "Attribute missing: %s\n", "DateTime");
487 return (time_t)0;
490 stamp = purple_str_to_time(datetime, TRUE, &tm2, NULL, NULL);
491 #ifdef HAVE_TM_GMTOFF
492 tm2.tm_gmtoff = 0;
493 #endif
494 #ifdef HAVE_STRUCT_TM_TM_ZONE
495 /* This is used in the place of a timezone abbreviation if the
496 * offset is way off. The user should never really see it, but
497 * it's here just in case. The parens are to make it clear it's
498 * not a real timezone. */
499 tm2.tm_zone = _("(UTC)");
500 #endif
503 date = xmlnode_get_attrib(message, "Date");
504 if (!(date && *date))
506 purple_debug_error("MSN log timestamp parse",
507 "Attribute missing: %s\n", "Date");
508 *tm_out = &tm2;
509 return stamp;
512 time = xmlnode_get_attrib(message, "Time");
513 if (!(time && *time))
515 purple_debug_error("MSN log timestamp parse",
516 "Attribute missing: %s\n", "Time");
517 *tm_out = &tm2;
518 return stamp;
521 if (sscanf(date, "%u/%u/%u", &month, &day, &year) != 3)
523 purple_debug_error("MSN log timestamp parse",
524 "%s parsing error\n", "Date");
525 *tm_out = &tm2;
526 return stamp;
528 else
530 if (month > 12)
532 int tmp = day;
533 day = month;
534 month = tmp;
538 if (sscanf(time, "%u:%u:%u %c", &hour, &min, &sec, &am_pm) != 4)
540 purple_debug_error("MSN log timestamp parse",
541 "%s parsing error\n", "Time");
542 *tm_out = &tm2;
543 return stamp;
546 if (am_pm == 'P') {
547 hour += 12;
548 } else if (hour == 12) {
549 /* 12 AM = 00 hr */
550 hour = 0;
553 str = g_strdup_printf("%04i-%02i-%02iT%02i:%02i:%02i", year, month, day, hour, min, sec);
554 t = purple_str_to_time(str, TRUE, &tm, NULL, NULL);
557 if (stamp > t)
558 diff = stamp - t;
559 else
560 diff = t - stamp;
562 if (diff > (14 * 60 * 60))
564 if (day <= 12)
566 /* Swap day & month variables, to see if it's a non-US date. */
567 g_free(str);
568 str = g_strdup_printf("%04i-%02i-%02iT%02i:%02i:%02i", year, month, day, hour, min, sec);
569 t = purple_str_to_time(str, TRUE, &tm, NULL, NULL);
571 if (stamp > t)
572 diff = stamp - t;
573 else
574 diff = t - stamp;
576 if (diff > (14 * 60 * 60))
578 /* We got a time, it's not impossible, but
579 * the diff is too large. Display the UTC time. */
580 g_free(str);
581 *tm_out = &tm2;
582 return stamp;
584 else
586 /* Legal time */
587 /* Fall out */
590 else
592 /* We got a time, it's not impossible, but
593 * the diff is too large. Display the UTC time. */
594 g_free(str);
595 *tm_out = &tm2;
596 return stamp;
600 /* If we got here, the time is legal with a reasonable offset.
601 * Let's find out if it's in our TZ. */
602 if (purple_str_to_time(str, FALSE, &tm, NULL, NULL) == stamp)
604 g_free(str);
605 *tm_out = &tm;
606 return stamp;
608 g_free(str);
610 /* The time isn't in our TZ, but it's reasonable. */
611 #ifdef HAVE_STRUCT_TM_TM_ZONE
612 tm.tm_zone = " ";
613 #endif
614 *tm_out = &tm;
615 return stamp;
618 static GList *msn_logger_list(PurpleLogType type, const char *sn, PurpleAccount *account)
620 GList *list = NULL;
621 char *username;
622 PurpleBuddy *buddy;
623 const char *logdir;
624 const char *savedfilename = NULL;
625 char *logfile;
626 char *path;
627 GError *error = NULL;
628 gchar *contents = NULL;
629 gsize length;
630 xmlnode *root;
631 xmlnode *message;
632 const char *old_session_id = "";
633 struct msn_logger_data *data = NULL;
635 g_return_val_if_fail(sn != NULL, NULL);
636 g_return_val_if_fail(account != NULL, NULL);
638 if (strcmp(account->protocol_id, "prpl-msn"))
639 return NULL;
641 logdir = purple_prefs_get_string("/plugins/core/log_reader/msn/log_directory");
643 /* By clearing the log directory path, this logger can be (effectively) disabled. */
644 if (!logdir || !*logdir)
645 return NULL;
647 buddy = purple_find_buddy(account, sn);
649 if ((username = g_strdup(purple_account_get_string(
650 account, "log_reader_msn_log_folder", NULL)))) {
651 /* As a special case, we allow the null string to kill the parsing
652 * straight away. This would allow the user to deal with the case
653 * when two account have the same username at different domains and
654 * only one has logs stored.
656 if (!*username) {
657 g_free(username);
658 return list;
660 } else {
661 username = g_strdup(purple_normalize(account, account->username));
664 if (buddy) {
665 savedfilename = purple_blist_node_get_string((PurpleBlistNode *)buddy,
666 "log_reader_msn_log_filename");
669 if (savedfilename) {
670 /* As a special case, we allow the null string to kill the parsing
671 * straight away. This would allow the user to deal with the case
672 * when two buddies have the same username at different domains and
673 * only one has logs stored.
675 if (!*savedfilename) {
676 g_free(username);
677 return list;
680 logfile = g_strdup(savedfilename);
681 } else {
682 logfile = g_strdup_printf("%s.xml", purple_normalize(account, sn));
685 path = g_build_filename(logdir, username, "History", logfile, NULL);
687 if (!g_file_test(path, G_FILE_TEST_EXISTS)) {
688 gboolean found = FALSE;
689 char *at_sign;
690 GDir *dir;
692 g_free(path);
694 if (savedfilename) {
695 /* We had a saved filename, but it doesn't exist.
696 * Returning now is the right course of action because we don't
697 * want to detect another file incorrectly.
699 g_free(username);
700 g_free(logfile);
701 return list;
704 /* Perhaps we're using a new version of MSN with the weird numbered folders.
705 * I don't know how the numbers are calculated, so I'm going to attempt to
706 * find logs by pattern matching...
709 at_sign = g_strrstr(username, "@");
710 if (at_sign)
711 *at_sign = '\0';
713 dir = g_dir_open(logdir, 0, NULL);
714 if (dir) {
715 const gchar *name;
717 while ((name = g_dir_read_name(dir))) {
718 const char *c = name;
720 if (!purple_str_has_prefix(c, username))
721 continue;
723 c += strlen(username);
724 while (*c) {
725 if (!g_ascii_isdigit(*c))
726 break;
728 c++;
731 path = g_build_filename(logdir, name, NULL);
732 /* The !c makes sure we got to the end of the while loop above. */
733 if (!*c && g_file_test(path, G_FILE_TEST_IS_DIR)) {
734 char *history_path = g_build_filename(
735 path, "History", NULL);
736 if (g_file_test(history_path, G_FILE_TEST_IS_DIR)) {
737 purple_account_set_string(account,
738 "log_reader_msn_log_folder", name);
739 g_free(path);
740 path = history_path;
741 found = TRUE;
742 break;
744 g_free(path);
745 g_free(history_path);
747 else
748 g_free(path);
750 g_dir_close(dir);
752 g_free(username);
754 if (!found) {
755 g_free(logfile);
756 return list;
759 /* If we've reached this point, we've found a History folder. */
761 username = g_strdup(purple_normalize(account, sn));
762 at_sign = g_strrstr(username, "@");
763 if (at_sign)
764 *at_sign = '\0';
766 found = FALSE;
767 dir = g_dir_open(path, 0, NULL);
768 if (dir) {
769 const gchar *name;
771 while ((name = g_dir_read_name(dir))) {
772 const char *c = name;
774 if (!purple_str_has_prefix(c, username))
775 continue;
777 c += strlen(username);
778 while (*c) {
779 if (!g_ascii_isdigit(*c))
780 break;
782 c++;
785 path = g_build_filename(path, name, NULL);
786 if (!strcmp(c, ".xml") &&
787 g_file_test(path, G_FILE_TEST_EXISTS)) {
788 found = TRUE;
789 g_free(logfile);
790 logfile = g_strdup(name);
791 break;
793 else
794 g_free(path);
796 g_dir_close(dir);
798 g_free(username);
800 if (!found) {
801 g_free(logfile);
802 return list;
804 } else {
805 g_free(username);
806 g_free(logfile);
807 logfile = NULL; /* No sense saving the obvious buddy@domain.com. */
810 purple_debug_info("MSN log read", "Reading %s\n", path);
811 if (!g_file_get_contents(path, &contents, &length, &error)) {
812 g_free(path);
813 purple_debug_error("MSN log read", "Error reading log\n");
814 if (error)
815 g_error_free(error);
816 return list;
818 g_free(path);
820 /* Reading the file was successful...
821 * Save its name if it involves the crazy numbers. The idea here is that you could
822 * then tweak the blist.xml file by hand if need be. This would be the case if two
823 * buddies have the same username at different domains. One set of logs would get
824 * detected for both buddies.
826 if (buddy && logfile) {
827 PurpleBlistNode *node = (PurpleBlistNode *)buddy;
828 purple_blist_node_set_string(node, "log_reader_msn_log_filename", logfile);
829 g_free(logfile);
832 root = xmlnode_from_str(contents, length);
833 g_free(contents);
834 if (!root)
835 return list;
837 for (message = xmlnode_get_child(root, "Message"); message;
838 message = xmlnode_get_next_twin(message)) {
839 const char *session_id;
841 session_id = xmlnode_get_attrib(message, "SessionID");
842 if (!session_id) {
843 purple_debug_error("MSN log parse",
844 "Error parsing message: %s\n", "SessionID missing");
845 continue;
848 if (strcmp(session_id, old_session_id)) {
850 * The session ID differs from the last message.
851 * Thus, this is the start of a new conversation.
853 struct tm *tm;
854 time_t stamp;
855 PurpleLog *log;
857 data = g_new0(struct msn_logger_data, 1);
858 data->root = root;
859 data->message = message;
860 data->session_id = session_id;
861 data->text = NULL;
862 data->last_log = FALSE;
864 stamp = msn_logger_parse_timestamp(message, &tm);
866 log = purple_log_new(PURPLE_LOG_IM, sn, account, NULL, stamp, tm);
867 log->logger = msn_logger;
868 log->logger_data = data;
870 list = g_list_prepend(list, log);
872 old_session_id = session_id;
875 if (data)
876 data->last_log = TRUE;
878 return g_list_reverse(list);
881 static char * msn_logger_read (PurpleLog *log, PurpleLogReadFlags *flags)
883 struct msn_logger_data *data;
884 GString *text = NULL;
885 xmlnode *message;
887 if (flags != NULL)
888 *flags = PURPLE_LOG_READ_NO_NEWLINE;
889 g_return_val_if_fail(log != NULL, g_strdup(""));
891 data = log->logger_data;
893 if (data->text) {
894 /* The GTK code which displays the logs g_free()s whatever is
895 * returned from this function. Thus, we can't reuse the str
896 * part of the GString. The only solution is to free it and
897 * start over.
899 g_string_free(data->text, FALSE);
902 text = g_string_new("");
904 if (!data->root || !data->message || !data->session_id) {
905 /* Something isn't allocated correctly. */
906 purple_debug_error("MSN log parse",
907 "Error parsing message: %s\n", "Internal variables inconsistent");
908 data->text = text;
910 return text->str;
913 for (message = data->message; message;
914 message = xmlnode_get_next_twin(message)) {
916 const char *new_session_id;
917 xmlnode *text_node;
918 const char *from_name = NULL;
919 const char *to_name = NULL;
920 xmlnode *from;
921 xmlnode *to;
922 enum name_guesses name_guessed = NAME_GUESS_UNKNOWN;
923 const char *their_name;
924 time_t time_unix;
925 struct tm *tm;
926 char *timestamp;
927 char *tmp;
928 const char *style;
930 new_session_id = xmlnode_get_attrib(message, "SessionID");
932 /* If this triggers, something is wrong with the XML. */
933 if (!new_session_id) {
934 purple_debug_error("MSN log parse",
935 "Error parsing message: %s\n", "New SessionID missing");
936 break;
939 if (strcmp(new_session_id, data->session_id)) {
940 /* The session ID differs from the first message.
941 * Thus, this is the start of a new conversation.
943 break;
946 text_node = xmlnode_get_child(message, "Text");
947 if (!text_node)
948 continue;
950 from = xmlnode_get_child(message, "From");
951 if (from) {
952 xmlnode *user = xmlnode_get_child(from, "User");
954 if (user) {
955 from_name = xmlnode_get_attrib(user, "FriendlyName");
957 /* This saves a check later. */
958 if (!*from_name)
959 from_name = NULL;
963 to = xmlnode_get_child(message, "To");
964 if (to) {
965 xmlnode *user = xmlnode_get_child(to, "User");
966 if (user) {
967 to_name = xmlnode_get_attrib(user, "FriendlyName");
969 /* This saves a check later. */
970 if (!*to_name)
971 to_name = NULL;
975 their_name = from_name;
976 if (from_name && purple_prefs_get_bool("/plugins/core/log_reader/use_name_heuristics")) {
977 const char *friendly_name = purple_connection_get_display_name(log->account->gc);
979 if (friendly_name != NULL) {
980 int friendly_name_length = strlen(friendly_name);
981 const char *alias;
982 int alias_length;
983 PurpleBuddy *buddy = purple_find_buddy(log->account, log->name);
984 gboolean from_name_matches;
985 gboolean to_name_matches;
987 if (buddy)
988 their_name = purple_buddy_get_alias(buddy);
990 if (log->account->alias)
992 alias = log->account->alias;
993 alias_length = strlen(alias);
995 else
997 alias = "";
998 alias_length = 0;
1001 /* Try to guess which user is me.
1002 * The first step is to determine if either of the names matches either my
1003 * friendly name or alias. For this test, "match" is defined as:
1004 * ^(friendly_name|alias)([^a-zA-Z0-9].*)?$
1006 from_name_matches = (purple_str_has_prefix(from_name, friendly_name) &&
1007 !isalnum(*(from_name + friendly_name_length))) ||
1008 (purple_str_has_prefix(from_name, alias) &&
1009 !isalnum(*(from_name + alias_length)));
1011 to_name_matches = to_name != NULL && (
1012 (purple_str_has_prefix(to_name, friendly_name) &&
1013 !isalnum(*(to_name + friendly_name_length))) ||
1014 (purple_str_has_prefix(to_name, alias) &&
1015 !isalnum(*(to_name + alias_length))));
1017 if (from_name_matches) {
1018 if (!to_name_matches) {
1019 name_guessed = NAME_GUESS_ME;
1021 } else if (to_name_matches) {
1022 name_guessed = NAME_GUESS_THEM;
1023 } else {
1024 if (buddy) {
1025 const char *server_alias = NULL;
1026 char *alias = g_strdup(purple_buddy_get_alias(buddy));
1027 char *temp;
1029 /* "Truncate" the string at the first non-alphanumeric
1030 * character. The idea is to relax the comparison.
1032 for (temp = alias; *temp ; temp++) {
1033 if (!isalnum(*temp)) {
1034 *temp = '\0';
1035 break;
1038 alias_length = strlen(alias);
1040 /* Try to guess which user is them.
1041 * The first step is to determine if either of the names
1042 * matches their alias. For this test, "match" is
1043 * defined as: ^alias([^a-zA-Z0-9].*)?$
1045 from_name_matches = (purple_str_has_prefix(
1046 from_name, alias) &&
1047 !isalnum(*(from_name +
1048 alias_length)));
1050 to_name_matches = to_name && (purple_str_has_prefix(
1051 to_name, alias) &&
1052 !isalnum(*(to_name +
1053 alias_length)));
1055 g_free(alias);
1057 if (from_name_matches) {
1058 if (!to_name_matches) {
1059 name_guessed = NAME_GUESS_THEM;
1061 } else if (to_name_matches) {
1062 name_guessed = NAME_GUESS_ME;
1063 } else if ((server_alias = purple_buddy_get_server_alias(buddy))) {
1064 friendly_name_length =
1065 strlen(server_alias);
1067 /* Try to guess which user is them.
1068 * The first step is to determine if either of
1069 * the names matches their friendly name. For
1070 * this test, "match" is defined as:
1071 * ^friendly_name([^a-zA-Z0-9].*)?$
1073 from_name_matches = (purple_str_has_prefix(
1074 from_name,
1075 server_alias) &&
1076 !isalnum(*(from_name +
1077 friendly_name_length)));
1079 to_name_matches = to_name && (
1080 (purple_str_has_prefix(
1081 to_name, server_alias) &&
1082 !isalnum(*(to_name +
1083 friendly_name_length))));
1085 if (from_name_matches) {
1086 if (!to_name_matches) {
1087 name_guessed = NAME_GUESS_THEM;
1089 } else if (to_name_matches) {
1090 name_guessed = NAME_GUESS_ME;
1098 if (name_guessed != NAME_GUESS_UNKNOWN) {
1099 text = g_string_append(text, "<span style=\"color: #");
1100 if (name_guessed == NAME_GUESS_ME)
1101 text = g_string_append(text, "16569E");
1102 else
1103 text = g_string_append(text, "A82F2F");
1104 text = g_string_append(text, ";\">");
1107 time_unix = msn_logger_parse_timestamp(message, &tm);
1109 timestamp = g_strdup_printf("<font size=\"2\">(%02u:%02u:%02u)</font> ",
1110 tm->tm_hour, tm->tm_min, tm->tm_sec);
1111 text = g_string_append(text, timestamp);
1112 g_free(timestamp);
1114 if (from_name) {
1115 text = g_string_append(text, "<b>");
1117 if (name_guessed == NAME_GUESS_ME) {
1118 if (log->account->alias)
1119 text = g_string_append(text, log->account->alias);
1120 else
1121 text = g_string_append(text, log->account->username);
1123 else if (name_guessed == NAME_GUESS_THEM)
1124 text = g_string_append(text, their_name);
1125 else
1126 text = g_string_append(text, from_name);
1128 text = g_string_append(text, ":</b> ");
1131 if (name_guessed != NAME_GUESS_UNKNOWN)
1132 text = g_string_append(text, "</span>");
1134 style = xmlnode_get_attrib(text_node, "Style");
1136 tmp = xmlnode_get_data(text_node);
1137 if (style && *style) {
1138 text = g_string_append(text, "<span style=\"");
1139 text = g_string_append(text, style);
1140 text = g_string_append(text, "\">");
1141 text = g_string_append(text, tmp);
1142 text = g_string_append(text, "</span><br>");
1143 } else {
1144 text = g_string_append(text, tmp);
1145 text = g_string_append(text, "<br>");
1147 g_free(tmp);
1150 data->text = text;
1152 return text->str;
1155 static int msn_logger_size (PurpleLog *log)
1157 char *text;
1158 size_t size;
1160 g_return_val_if_fail(log != NULL, 0);
1162 if (purple_prefs_get_bool("/plugins/core/log_reader/fast_sizes"))
1163 return 0;
1165 text = msn_logger_read(log, NULL);
1166 size = strlen(text);
1167 g_free(text);
1169 return size;
1172 static void msn_logger_finalize(PurpleLog *log)
1174 struct msn_logger_data *data;
1176 g_return_if_fail(log != NULL);
1178 data = log->logger_data;
1180 if (data->last_log)
1181 xmlnode_free(data->root);
1183 if (data->text)
1184 g_string_free(data->text, FALSE);
1186 g_free(data);
1190 /*****************************************************************************
1191 * Trillian Logger *
1192 *****************************************************************************/
1194 /* The trillian logger doesn't write logs, only reads them. This is to include
1195 * Trillian logs in the log viewer transparently.
1198 static PurpleLogLogger *trillian_logger;
1199 static void trillian_logger_finalize(PurpleLog *log);
1201 struct trillian_logger_data {
1202 char *path; /* FIXME: Change this to use PurpleStringref like log.c:old_logger_list */
1203 int offset;
1204 int length;
1205 char *their_nickname;
1208 static GList *trillian_logger_list(PurpleLogType type, const char *sn, PurpleAccount *account)
1210 GList *list = NULL;
1211 const char *logdir;
1212 PurplePlugin *plugin;
1213 PurplePluginProtocolInfo *prpl_info;
1214 char *prpl_name;
1215 const char *buddy_name;
1216 char *filename;
1217 char *path;
1218 GError *error = NULL;
1219 gchar *contents = NULL;
1220 gsize length;
1221 gchar *line;
1222 gchar *c;
1224 g_return_val_if_fail(sn != NULL, NULL);
1225 g_return_val_if_fail(account != NULL, NULL);
1227 logdir = purple_prefs_get_string("/plugins/core/log_reader/trillian/log_directory");
1229 /* By clearing the log directory path, this logger can be (effectively) disabled. */
1230 if (!logdir || !*logdir)
1231 return NULL;
1233 plugin = purple_find_prpl(purple_account_get_protocol_id(account));
1234 if (!plugin)
1235 return NULL;
1237 prpl_info = PURPLE_PLUGIN_PROTOCOL_INFO(plugin);
1238 if (!prpl_info->list_icon)
1239 return NULL;
1241 prpl_name = g_ascii_strup(prpl_info->list_icon(account, NULL), -1);
1243 buddy_name = purple_normalize(account, sn);
1245 filename = g_strdup_printf("%s.log", buddy_name);
1246 path = g_build_filename(
1247 logdir, prpl_name, filename, NULL);
1249 purple_debug_info("Trillian log list", "Reading %s\n", path);
1250 /* FIXME: There's really no need to read the entire file at once.
1251 * See src/log.c:old_logger_list for a better approach.
1253 if (!g_file_get_contents(path, &contents, &length, &error)) {
1254 if (error) {
1255 g_error_free(error);
1256 error = NULL;
1258 g_free(path);
1260 path = g_build_filename(
1261 logdir, prpl_name, "Query", filename, NULL);
1262 purple_debug_info("Trillian log list", "Reading %s\n", path);
1263 if (!g_file_get_contents(path, &contents, &length, &error)) {
1264 if (error)
1265 g_error_free(error);
1268 g_free(filename);
1270 if (contents) {
1271 struct trillian_logger_data *data = NULL;
1272 int offset = 0;
1273 int last_line_offset = 0;
1275 line = contents;
1276 c = contents;
1277 while (*c) {
1278 offset++;
1280 if (*c != '\n') {
1281 c++;
1282 continue;
1285 *c = '\0';
1286 if (purple_str_has_prefix(line, "Session Close ")) {
1287 if (data && !data->length) {
1288 if (!(data->length = last_line_offset - data->offset)) {
1289 /* This log had no data, so we remove it. */
1290 GList *last = g_list_last(list);
1292 purple_debug_info("Trillian log list",
1293 "Empty log. Offset %i\n", data->offset);
1295 trillian_logger_finalize((PurpleLog *)last->data);
1296 list = g_list_delete_link(list, last);
1299 } else if (line[0] && line[1] && line[2] &&
1300 purple_str_has_prefix(&line[3], "sion Start ")) {
1301 /* The conditional is to make sure we're not reading off
1302 * the end of the string. We don't want strlen(), as that'd
1303 * have to count the whole string needlessly.
1305 * The odd check here is because a Session Start at the
1306 * beginning of the file can be overwritten with a UTF-8
1307 * byte order mark. Yes, it's weird.
1309 char *their_nickname = line;
1310 char *timestamp;
1312 if (data && !data->length)
1313 data->length = last_line_offset - data->offset;
1315 while (*their_nickname && (*their_nickname != ':'))
1316 their_nickname++;
1317 their_nickname++;
1319 /* This code actually has nothing to do with
1320 * the timestamp YET. I'm simply using this
1321 * variable for now to NUL-terminate the
1322 * their_nickname string.
1324 timestamp = their_nickname;
1325 while (*timestamp && *timestamp != ')')
1326 timestamp++;
1328 if (*timestamp == ')') {
1329 char *month;
1330 struct tm tm;
1332 *timestamp = '\0';
1333 if (line[0] && line[1] && line[2])
1334 timestamp += 3;
1336 /* Now we start dealing with the timestamp. */
1338 /* Skip over the day name. */
1339 while (*timestamp && (*timestamp != ' '))
1340 timestamp++;
1341 *timestamp = '\0';
1342 timestamp++;
1344 /* Parse out the month. */
1345 month = timestamp;
1346 while (*timestamp && (*timestamp != ' '))
1347 timestamp++;
1348 *timestamp = '\0';
1349 timestamp++;
1351 /* Parse the day, time, and year. */
1352 if (sscanf(timestamp, "%u %u:%u:%u %u",
1353 &tm.tm_mday, &tm.tm_hour,
1354 &tm.tm_min, &tm.tm_sec,
1355 &tm.tm_year) != 5) {
1357 purple_debug_error("Trillian log timestamp parse",
1358 "Session Start parsing error\n");
1359 } else {
1360 PurpleLog *log;
1362 tm.tm_year -= 1900;
1364 /* Let the C library deal with
1365 * daylight savings time.
1367 tm.tm_isdst = -1;
1368 tm.tm_mon = get_month(month);
1370 data = g_new0(
1371 struct trillian_logger_data, 1);
1372 data->path = g_strdup(path);
1373 data->offset = offset;
1374 data->length = 0;
1375 data->their_nickname =
1376 g_strdup(their_nickname);
1378 /* XXX: Look into this later... Should we pass in a struct tm? */
1379 log = purple_log_new(PURPLE_LOG_IM,
1380 sn, account, NULL, mktime(&tm), NULL);
1381 log->logger = trillian_logger;
1382 log->logger_data = data;
1384 list = g_list_prepend(list, log);
1388 c++;
1389 line = c;
1390 last_line_offset = offset;
1393 g_free(contents);
1395 g_free(path);
1397 g_free(prpl_name);
1399 return g_list_reverse(list);
1402 static char * trillian_logger_read (PurpleLog *log, PurpleLogReadFlags *flags)
1404 struct trillian_logger_data *data;
1405 char *read;
1406 FILE *file;
1407 PurpleBuddy *buddy;
1408 char *escaped;
1409 GString *formatted;
1410 char *c;
1411 const char *line;
1413 if (flags != NULL)
1414 *flags = PURPLE_LOG_READ_NO_NEWLINE;
1416 g_return_val_if_fail(log != NULL, g_strdup(""));
1418 data = log->logger_data;
1420 g_return_val_if_fail(data->path != NULL, g_strdup(""));
1421 g_return_val_if_fail(data->length > 0, g_strdup(""));
1422 g_return_val_if_fail(data->their_nickname != NULL, g_strdup(""));
1424 purple_debug_info("Trillian log read", "Reading %s\n", data->path);
1426 read = g_malloc(data->length + 2);
1428 file = g_fopen(data->path, "rb");
1429 fseek(file, data->offset, SEEK_SET);
1430 data->length = fread(read, 1, data->length, file);
1431 fclose(file);
1433 if (read[data->length-1] == '\n') {
1434 read[data->length] = '\0';
1435 } else {
1436 read[data->length] = '\n';
1437 read[data->length+1] = '\0';
1440 /* Load miscellaneous data. */
1441 buddy = purple_find_buddy(log->account, log->name);
1443 escaped = g_markup_escape_text(read, -1);
1444 g_free(read);
1445 read = escaped;
1447 /* Apply formatting... */
1448 formatted = g_string_sized_new(strlen(read));
1449 c = read;
1450 line = read;
1451 while (c)
1453 const char *link;
1454 const char *footer = NULL;
1455 GString *temp = NULL;
1457 if ((c = strstr(c, "\n")))
1459 *c = '\0';
1460 c++;
1463 /* Convert links.
1465 * The format is (Link: URL)URL
1466 * So, I want to find each occurance of "(Link: " and replace that chunk with:
1467 * <a href="
1468 * Then, replace the next ")" with:
1469 * ">
1470 * Then, replace the next " " (or add this if the end-of-line is reached) with:
1471 * </a>
1473 * As implemented, this isn't perfect, but it should cover common cases.
1475 while (line && (link = strstr(line, "(Link: ")))
1477 const char *tmp = link;
1479 link += 7;
1480 if (*link)
1482 char *end_paren;
1483 char *space;
1485 if (!(end_paren = strstr(link, ")")))
1487 /* Something is not as we expect. Bail out. */
1488 break;
1491 if (!temp)
1492 temp = g_string_sized_new(c ? (c - 1 - line) : strlen(line));
1494 g_string_append_len(temp, line, (tmp - line));
1496 /* Start an <a> tag. */
1497 g_string_append(temp, "<a href=\"");
1499 /* Append up to the ) */
1500 g_string_append_len(temp, link, end_paren - link);
1502 /* Finish the <a> tag. */
1503 g_string_append(temp, "\">");
1505 /* The \r is a bit of a hack to keep there from being a \r in
1506 * the link text, which may not matter. */
1507 if ((space = strstr(end_paren, " ")) || (space = strstr(end_paren, "\r")))
1509 g_string_append_len(temp, end_paren + 1, space - end_paren - 1);
1511 /* Close the <a> tag. */
1512 g_string_append(temp, "</a>");
1514 space++;
1516 else
1518 /* There is no space before the end of the line. */
1519 g_string_append(temp, end_paren + 1);
1520 /* Close the <a> tag. */
1521 g_string_append(temp, "</a>");
1523 line = space;
1525 else
1527 /* Something is not as we expect. Bail out. */
1528 break;
1532 if (temp)
1534 if (line)
1535 g_string_append(temp, line);
1536 line = temp->str;
1539 if (*line == '[') {
1540 const char *timestamp;
1542 if ((timestamp = strstr(line, "]"))) {
1543 line++;
1544 /* TODO: Parse the timestamp and convert it to Purple's format. */
1545 g_string_append(formatted, "<font size=\"2\">(");
1546 g_string_append_len(formatted, line, (timestamp - line));
1547 g_string_append(formatted,")</font> ");
1548 line = timestamp + 1;
1549 if (line[0] && line[1])
1550 line++;
1553 if (purple_str_has_prefix(line, "*** ")) {
1554 line += (sizeof("*** ") - 1);
1555 g_string_append(formatted, "<b>");
1556 footer = "</b>";
1557 if (purple_str_has_prefix(line, "NOTE: This user is offline.")) {
1558 line = _("User is offline.");
1559 } else if (purple_str_has_prefix(line,
1560 "NOTE: Your status is currently set to ")) {
1562 line += (sizeof("NOTE: ") - 1);
1563 } else if (purple_str_has_prefix(line, "Auto-response sent to ")) {
1564 g_string_append(formatted, _("Auto-response sent:"));
1565 while (*line && *line != ':')
1566 line++;
1567 if (*line)
1568 line++;
1569 g_string_append(formatted, "</b>");
1570 footer = NULL;
1571 } else if (strstr(line, " signed off ")) {
1572 const char *alias = NULL;
1574 if (buddy != NULL)
1575 alias = purple_buddy_get_alias(buddy);
1577 if (alias != NULL) {
1578 g_string_append_printf(formatted,
1579 _("%s has signed off."), alias);
1580 } else {
1581 g_string_append_printf(formatted,
1582 _("%s has signed off."), log->name);
1584 line = "";
1585 } else if (strstr(line, " signed on ")) {
1586 const char *alias = NULL;
1588 if (buddy != NULL)
1589 alias = purple_buddy_get_alias(buddy);
1591 if (alias != NULL)
1592 g_string_append(formatted, alias);
1593 else
1594 g_string_append(formatted, log->name);
1596 line = " logged in.";
1597 } else if (purple_str_has_prefix(line,
1598 "One or more messages may have been undeliverable.")) {
1600 g_string_append(formatted,
1601 "<span style=\"color: #ff0000;\">");
1602 g_string_append(formatted,
1603 _("One or more messages may have been "
1604 "undeliverable."));
1605 line = "";
1606 footer = "</span></b>";
1607 } else if (purple_str_has_prefix(line,
1608 "You have been disconnected.")) {
1610 g_string_append(formatted,
1611 "<span style=\"color: #ff0000;\">");
1612 g_string_append(formatted,
1613 _("You were disconnected from the server."));
1614 line = "";
1615 footer = "</span></b>";
1616 } else if (purple_str_has_prefix(line,
1617 "You are currently disconnected.")) {
1619 g_string_append(formatted,
1620 "<span style=\"color: #ff0000;\">");
1621 line = _("You are currently disconnected. Messages "
1622 "will not be received unless you are "
1623 "logged in.");
1624 footer = "</span></b>";
1625 } else if (purple_str_has_prefix(line,
1626 "Your previous message has not been sent.")) {
1628 g_string_append(formatted,
1629 "<span style=\"color: #ff0000;\">");
1631 if (purple_str_has_prefix(line,
1632 "Your previous message has not been sent. "
1633 "Reason: Maximum length exceeded.")) {
1635 g_string_append(formatted,
1636 _("Message could not be sent because "
1637 "the maximum length was exceeded."));
1638 line = "";
1639 } else {
1640 g_string_append(formatted,
1641 _("Message could not be sent."));
1642 line += (sizeof(
1643 "Your previous message "
1644 "has not been sent. ") - 1);
1647 footer = "</span></b>";
1649 } else if (purple_str_has_prefix(line, data->their_nickname)) {
1650 if (buddy != NULL) {
1651 const char *alias = purple_buddy_get_alias(buddy);
1653 if (alias != NULL) {
1654 line += strlen(data->their_nickname) + 2;
1655 g_string_append_printf(formatted,
1656 "<span style=\"color: #A82F2F;\">"
1657 "<b>%s</b></span>: ", alias);
1660 } else {
1661 const char *line2 = strstr(line, ":");
1662 if (line2) {
1663 const char *acct_name;
1664 line2++;
1665 line = line2;
1666 acct_name = purple_account_get_alias(log->account);
1667 if (!acct_name)
1668 acct_name = purple_account_get_username(log->account);
1670 g_string_append_printf(formatted,
1671 "<span style=\"color: #16569E;\">"
1672 "<b>%s</b></span>:", acct_name);
1677 g_string_append(formatted, line);
1679 line = c;
1680 if (temp)
1681 g_string_free(temp, TRUE);
1683 if (footer)
1684 g_string_append(formatted, footer);
1686 g_string_append(formatted, "<br>");
1689 g_free(read);
1691 /* XXX: TODO: What can we do about removing \r characters?
1692 * XXX: TODO: and will that allow us to avoid this
1693 * XXX: TODO: g_strchomp(), or is that unrelated? */
1694 /* XXX: TODO: Avoid this g_strchomp() */
1695 return g_strchomp(g_string_free(formatted, FALSE));
1698 static int trillian_logger_size (PurpleLog *log)
1700 struct trillian_logger_data *data;
1701 char *text;
1702 size_t size;
1704 g_return_val_if_fail(log != NULL, 0);
1706 data = log->logger_data;
1708 if (purple_prefs_get_bool("/plugins/core/log_reader/fast_sizes")) {
1709 return data ? data->length : 0;
1712 text = trillian_logger_read(log, NULL);
1713 size = strlen(text);
1714 g_free(text);
1716 return size;
1719 static void trillian_logger_finalize(PurpleLog *log)
1721 struct trillian_logger_data *data;
1723 g_return_if_fail(log != NULL);
1725 data = log->logger_data;
1727 g_free(data->path);
1728 g_free(data->their_nickname);
1729 g_free(data);
1732 /*****************************************************************************
1733 * QIP Logger *
1734 *****************************************************************************/
1736 /* The QIP logger doesn't write logs, only reads them. This is to include
1737 * QIP logs in the log viewer transparently.
1739 #define QIP_LOG_DELIMITER "--------------------------------------"
1740 #define QIP_LOG_IN_MESSAGE (QIP_LOG_DELIMITER "<-")
1741 #define QIP_LOG_OUT_MESSAGE (QIP_LOG_DELIMITER ">-")
1742 #define QIP_LOG_IN_MESSAGE_ESC (QIP_LOG_DELIMITER "&lt;-")
1743 #define QIP_LOG_OUT_MESSAGE_ESC (QIP_LOG_DELIMITER "&gt;-")
1744 #define QIP_LOG_TIMEOUT (60*60)
1746 static PurpleLogLogger *qip_logger;
1748 struct qip_logger_data {
1750 char *path; /* FIXME: Change this to use PurpleStringref like log.c:old_logger_list */
1751 int offset;
1752 int length;
1755 static GList *qip_logger_list(PurpleLogType type, const char *sn, PurpleAccount *account)
1757 GList *list = NULL;
1758 const char *logdir;
1759 PurplePlugin *plugin;
1760 PurplePluginProtocolInfo *prpl_info;
1761 char *username;
1762 char *filename;
1763 char *path;
1764 char *contents;
1765 struct qip_logger_data *data = NULL;
1766 struct tm prev_tm;
1767 struct tm tm;
1768 gboolean prev_tm_init = FALSE;
1769 gboolean main_cycle = TRUE;
1770 char *c;
1771 char *start_log;
1772 char *new_line = NULL;
1773 int offset = 0;
1774 GError *error;
1776 g_return_val_if_fail(sn != NULL, NULL);
1777 g_return_val_if_fail(account != NULL, NULL);
1779 /* QIP only supports ICQ. */
1780 if (strcmp(account->protocol_id, "prpl-icq"))
1781 return NULL;
1783 logdir = purple_prefs_get_string("/plugins/core/log_reader/qip/log_directory");
1785 /* By clearing the log directory path, this logger can be (effectively) disabled. */
1786 if (!logdir || !*logdir)
1787 return NULL;
1789 plugin = purple_find_prpl(purple_account_get_protocol_id(account));
1790 if (!plugin)
1791 return NULL;
1793 prpl_info = PURPLE_PLUGIN_PROTOCOL_INFO(plugin);
1794 if (!prpl_info->list_icon)
1795 return NULL;
1797 username = g_strdup(purple_normalize(account, account->username));
1798 filename = g_strdup_printf("%s.txt", purple_normalize(account, sn));
1799 path = g_build_filename(logdir, username, "History", filename, NULL);
1800 g_free(username);
1801 g_free(filename);
1803 purple_debug_info("QIP logger", "Reading %s\n", path);
1805 error = NULL;
1806 if (!g_file_get_contents(path, &contents, NULL, &error)) {
1807 purple_debug_error("QIP logger",
1808 "Couldn't read file %s: %s \n", path,
1809 (error && error->message) ? error->message : "Unknown error");
1810 if (error)
1811 g_error_free(error);
1812 g_free(path);
1813 return list;
1816 c = contents;
1817 start_log = contents;
1818 while (main_cycle) {
1820 gboolean add_new_log = FALSE;
1822 if (*c) {
1823 if (purple_str_has_prefix(c, QIP_LOG_IN_MESSAGE) ||
1824 purple_str_has_prefix(c, QIP_LOG_OUT_MESSAGE)) {
1826 char *tmp;
1828 new_line = c;
1830 /* find EOL */
1831 c = strstr(c, "\n");
1832 c++;
1834 /* Find the last '(' character. */
1835 if ((tmp = strstr(c, "\n")) != NULL) {
1836 while (*tmp && *tmp != '(') --tmp;
1837 c = tmp;
1838 } else {
1839 while (*c)
1840 c++;
1841 c--;
1842 c = g_strrstr(c, "(");
1845 if (c != NULL) {
1846 const char *timestamp = ++c;
1848 /* Parse the time, day, month and year */
1849 if (sscanf(timestamp, "%u:%u:%u %u/%u/%u",
1850 &tm.tm_hour, &tm.tm_min, &tm.tm_sec,
1851 &tm.tm_mday, &tm.tm_mon, &tm.tm_year) != 6) {
1853 purple_debug_error("QIP logger list",
1854 "Parsing timestamp error\n");
1855 } else {
1856 tm.tm_mon -= 1;
1857 tm.tm_year -= 1900;
1859 /* Let the C library deal with
1860 * daylight savings time. */
1861 tm.tm_isdst = -1;
1863 if (!prev_tm_init) {
1864 prev_tm = tm;
1865 prev_tm_init = TRUE;
1866 } else {
1867 add_new_log = difftime(mktime(&tm), mktime(&prev_tm)) > QIP_LOG_TIMEOUT;
1872 } else {
1873 add_new_log = TRUE;
1874 main_cycle = FALSE;
1875 new_line = c;
1878 /* adding log */
1879 if (add_new_log && prev_tm_init) {
1880 PurpleLog *log;
1882 /* filling data */
1883 data = g_new0(struct qip_logger_data, 1);
1884 data->path = g_strdup(path);
1885 data->length = new_line - start_log;
1886 data->offset = offset;
1887 offset += data->length;
1888 purple_debug_info("QIP logger list",
1889 "Creating log: path = (%s); length = (%d); offset = (%d)\n",
1890 data->path, data->length, data->offset);
1892 /* XXX: Look into this later... Should we pass in a struct tm? */
1893 log = purple_log_new(PURPLE_LOG_IM, sn, account,
1894 NULL, mktime(&prev_tm), NULL);
1896 log->logger = qip_logger;
1897 log->logger_data = data;
1899 list = g_list_prepend(list, log);
1901 prev_tm = tm;
1902 start_log = new_line;
1905 if (*c) {
1906 /* find EOF */
1907 c = strstr(c, "\n");
1908 c++;
1912 g_free(contents);
1913 g_free(path);
1914 return g_list_reverse(list);
1917 static char *qip_logger_read(PurpleLog *log, PurpleLogReadFlags *flags)
1919 struct qip_logger_data *data;
1920 PurpleBuddy *buddy;
1921 GString *formatted;
1922 char *c;
1923 const char *line;
1924 gchar *contents;
1925 GError *error;
1926 char *utf8_string;
1927 FILE *file;
1929 if (flags != NULL)
1930 *flags = PURPLE_LOG_READ_NO_NEWLINE;
1932 g_return_val_if_fail(log != NULL, g_strdup(""));
1934 data = log->logger_data;
1936 g_return_val_if_fail(data->path != NULL, g_strdup(""));
1937 g_return_val_if_fail(data->length > 0, g_strdup(""));
1939 file = g_fopen(data->path, "rb");
1940 g_return_val_if_fail(file != NULL, g_strdup(""));
1942 contents = g_malloc(data->length + 2);
1944 fseek(file, data->offset, SEEK_SET);
1945 data->length = fread(contents, 1, data->length, file);
1946 fclose(file);
1948 contents[data->length] = '\n';
1949 contents[data->length + 1] = '\0';
1951 /* Convert file contents from Cp1251 to UTF-8 codeset */
1952 error = NULL;
1953 if (!(utf8_string = g_convert(contents, -1, "UTF-8", "Cp1251", NULL, NULL, &error))) {
1954 purple_debug_error("QIP logger",
1955 "Couldn't convert file %s to UTF-8: %s\n", data->path,
1956 (error && error->message) ? error->message : "Unknown error");
1957 if (error)
1958 g_error_free(error);
1959 g_free(contents);
1960 return g_strdup("");
1963 g_free(contents);
1964 contents = g_markup_escape_text(utf8_string, -1);
1965 g_free(utf8_string);
1967 buddy = purple_find_buddy(log->account, log->name);
1969 /* Apply formatting... */
1970 formatted = g_string_sized_new(data->length + 2);
1971 c = contents;
1972 line = contents;
1974 while (c && *c) {
1975 gboolean is_in_message = FALSE;
1977 if (purple_str_has_prefix(line, QIP_LOG_IN_MESSAGE_ESC) ||
1978 purple_str_has_prefix(line, QIP_LOG_OUT_MESSAGE_ESC)) {
1980 char *tmp;
1981 const char *buddy_name;
1983 is_in_message = purple_str_has_prefix(line, QIP_LOG_IN_MESSAGE_ESC);
1985 /* find EOL */
1986 c = strstr(c, "\n");
1988 /* XXX: Do we need buddy_name when we have buddy->alias? */
1989 buddy_name = ++c;
1991 /* Find the last '(' character. */
1992 if ((tmp = strstr(c, "\n")) != NULL) {
1993 while (*tmp && *tmp != '(') --tmp;
1994 c = tmp;
1995 } else {
1996 while (*c)
1997 c++;
1998 c--;
1999 c = g_strrstr(c, "(");
2002 if (c != NULL) {
2003 const char *timestamp = c;
2004 int hour;
2005 int min;
2006 int sec;
2008 timestamp++;
2010 /* Parse the time, day, month and year */
2011 if (sscanf(timestamp, "%u:%u:%u",
2012 &hour, &min, &sec) != 3) {
2013 purple_debug_error("QIP logger read",
2014 "Parsing timestamp error\n");
2015 } else {
2016 g_string_append(formatted, "<font size=\"2\">");
2017 /* TODO: Figure out if we can do anything more locale-independent. */
2018 g_string_append_printf(formatted,
2019 "(%u:%02u:%02u) %cM ", hour % 12,
2020 min, sec, (hour >= 12) ? 'P': 'A');
2021 g_string_append(formatted, "</font> ");
2023 if (is_in_message) {
2024 const char *alias = NULL;
2026 if (buddy_name != NULL && buddy != NULL &&
2027 (alias = purple_buddy_get_alias(buddy)))
2029 g_string_append_printf(formatted,
2030 "<span style=\"color: #A82F2F;\">"
2031 "<b>%s</b></span>: ", alias);
2033 } else {
2034 const char *acct_name;
2035 acct_name = purple_account_get_alias(log->account);
2036 if (!acct_name)
2037 acct_name = purple_account_get_username(log->account);
2039 g_string_append_printf(formatted,
2040 "<span style=\"color: #16569E;\">"
2041 "<b>%s</b></span>: ", acct_name);
2044 /* find EOF */
2045 c = strstr(c, "\n");
2046 line = ++c;
2049 } else {
2050 if ((c = strstr(c, "\n")))
2051 *c = '\0';
2053 if (line[0] != '\n' && line[0] != '\r') {
2055 g_string_append(formatted, line);
2056 g_string_append(formatted, "<br>");
2059 if (c)
2060 line = ++c;
2063 g_free(contents);
2065 /* XXX: TODO: Avoid this g_strchomp() */
2066 return g_strchomp(g_string_free(formatted, FALSE));
2069 static int qip_logger_size (PurpleLog *log)
2071 struct qip_logger_data *data;
2072 char *text;
2073 size_t size;
2075 g_return_val_if_fail(log != NULL, 0);
2077 data = log->logger_data;
2079 if (purple_prefs_get_bool("/plugins/core/log_reader/fast_sizes")) {
2080 return data ? data->length : 0;
2083 text = qip_logger_read(log, NULL);
2084 size = strlen(text);
2085 g_free(text);
2087 return size;
2090 static void qip_logger_finalize(PurpleLog *log)
2092 struct qip_logger_data *data;
2094 g_return_if_fail(log != NULL);
2096 data = log->logger_data;
2098 g_free(data->path);
2099 g_free(data);
2102 /*************************************************************************
2103 * aMSN Logger *
2104 *************************************************************************/
2106 /* The aMSN logger doesn't write logs, only reads them. This is to include
2107 * aMSN logs in the log viewer transparently.
2110 static PurpleLogLogger *amsn_logger;
2112 struct amsn_logger_data {
2113 char *path;
2114 int offset;
2115 int length;
2118 #define AMSN_LOG_CONV_START "|\"LRED[Conversation started on "
2119 #define AMSN_LOG_CONV_END "|\"LRED[You have closed the window on "
2120 #define AMSN_LOG_CONV_EXTRA "01 Aug 2001 00:00:00]"
2122 static GList *amsn_logger_parse_file(char *filename, const char *sn, PurpleAccount *account)
2124 GList *list = NULL;
2125 GError *error;
2126 char *contents;
2127 struct amsn_logger_data *data;
2128 PurpleLog *log;
2130 purple_debug_info("aMSN logger", "Reading %s\n", filename);
2131 error = NULL;
2132 if (!g_file_get_contents(filename, &contents, NULL, &error)) {
2133 purple_debug_error("aMSN logger",
2134 "Couldn't read file %s: %s \n", filename,
2135 (error && error->message) ?
2136 error->message : "Unknown error");
2137 if (error)
2138 g_error_free(error);
2139 } else {
2140 char *c = contents;
2141 gboolean found_start = FALSE;
2142 char *start_log = c;
2143 int offset = 0;
2144 struct tm tm;
2145 while (c && *c) {
2146 if (purple_str_has_prefix(c, AMSN_LOG_CONV_START)) {
2147 char month[4];
2148 if (sscanf(c + strlen(AMSN_LOG_CONV_START),
2149 "%u %3s %u %u:%u:%u",
2150 &tm.tm_mday, (char*)&month, &tm.tm_year,
2151 &tm.tm_hour, &tm.tm_min, &tm.tm_sec) != 6) {
2152 found_start = FALSE;
2153 purple_debug_error("aMSN logger",
2154 "Error parsing start date for %s\n",
2155 filename);
2156 } else {
2157 tm.tm_year -= 1900;
2159 /* Let the C library deal with
2160 * daylight savings time.
2162 tm.tm_isdst = -1;
2163 tm.tm_mon = get_month(month);
2165 found_start = TRUE;
2166 offset = c - contents;
2167 start_log = c;
2169 } else if (purple_str_has_prefix(c, AMSN_LOG_CONV_END) && found_start) {
2170 data = g_new0(struct amsn_logger_data, 1);
2171 data->path = g_strdup(filename);
2172 data->offset = offset;
2173 data->length = c - start_log
2174 + strlen(AMSN_LOG_CONV_END)
2175 + strlen(AMSN_LOG_CONV_EXTRA);
2176 log = purple_log_new(PURPLE_LOG_IM, sn, account, NULL, mktime(&tm), NULL);
2177 log->logger = amsn_logger;
2178 log->logger_data = data;
2179 list = g_list_prepend(list, log);
2180 found_start = FALSE;
2182 purple_debug_info("aMSN logger",
2183 "Found log for %s:"
2184 " path = (%s),"
2185 " offset = (%d),"
2186 " length = (%d)\n",
2187 sn, data->path, data->offset, data->length);
2189 c = strstr(c, "\n");
2190 c++;
2193 /* I've seen the file end without the AMSN_LOG_CONV_END bit */
2194 if (found_start) {
2195 data = g_new0(struct amsn_logger_data, 1);
2196 data->path = g_strdup(filename);
2197 data->offset = offset;
2198 data->length = c - start_log
2199 + strlen(AMSN_LOG_CONV_END)
2200 + strlen(AMSN_LOG_CONV_EXTRA);
2201 log = purple_log_new(PURPLE_LOG_IM, sn, account, NULL, mktime(&tm), NULL);
2202 log->logger = amsn_logger;
2203 log->logger_data = data;
2204 list = g_list_prepend(list, log);
2205 found_start = FALSE;
2207 purple_debug_info("aMSN logger",
2208 "Found log for %s:"
2209 " path = (%s),"
2210 " offset = (%d),"
2211 " length = (%d)\n",
2212 sn, data->path, data->offset, data->length);
2214 g_free(contents);
2217 return list;
2220 /* `log_dir`/username@hotmail.com/logs/buddyname@hotmail.com.log */
2221 /* `log_dir`/username@hotmail.com/logs/Month Year/buddyname@hotmail.com.log */
2222 static GList *amsn_logger_list(PurpleLogType type, const char *sn, PurpleAccount *account)
2224 GList *list = NULL;
2225 const char *logdir;
2226 char *username;
2227 char *log_path;
2228 char *buddy_log;
2229 char *filename;
2230 GDir *dir;
2231 const char *name;
2233 logdir = purple_prefs_get_string("/plugins/core/log_reader/amsn/log_directory");
2235 /* By clearing the log directory path, this logger can be (effectively) disabled. */
2236 if (!logdir || !*logdir)
2237 return NULL;
2239 /* aMSN only works with MSN/WLM */
2240 if (strcmp(account->protocol_id, "prpl-msn"))
2241 return NULL;
2243 username = g_strdup(purple_normalize(account, account->username));
2244 buddy_log = g_strdup_printf("%s.log", purple_normalize(account, sn));
2245 log_path = g_build_filename(logdir, username, "logs", NULL);
2247 /* First check in the top-level */
2248 filename = g_build_filename(log_path, buddy_log, NULL);
2249 if (g_file_test(filename, G_FILE_TEST_EXISTS))
2250 list = amsn_logger_parse_file(filename, sn, account);
2251 else
2252 g_free(filename);
2254 /* Check in previous months */
2255 dir = g_dir_open(log_path, 0, NULL);
2256 if (dir) {
2257 while ((name = g_dir_read_name(dir)) != NULL) {
2258 filename = g_build_filename(log_path, name, buddy_log, NULL);
2259 if (g_file_test(filename, G_FILE_TEST_EXISTS))
2260 list = g_list_concat(list, amsn_logger_parse_file(filename, sn, account));
2261 g_free(filename);
2263 g_dir_close(dir);
2266 g_free(log_path);
2268 /* New versions use 'friendlier' directory names */
2269 purple_util_chrreplace(username, '@', '_');
2270 purple_util_chrreplace(username, '.', '_');
2272 log_path = g_build_filename(logdir, username, "logs", NULL);
2274 /* First check in the top-level */
2275 filename = g_build_filename(log_path, buddy_log, NULL);
2276 if (g_file_test(filename, G_FILE_TEST_EXISTS))
2277 list = g_list_concat(list, amsn_logger_parse_file(filename, sn, account));
2278 g_free(filename);
2280 /* Check in previous months */
2281 dir = g_dir_open(log_path, 0, NULL);
2282 if (dir) {
2283 while ((name = g_dir_read_name(dir)) != NULL) {
2284 filename = g_build_filename(log_path, name, buddy_log, NULL);
2285 if (g_file_test(filename, G_FILE_TEST_EXISTS))
2286 list = g_list_concat(list, amsn_logger_parse_file(filename, sn, account));
2287 g_free(filename);
2289 g_dir_close(dir);
2292 g_free(log_path);
2293 g_free(username);
2294 g_free(buddy_log);
2296 return list;
2299 /* Really it's |"L, but the string's been escaped */
2300 #define AMSN_LOG_FORMAT_TAG "|&quot;L"
2302 static char *amsn_logger_read(PurpleLog *log, PurpleLogReadFlags *flags)
2304 struct amsn_logger_data *data;
2305 FILE *file;
2306 char *contents;
2307 char *escaped;
2308 GString *formatted;
2309 char *start;
2310 gboolean in_span = FALSE;
2312 if (flags != NULL)
2313 *flags = PURPLE_LOG_READ_NO_NEWLINE;
2315 g_return_val_if_fail(log != NULL, g_strdup(""));
2317 data = log->logger_data;
2319 g_return_val_if_fail(data->path != NULL, g_strdup(""));
2320 g_return_val_if_fail(data->length > 0, g_strdup(""));
2322 contents = g_malloc(data->length + 2);
2324 file = g_fopen(data->path, "rb");
2325 g_return_val_if_fail(file != NULL, g_strdup(""));
2327 fseek(file, data->offset, SEEK_SET);
2328 data->length = fread(contents, 1, data->length, file);
2329 fclose(file);
2331 contents[data->length] = '\n';
2332 contents[data->length + 1] = '\0';
2334 escaped = g_markup_escape_text(contents, -1);
2335 g_free(contents);
2336 contents = escaped;
2338 formatted = g_string_sized_new(data->length + 2);
2340 start = contents;
2341 while (start && *start) {
2342 char *end;
2343 char *old_tag;
2344 char *tag;
2345 end = strstr(start, "\n");
2346 if (!end)
2347 break;
2348 *end = '\0';
2349 if (purple_str_has_prefix(start, AMSN_LOG_FORMAT_TAG) && in_span) {
2350 /* New format for this line */
2351 g_string_append(formatted, "</span><br>");
2352 in_span = FALSE;
2353 } else if (start != contents) {
2354 /* Continue format from previous line */
2355 g_string_append(formatted, "<br>");
2357 old_tag = start;
2358 tag = strstr(start, AMSN_LOG_FORMAT_TAG);
2359 while (tag) {
2360 g_string_append_len(formatted, old_tag, tag - old_tag);
2361 tag += strlen(AMSN_LOG_FORMAT_TAG);
2362 if (in_span) {
2363 g_string_append(formatted, "</span>");
2364 in_span = FALSE;
2366 if (*tag == 'C') {
2367 /* |"LCxxxxxx is a hex colour */
2368 char colour[7];
2369 strncpy(colour, tag + 1, 6);
2370 colour[6] = '\0';
2371 g_string_append_printf(formatted, "<span style=\"color: #%s;\">", colour);
2372 /* This doesn't appear to work? */
2373 /* g_string_append_printf(formatted, "<span style=\"color: #%6s;\">", tag + 1); */
2374 in_span = TRUE;
2375 old_tag = tag + 7; /* C + xxxxxx */
2376 } else {
2377 /* |"Lxxx is a 3-digit colour code */
2378 if (purple_str_has_prefix(tag, "RED")) {
2379 g_string_append(formatted, "<span style=\"color: red;\">");
2380 in_span = TRUE;
2381 } else if (purple_str_has_prefix(tag, "GRA")) {
2382 g_string_append(formatted, "<span style=\"color: gray;\">");
2383 in_span = TRUE;
2384 } else if (purple_str_has_prefix(tag, "NOR")) {
2385 g_string_append(formatted, "<span style=\"color: black;\">");
2386 in_span = TRUE;
2387 } else if (purple_str_has_prefix(tag, "ITA")) {
2388 g_string_append(formatted, "<span style=\"color: blue;\">");
2389 in_span = TRUE;
2390 } else if (purple_str_has_prefix(tag, "GRE")) {
2391 g_string_append(formatted, "<span style=\"color: darkgreen;\">");
2392 in_span = TRUE;
2393 } else {
2394 purple_debug_info("aMSN logger", "Unknown colour format: %3s\n", tag);
2396 old_tag = tag + 3;
2398 tag = strstr(tag, AMSN_LOG_FORMAT_TAG);
2400 g_string_append(formatted, old_tag);
2401 start = end + 1;
2403 if (in_span)
2404 g_string_append(formatted, "</span>");
2406 g_free(contents);
2408 return g_string_free(formatted, FALSE);
2411 static int amsn_logger_size(PurpleLog *log)
2413 struct amsn_logger_data *data;
2414 char *text;
2415 int size;
2417 g_return_val_if_fail(log != NULL, 0);
2419 data = log->logger_data;
2421 if (purple_prefs_get_bool("/plugins/core/log_reader/fast_sizes")) {
2422 return data ? data->length : 0;
2425 text = amsn_logger_read(log, NULL);
2426 size = strlen(text);
2427 g_free(text);
2429 return size;
2432 static void amsn_logger_finalize(PurpleLog *log)
2434 struct amsn_logger_data *data;
2436 g_return_if_fail(log != NULL);
2438 data = log->logger_data;
2439 g_free(data->path);
2440 g_free(data);
2443 /*****************************************************************************
2444 * Plugin Code *
2445 *****************************************************************************/
2447 static void
2448 init_plugin(PurplePlugin *plugin)
2453 static void log_reader_init_prefs(void) {
2454 char *path;
2455 #ifdef _WIN32
2456 char *folder;
2457 gboolean found = FALSE;
2458 #endif
2460 purple_prefs_add_none("/plugins/core/log_reader");
2463 /* Add general preferences. */
2465 purple_prefs_add_bool("/plugins/core/log_reader/fast_sizes", FALSE);
2466 purple_prefs_add_bool("/plugins/core/log_reader/use_name_heuristics", TRUE);
2469 /* Add Adium log directory preference. */
2470 purple_prefs_add_none("/plugins/core/log_reader/adium");
2472 /* Calculate default Adium log directory. */
2473 #ifdef _WIN32
2474 purple_prefs_add_string("/plugins/core/log_reader/adium/log_directory", "");
2475 #else
2476 path = g_build_filename(purple_home_dir(), "Library", "Application Support",
2477 "Adium 2.0", "Users", "Default", "Logs", NULL);
2478 purple_prefs_add_string("/plugins/core/log_reader/adium/log_directory", path);
2479 g_free(path);
2480 #endif
2483 /* Add Fire log directory preference. */
2484 purple_prefs_add_none("/plugins/core/log_reader/fire");
2486 /* Calculate default Fire log directory. */
2487 #ifdef _WIN32
2488 purple_prefs_add_string("/plugins/core/log_reader/fire/log_directory", "");
2489 #else
2490 path = g_build_filename(purple_home_dir(), "Library", "Application Support",
2491 "Fire", "Sessions", NULL);
2492 purple_prefs_add_string("/plugins/core/log_reader/fire/log_directory", path);
2493 g_free(path);
2494 #endif
2497 /* Add Messenger Plus! log directory preference. */
2498 purple_prefs_add_none("/plugins/core/log_reader/messenger_plus");
2500 /* Calculate default Messenger Plus! log directory. */
2501 #ifdef _WIN32
2502 path = NULL;
2503 folder = wpurple_get_special_folder(CSIDL_PERSONAL);
2504 if (folder) {
2505 path = g_build_filename(folder, "My Chat Logs", NULL);
2506 g_free(folder);
2508 #else
2509 path = g_build_filename(PURPLE_LOG_READER_WINDOWS_MOUNT_POINT,
2510 "Documents and Settings", g_get_user_name(),
2511 "My Documents", "My Chat Logs", NULL);
2512 #endif
2513 purple_prefs_add_string("/plugins/core/log_reader/messenger_plus/log_directory", path ? path : "");
2514 g_free(path);
2517 /* Add MSN Messenger log directory preference. */
2518 purple_prefs_add_none("/plugins/core/log_reader/msn");
2520 /* Calculate default MSN message history directory. */
2521 #ifdef _WIN32
2522 path = NULL;
2523 folder = wpurple_get_special_folder(CSIDL_PERSONAL);
2524 if (folder) {
2525 path = g_build_filename(folder, "My Received Files", NULL);
2526 g_free(folder);
2528 #else
2529 path = g_build_filename(PURPLE_LOG_READER_WINDOWS_MOUNT_POINT,
2530 "Documents and Settings", g_get_user_name(),
2531 "My Documents", "My Received Files", NULL);
2532 #endif
2533 purple_prefs_add_string("/plugins/core/log_reader/msn/log_directory", path ? path : "");
2534 g_free(path);
2537 /* Add Trillian log directory preference. */
2538 purple_prefs_add_none("/plugins/core/log_reader/trillian");
2540 #ifdef _WIN32
2541 /* XXX: While a major hack, this is the most reliable way I could
2542 * think of to determine the Trillian installation directory.
2545 path = NULL;
2546 if ((folder = wpurple_read_reg_string(HKEY_CLASSES_ROOT, "Trillian.SkinZip\\shell\\Add\\command\\", NULL))) {
2547 char *value = folder;
2548 char *temp;
2550 /* Break apart buffer. */
2551 if (*value == '"') {
2552 value++;
2553 temp = value;
2554 while (*temp && *temp != '"')
2555 temp++;
2556 } else {
2557 temp = value;
2558 while (*temp && *temp != ' ')
2559 temp++;
2561 *temp = '\0';
2563 /* Set path. */
2564 if (purple_str_has_suffix(value, "trillian.exe")) {
2565 value[strlen(value) - (sizeof("trillian.exe") - 1)] = '\0';
2566 path = g_build_filename(value, "users", "default", "talk.ini", NULL);
2568 g_free(folder);
2571 if (!path) {
2572 char *folder = wpurple_get_special_folder(CSIDL_PROGRAM_FILES);
2573 if (folder) {
2574 path = g_build_filename(folder, "Trillian",
2575 "users", "default", "talk.ini", NULL);
2576 g_free(folder);
2580 if (path) {
2581 /* Read talk.ini file to find the log directory. */
2582 GError *error = NULL;
2584 #if 0 && GLIB_CHECK_VERSION(2,6,0) /* FIXME: Not tested yet. */
2585 GKeyFile *key_file;
2587 purple_debug_info("Trillian talk.ini read", "Reading %s\n", path);
2589 error = NULL;
2590 if (!g_key_file_load_from_file(key_file, path, G_KEY_FILE_NONE, GError &error)) {
2591 purple_debug_error("Trillian talk.ini read",
2592 "Error reading talk.ini\n");
2593 if (error)
2594 g_error_free(error);
2595 } else {
2596 char *logdir = g_key_file_get_string(key_file, "Logging", "Directory", &error);
2597 if (error) {
2598 purple_debug_error("Trillian talk.ini read",
2599 "Error reading Directory value from Logging section\n");
2600 g_error_free(error);
2603 if (logdir) {
2604 g_strchomp(logdir);
2605 purple_prefs_add_string("/plugins/core/log_reader/trillian/log_directory", logdir);
2606 found = TRUE;
2609 g_key_file_free(key_file);
2611 #else /* !GLIB_CHECK_VERSION(2,6,0) */
2612 gchar *contents = NULL;
2614 purple_debug_info("Trillian talk.ini read",
2615 "Reading %s\n", path);
2616 if (!g_file_get_contents(path, &contents, NULL, &error)) {
2617 purple_debug_error("Trillian talk.ini read",
2618 "Error reading talk.ini: %s\n",
2619 (error && error->message) ? error->message : "Unknown error");
2620 if (error)
2621 g_error_free(error);
2622 } else {
2623 char *cursor, *line;
2624 line = cursor = contents;
2625 while (*cursor) {
2626 if (*cursor == '\n') {
2627 *cursor = '\0';
2629 /* XXX: This assumes the first Directory key is under [Logging]. */
2630 if (purple_str_has_prefix(line, "Directory=")) {
2631 line += (sizeof("Directory=") - 1);
2632 g_strchomp(line);
2633 purple_prefs_add_string(
2634 "/plugins/core/log_reader/trillian/log_directory",
2635 line);
2636 found = TRUE;
2639 cursor++;
2640 line = cursor;
2641 } else
2642 cursor++;
2644 g_free(contents);
2646 g_free(path);
2647 #endif /* !GLIB_CHECK_VERSION(2,6,0) */
2648 } /* path */
2650 if (!found) {
2651 path = NULL;
2652 folder = wpurple_get_special_folder(CSIDL_PROGRAM_FILES);
2653 if (folder) {
2654 path = g_build_filename(folder, "Trillian", "users",
2655 "default", "logs", NULL);
2656 g_free(folder);
2659 purple_prefs_add_string(
2660 "/plugins/core/log_reader/trillian/log_directory", path ? path : "");
2661 g_free(path);
2663 #else /* !defined(_WIN32) */
2664 /* TODO: At some point, this could attempt to parse talk.ini
2665 * TODO: from the default Trillian install directory on the
2666 * TODO: Windows mount point. */
2668 /* Calculate default Trillian log directory. */
2669 path = g_build_filename(PURPLE_LOG_READER_WINDOWS_MOUNT_POINT,
2670 "Program Files", "Trillian", "users",
2671 "default", "logs", NULL);
2672 purple_prefs_add_string(
2673 "/plugins/core/log_reader/trillian/log_directory", path);
2674 g_free(path);
2675 #endif
2677 /* Add QIP log directory preference. */
2678 purple_prefs_add_none("/plugins/core/log_reader/qip");
2680 /* Calculate default QIP log directory. */
2681 #ifdef _WIN32
2682 path = NULL;
2683 folder = wpurple_get_special_folder(CSIDL_PROGRAM_FILES);
2684 if (folder) {
2685 path = g_build_filename(folder, "QIP", "Users", NULL);
2686 g_free(folder);
2688 #else
2689 path = g_build_filename(PURPLE_LOG_READER_WINDOWS_MOUNT_POINT,
2690 "Program Files", "QIP", "Users", NULL);
2691 #endif
2692 purple_prefs_add_string("/plugins/core/log_reader/qip/log_directory", path ? path : "");
2693 g_free(path);
2695 /* Add aMSN Messenger log directory preference. */
2696 purple_prefs_add_none("/plugins/core/log_reader/amsn");
2698 /* Calculate default aMSN log directory. */
2699 #ifdef _WIN32
2700 path = NULL;
2701 folder = wpurple_get_special_folder(CSIDL_PROFILE); /* Silly aMSN, not using CSIDL_APPDATA */
2702 if (folder) {
2703 path = g_build_filename(folder, "amsn", NULL);
2704 g_free(folder);
2706 #else
2707 path = g_build_filename(purple_home_dir(), ".amsn", NULL);
2708 #endif
2709 purple_prefs_add_string("/plugins/core/log_reader/amsn/log_directory", path ? path : "");
2710 g_free(path);
2713 static gboolean
2714 plugin_load(PurplePlugin *plugin)
2716 g_return_val_if_fail(plugin != NULL, FALSE);
2718 log_reader_init_prefs();
2720 /* The names of IM clients are marked for translation at the request of
2721 translators who wanted to transliterate them. Many translators
2722 choose to leave them alone. Choose what's best for your language. */
2723 adium_logger = purple_log_logger_new("adium", _("Adium"), 6,
2724 NULL,
2725 NULL,
2726 adium_logger_finalize,
2727 adium_logger_list,
2728 adium_logger_read,
2729 adium_logger_size);
2730 purple_log_logger_add(adium_logger);
2732 #if 0
2733 /* The names of IM clients are marked for translation at the request of
2734 translators who wanted to transliterate them. Many translators
2735 choose to leave them alone. Choose what's best for your language. */
2736 fire_logger = purple_log_logger_new("fire", _("Fire"), 6,
2737 NULL,
2738 NULL,
2739 fire_logger_finalize,
2740 fire_logger_list,
2741 fire_logger_read,
2742 fire_logger_size);
2743 purple_log_logger_add(fire_logger);
2745 /* The names of IM clients are marked for translation at the request of
2746 translators who wanted to transliterate them. Many translators
2747 choose to leave them alone. Choose what's best for your language. */
2748 messenger_plus_logger = purple_log_logger_new("messenger_plus", _("Messenger Plus!"), 6,
2749 NULL,
2750 NULL,
2751 messenger_plus_logger_finalize,
2752 messenger_plus_logger_list,
2753 messenger_plus_logger_read,
2754 messenger_plus_logger_size);
2755 purple_log_logger_add(messenger_plus_logger);
2757 #endif
2759 /* The names of IM clients are marked for translation at the request of
2760 translators who wanted to transliterate them. Many translators
2761 choose to leave them alone. Choose what's best for your language. */
2762 qip_logger = purple_log_logger_new("qip", _("QIP"), 6,
2763 NULL,
2764 NULL,
2765 qip_logger_finalize,
2766 qip_logger_list,
2767 qip_logger_read,
2768 qip_logger_size);
2769 purple_log_logger_add(qip_logger);
2771 /* The names of IM clients are marked for translation at the request of
2772 translators who wanted to transliterate them. Many translators
2773 choose to leave them alone. Choose what's best for your language. */
2774 msn_logger = purple_log_logger_new("msn", _("MSN Messenger"), 6,
2775 NULL,
2776 NULL,
2777 msn_logger_finalize,
2778 msn_logger_list,
2779 msn_logger_read,
2780 msn_logger_size);
2781 purple_log_logger_add(msn_logger);
2783 /* The names of IM clients are marked for translation at the request of
2784 translators who wanted to transliterate them. Many translators
2785 choose to leave them alone. Choose what's best for your language. */
2786 trillian_logger = purple_log_logger_new("trillian", _("Trillian"), 6,
2787 NULL,
2788 NULL,
2789 trillian_logger_finalize,
2790 trillian_logger_list,
2791 trillian_logger_read,
2792 trillian_logger_size);
2793 purple_log_logger_add(trillian_logger);
2795 /* The names of IM clients are marked for translation at the request of
2796 translators who wanted to transliterate them. Many translators
2797 choose to leave them alone. Choose what's best for your language. */
2798 amsn_logger = purple_log_logger_new("amsn", _("aMSN"), 6,
2799 NULL,
2800 NULL,
2801 amsn_logger_finalize,
2802 amsn_logger_list,
2803 amsn_logger_read,
2804 amsn_logger_size);
2805 purple_log_logger_add(amsn_logger);
2807 return TRUE;
2810 static gboolean
2811 plugin_unload(PurplePlugin *plugin)
2813 g_return_val_if_fail(plugin != NULL, FALSE);
2815 purple_log_logger_remove(adium_logger);
2816 purple_log_logger_free(adium_logger);
2817 adium_logger = NULL;
2819 #if 0
2820 purple_log_logger_remove(fire_logger);
2821 purple_log_logger_free(fire_logger);
2822 fire_logger = NULL;
2824 purple_log_logger_remove(messenger_plus_logger);
2825 purple_log_logger_free(messenger_plus_logger);
2826 messenger_plus_logger = NULL;
2827 #endif
2829 purple_log_logger_remove(msn_logger);
2830 purple_log_logger_free(msn_logger);
2831 msn_logger = NULL;
2833 purple_log_logger_remove(trillian_logger);
2834 purple_log_logger_free(trillian_logger);
2835 trillian_logger = NULL;
2837 purple_log_logger_remove(qip_logger);
2838 purple_log_logger_free(qip_logger);
2839 qip_logger = NULL;
2841 purple_log_logger_remove(amsn_logger);
2842 purple_log_logger_free(amsn_logger);
2843 amsn_logger = NULL;
2845 return TRUE;
2848 static PurplePluginPrefFrame *
2849 get_plugin_pref_frame(PurplePlugin *plugin)
2851 PurplePluginPrefFrame *frame;
2852 PurplePluginPref *ppref;
2854 g_return_val_if_fail(plugin != NULL, FALSE);
2856 frame = purple_plugin_pref_frame_new();
2859 /* Add general preferences. */
2861 ppref = purple_plugin_pref_new_with_label(_("General Log Reading Configuration"));
2862 purple_plugin_pref_frame_add(frame, ppref);
2864 ppref = purple_plugin_pref_new_with_name_and_label(
2865 "/plugins/core/log_reader/fast_sizes", _("Fast size calculations"));
2866 purple_plugin_pref_frame_add(frame, ppref);
2868 ppref = purple_plugin_pref_new_with_name_and_label(
2869 "/plugins/core/log_reader/use_name_heuristics", _("Use name heuristics"));
2870 purple_plugin_pref_frame_add(frame, ppref);
2873 /* Add Log Directory preferences. */
2875 ppref = purple_plugin_pref_new_with_label(_("Log Directory"));
2876 purple_plugin_pref_frame_add(frame, ppref);
2878 ppref = purple_plugin_pref_new_with_name_and_label(
2879 "/plugins/core/log_reader/adium/log_directory", _("Adium"));
2880 purple_plugin_pref_frame_add(frame, ppref);
2882 #if 0
2883 ppref = purple_plugin_pref_new_with_name_and_label(
2884 "/plugins/core/log_reader/fire/log_directory", _("Fire"));
2885 purple_plugin_pref_frame_add(frame, ppref);
2887 ppref = purple_plugin_pref_new_with_name_and_label(
2888 "/plugins/core/log_reader/messenger_plus/log_directory", _("Messenger Plus!"));
2889 purple_plugin_pref_frame_add(frame, ppref);
2890 #endif
2892 ppref = purple_plugin_pref_new_with_name_and_label(
2893 "/plugins/core/log_reader/qip/log_directory", _("QIP"));
2894 purple_plugin_pref_frame_add(frame, ppref);
2896 ppref = purple_plugin_pref_new_with_name_and_label(
2897 "/plugins/core/log_reader/msn/log_directory", _("MSN Messenger"));
2898 purple_plugin_pref_frame_add(frame, ppref);
2900 ppref = purple_plugin_pref_new_with_name_and_label(
2901 "/plugins/core/log_reader/trillian/log_directory", _("Trillian"));
2902 purple_plugin_pref_frame_add(frame, ppref);
2904 ppref = purple_plugin_pref_new_with_name_and_label(
2905 "/plugins/core/log_reader/amsn/log_directory", _("aMSN"));
2906 purple_plugin_pref_frame_add(frame, ppref);
2908 return frame;
2911 static PurplePluginUiInfo prefs_info = {
2912 get_plugin_pref_frame,
2913 0, /* page_num (reserved) */
2914 NULL, /* frame (reserved) */
2916 /* padding */
2917 NULL,
2918 NULL,
2919 NULL,
2920 NULL
2923 static PurplePluginInfo info =
2925 PURPLE_PLUGIN_MAGIC,
2926 PURPLE_MAJOR_VERSION,
2927 PURPLE_MINOR_VERSION,
2928 PURPLE_PLUGIN_STANDARD, /**< type */
2929 NULL, /**< ui_requirement */
2930 0, /**< flags */
2931 NULL, /**< dependencies */
2932 PURPLE_PRIORITY_DEFAULT, /**< priority */
2933 "core-log_reader", /**< id */
2934 N_("Log Reader"), /**< name */
2935 DISPLAY_VERSION, /**< version */
2937 /** summary */
2938 N_("Includes other IM clients' logs in the "
2939 "log viewer."),
2941 /** description */
2942 N_("When viewing logs, this plugin will include "
2943 "logs from other IM clients. Currently, this "
2944 "includes Adium, MSN Messenger, aMSN, and "
2945 "Trillian.\n\n"
2946 "WARNING: This plugin is still alpha code and "
2947 "may crash frequently. Use it at your own risk!"),
2949 "Richard Laager <rlaager@pidgin.im>", /**< author */
2950 PURPLE_WEBSITE, /**< homepage */
2951 plugin_load, /**< load */
2952 plugin_unload, /**< unload */
2953 NULL, /**< destroy */
2954 NULL, /**< ui_info */
2955 NULL, /**< extra_info */
2956 &prefs_info, /**< prefs_info */
2957 NULL, /**< actions */
2959 /* padding */
2960 NULL,
2961 NULL,
2962 NULL,
2963 NULL
2966 PURPLE_INIT_PLUGIN(log_reader, init_plugin, info)