Mercurial > pidgin.yaz
view plugins/log_reader.c @ 11707:b7af9100af6c
[gaim-migrate @ 13998]
Document that -1 is an allowed size value for xmlnode_from_str().
committer: Tailor Script <tailor@pidgin.im>
author | Richard Laager <rlaager@wiktel.com> |
---|---|
date | Fri, 21 Oct 2005 19:03:51 +0000 |
parents | 994f1c7bee8b |
children | ae51c59bf819 |
line wrap: on
line source
#ifdef HAVE_CONFIG_H # include <config.h> #endif #include <stdio.h> #ifndef GAIM_PLUGINS # define GAIM_PLUGINS #endif #include "internal.h" #include "debug.h" #include "log.h" #include "plugin.h" #include "pluginpref.h" #include "prefs.h" #include "stringref.h" #include "util.h" #include "version.h" #include "xmlnode.h" /* This must be the last Gaim header included. */ #ifdef _WIN32 #include "win32dep.h" #endif /* Where is the Windows partition mounted? */ #ifndef GAIM_LOG_READER_WINDOWS_MOUNT_POINT #define GAIM_LOG_READER_WINDOWS_MOUNT_POINT "/mnt/windows" #endif enum name_guesses { NAME_GUESS_UNKNOWN, NAME_GUESS_ME, NAME_GUESS_THEM }; /***************************************************************************** * Adium Logger * *****************************************************************************/ /* The adium logger doesn't write logs, only reads them. This is to include * Adium logs in the log viewer transparently. */ static GaimLogLogger *adium_logger; enum adium_log_type { ADIUM_HTML, ADIUM_TEXT, }; struct adium_logger_data { char *path; enum adium_log_type type; }; static GList *adium_logger_list(GaimLogType type, const char *sn, GaimAccount *account) { GList *list = NULL; const char *logdir; GaimPlugin *plugin; GaimPluginProtocolInfo *prpl_info; char *prpl_name; char *temp; char *path; GDir *dir; g_return_val_if_fail(sn != NULL, list); g_return_val_if_fail(account != NULL, list); logdir = gaim_prefs_get_string("/plugins/core/log_reader/adium/log_directory"); /* By clearing the log directory path, this logger can be (effectively) disabled. */ if (!*logdir) return list; plugin = gaim_find_prpl(gaim_account_get_protocol_id(account)); if (!plugin) return NULL; prpl_info = GAIM_PLUGIN_PROTOCOL_INFO(plugin); if (!prpl_info->list_icon) return NULL; prpl_name = g_ascii_strup(prpl_info->list_icon(account, NULL), -1); temp = g_strdup_printf("%s.%s", prpl_name, account->username); path = g_build_filename(logdir, temp, sn, NULL); g_free(temp); dir = g_dir_open(path, 0, NULL); if (dir) { const gchar *file; while ((file = g_dir_read_name(dir))) { if (!g_str_has_prefix(file, sn)) continue; if (g_str_has_suffix(file, ".html")) { struct tm tm; const char *date = file; date += strlen(sn) + 2; if (sscanf(date, "%u|%u|%u", &tm.tm_year, &tm.tm_mon, &tm.tm_mday) != 3) { gaim_debug(GAIM_DEBUG_ERROR, "Adium log parse", "Filename timestamp parsing error\n"); } else { char *filename = g_build_filename(path, file, NULL); FILE *handle = fopen(filename, "rb"); char *contents; char *contents2; struct adium_logger_data *data; GaimLog *log; if (!handle) { g_free(filename); continue; } /* XXX: This is really inflexible. */ contents = g_malloc(57); fread(contents, 56, 1, handle); fclose(handle); contents[56] = '\0'; /* XXX: This is fairly inflexible. */ contents2 = contents; while (*contents2 && *contents2 != '>') contents2++; if (*contents2) contents2++; while (*contents2 && *contents2 != '>') contents2++; if (*contents2) contents2++; if (sscanf(contents2, "%u.%u.%u", &tm.tm_hour, &tm.tm_min, &tm.tm_sec) != 3) { gaim_debug(GAIM_DEBUG_ERROR, "Adium log parse", "Contents timestamp parsing error\n"); g_free(contents); g_free(filename); continue; } g_free(contents); data = g_new0(struct adium_logger_data, 1); data->path = filename; data->type = ADIUM_HTML; tm.tm_year -= 1900; tm.tm_mon -= 1; log = gaim_log_new(GAIM_LOG_IM, sn, account, NULL, mktime(&tm)); log->logger = adium_logger; log->logger_data = data; list = g_list_append(list, log); } } else if (g_str_has_suffix(file, ".adiumLog")) { struct tm tm; const char *date = file; date += strlen(sn) + 2; if (sscanf(date, "%u|%u|%u", &tm.tm_year, &tm.tm_mon, &tm.tm_mday) != 3) { gaim_debug(GAIM_DEBUG_ERROR, "Adium log parse", "Filename timestamp parsing error\n"); } else { char *filename = g_build_filename(path, file, NULL); FILE *handle = fopen(filename, "rb"); char *contents; char *contents2; struct adium_logger_data *data; GaimLog *log; if (!handle) { g_free(filename); continue; } /* XXX: This is really inflexible. */ contents = g_malloc(14); fread(contents, 13, 1, handle); fclose(handle); contents[13] = '\0'; contents2 = contents; while (*contents2 && *contents2 != '(') contents2++; if (*contents2) contents2++; if (sscanf(contents2, "%u.%u.%u", &tm.tm_hour, &tm.tm_min, &tm.tm_sec) != 3) { gaim_debug(GAIM_DEBUG_ERROR, "Adium log parse", "Contents timestamp parsing error\n"); g_free(contents); g_free(filename); continue; } g_free(contents); tm.tm_year -= 1900; tm.tm_mon -= 1; data = g_new0(struct adium_logger_data, 1); data->path = filename; data->type = ADIUM_TEXT; log = gaim_log_new(GAIM_LOG_IM, sn, account, NULL, mktime(&tm)); log->logger = adium_logger; log->logger_data = data; list = g_list_append(list, log); } } } g_dir_close(dir); } g_free(prpl_name); g_free(path); return list; } static char *adium_logger_read (GaimLog *log, GaimLogReadFlags *flags) { struct adium_logger_data *data; GError *error = NULL; gchar *read = NULL; gsize length; g_return_val_if_fail(log != NULL, g_strdup("")); data = log->logger_data; g_return_val_if_fail(data->path != NULL, g_strdup("")); gaim_debug(GAIM_DEBUG_INFO, "Adium log read", "Reading %s\n", data->path); if (!g_file_get_contents(data->path, &read, &length, &error)) { gaim_debug(GAIM_DEBUG_ERROR, "Adium log read", "Error reading log\n"); if (error) g_error_free(error); return g_strdup(""); } if (data->type != ADIUM_HTML) { char *escaped = g_markup_escape_text(read, -1); g_free(read); read = escaped; } #ifdef WIN32 /* This problem only seems to show up on Windows. * The BOM is displaying as a space at the beginning of the log. */ if (g_str_has_prefix(read, "\xef\xbb\xbf")) { /* FIXME: This feels so wrong... */ char *temp = g_strdup(&(read[3])); g_free(read); read = temp; } #endif /* TODO: Apply formatting. * Replace the above hack with something better, since we'll * be looping over the entire log file contents anyway. */ return read; } static int adium_logger_size (GaimLog *log) { struct adium_logger_data *data; char *text; size_t size; g_return_val_if_fail(log != NULL, 0); data = log->logger_data; if (gaim_prefs_get_bool("/plugins/core/log_reader/fast_sizes")) { struct stat st; if (!data->path || stat(data->path, &st)) st.st_size = 0; return st.st_size; } text = adium_logger_read(log, NULL); size = strlen(text); g_free(text); return size; } static void adium_logger_finalize(GaimLog *log) { struct adium_logger_data *data; g_return_if_fail(log != NULL); data = log->logger_data; g_free(data->path); } /***************************************************************************** * Fire Logger * *****************************************************************************/ /* The fire logger doesn't write logs, only reads them. This is to include * Fire logs in the log viewer transparently. */ static GaimLogLogger *fire_logger; struct fire_logger_data { }; static GList *fire_logger_list(GaimLogType type, const char *sn, GaimAccount *account) { /* TODO: Do something here. */ return NULL; } static char * fire_logger_read (GaimLog *log, GaimLogReadFlags *flags) { struct fire_logger_data *data; g_return_val_if_fail(log != NULL, g_strdup("")); data = log->logger_data; /* TODO: Do something here. */ return g_strdup(""); } static int fire_logger_size (GaimLog *log) { g_return_val_if_fail(log != NULL, 0); if (gaim_prefs_get_bool("/plugins/core/log_reader/fast_sizes")) return 0; /* TODO: Do something here. */ return 0; } static void fire_logger_finalize(GaimLog *log) { g_return_if_fail(log != NULL); /* TODO: Do something here. */ } /***************************************************************************** * Messenger Plus! Logger * *****************************************************************************/ /* The messenger_plus logger doesn't write logs, only reads them. This is to include * Messenger Plus! logs in the log viewer transparently. */ static GaimLogLogger *messenger_plus_logger; struct messenger_plus_logger_data { }; static GList *messenger_plus_logger_list(GaimLogType type, const char *sn, GaimAccount *account) { /* TODO: Do something here. */ return NULL; } static char * messenger_plus_logger_read (GaimLog *log, GaimLogReadFlags *flags) { struct messenger_plus_logger_data *data = log->logger_data; g_return_val_if_fail(log != NULL, g_strdup("")); data = log->logger_data; /* TODO: Do something here. */ return g_strdup(""); } static int messenger_plus_logger_size (GaimLog *log) { g_return_val_if_fail(log != NULL, 0); if (gaim_prefs_get_bool("/plugins/core/log_reader/fast_sizes")) return 0; /* TODO: Do something here. */ return 0; } static void messenger_plus_logger_finalize(GaimLog *log) { g_return_if_fail(log != NULL); /* TODO: Do something here. */ } /***************************************************************************** * MSN Messenger Logger * *****************************************************************************/ /* The msn logger doesn't write logs, only reads them. This is to include * MSN Messenger message histories in the log viewer transparently. */ static GaimLogLogger *msn_logger; struct msn_logger_data { xmlnode *root; xmlnode *message; const char *session_id; int last_log; GString *text; }; static time_t msn_logger_parse_timestamp(xmlnode *message) { const char *date; const char *time; struct tm tm; char am_pm; g_return_val_if_fail(message != NULL, (time_t)0); date = xmlnode_get_attrib(message, "Date"); if (!(date && *date)) { gaim_debug(GAIM_DEBUG_ERROR, "MSN log timestamp parse", "Attribute missing: %s\n", "Date"); return (time_t)0; } time = xmlnode_get_attrib(message, "Time"); if (!(time && *time)) { gaim_debug(GAIM_DEBUG_ERROR, "MSN log timestamp parse", "Attribute missing: %s\n", "Time"); return (time_t)0; } if (sscanf(date, "%u/%u/%u", &tm.tm_mon, &tm.tm_mday, &tm.tm_year) != 3) gaim_debug(GAIM_DEBUG_ERROR, "MSN log timestamp parse", "%s parsing error\n", "Date"); if (sscanf(time, "%u:%u:%u %c", &tm.tm_hour, &tm.tm_min, &tm.tm_sec, &am_pm) != 4) gaim_debug(GAIM_DEBUG_ERROR, "MSN log timestamp parse", "%s parsing error\n", "Time"); tm.tm_year -= 1900; tm.tm_mon -= 1; if (am_pm == 'P') { tm.tm_hour += 12; } else if (tm.tm_hour == 12) { /* 12 AM = 00 hr */ tm.tm_hour = 0; } /* Let the C library deal with daylight savings time. */ tm.tm_isdst = -1; return mktime(&tm); } static GList *msn_logger_list(GaimLogType type, const char *sn, GaimAccount *account) { GList *list = NULL; char *username; GaimBuddy *buddy; const char *logdir; const char *savedfilename = NULL; char *logfile; char *path; GError *error = NULL; gchar *contents = NULL; gsize length; xmlnode *root; xmlnode *message; const char *old_session_id = ""; struct msn_logger_data *data = NULL; g_return_val_if_fail(sn != NULL, list); g_return_val_if_fail(account != NULL, list); if (strcmp(account->protocol_id, "prpl-msn")) return list; logdir = gaim_prefs_get_string("/plugins/core/log_reader/msn/log_directory"); /* By clearing the log directory path, this logger can be (effectively) disabled. */ if (!*logdir) return list; buddy = gaim_find_buddy(account, sn); if ((username = g_strdup(gaim_account_get_string( account, "log_reader_msn_log_folder", NULL)))) { /* As a special case, we allow the null string to kill the parsing * straight away. This would allow the user to deal with the case * when two account have the same username at different domains and * only one has logs stored. */ if (!*username) { g_free(username); return list; } } else { username = g_strdup(gaim_normalize(account, account->username)); } if (buddy) savedfilename = gaim_blist_node_get_string(&buddy->node, "log_reader_msn_log_filename"); if (savedfilename) { /* As a special case, we allow the null string to kill the parsing * straight away. This would allow the user to deal with the case * when two buddies have the same username at different domains and * only one has logs stored. */ if (!*savedfilename) { g_free(username); return list; } logfile = g_strdup(savedfilename); } else { logfile = g_strdup_printf("%s.xml", gaim_normalize(account, sn)); } path = g_build_filename(logdir, username, "History", logfile, NULL); if (!g_file_test(path, G_FILE_TEST_EXISTS)) { gboolean found = FALSE; char *at_sign; GDir *dir; g_free(path); if (savedfilename) { /* We had a saved filename, but it doesn't exist. * Returning now is the right course of action because we don't * want to detect another file incorrectly. */ g_free(username); g_free(logfile); return list; } /* Perhaps we're using a new version of MSN with the weird numbered folders. * I don't know how the numbers are calculated, so I'm going to attempt to * find logs by pattern matching... */ at_sign = g_strrstr(username, "@"); if (at_sign) *at_sign = '\0'; dir = g_dir_open(logdir, 0, NULL); if (dir) { const gchar *name; while ((name = g_dir_read_name(dir))) { const char *c = name; if (!g_str_has_prefix(c, username)) continue; c += strlen(username); while (*c) { if (!g_ascii_isdigit(*c)) break; c++; } path = g_build_filename(logdir, name, NULL); /* The !c makes sure we got to the end of the while loop above. */ if (!*c && g_file_test(path, G_FILE_TEST_IS_DIR)) { char *history_path = g_build_filename( path, "History", NULL); if (g_file_test(history_path, G_FILE_TEST_IS_DIR)) { gaim_account_set_string(account, "log_reader_msn_log_folder", name); g_free(path); path = history_path; found = TRUE; break; } g_free(path); g_free(history_path); } else g_free(path); } g_dir_close(dir); } g_free(username); if (!found) { g_free(logfile); return list; } /* If we've reached this point, we've found a History folder. */ username = g_strdup(gaim_normalize(account, sn)); at_sign = g_strrstr(username, "@"); if (at_sign) *at_sign = '\0'; found = FALSE; dir = g_dir_open(path, 0, NULL); if (dir) { const gchar *name; while ((name = g_dir_read_name(dir))) { const char *c = name; if (!g_str_has_prefix(c, username)) continue; c += strlen(username); while (*c) { if (!g_ascii_isdigit(*c)) break; c++; } path = g_build_filename(path, name, NULL); if (!strcmp(c, ".xml") && g_file_test(path, G_FILE_TEST_EXISTS)) { found = TRUE; g_free(logfile); logfile = g_strdup(name); break; } else g_free(path); } g_dir_close(dir); } g_free(username); if (!found) { g_free(logfile); return list; } } else { g_free(username); g_free(logfile); logfile = NULL; /* No sense saving the obvious buddy@domain.com. */ } gaim_debug(GAIM_DEBUG_INFO, "MSN log read", "Reading %s\n", path); if (!g_file_get_contents(path, &contents, &length, &error)) { g_free(path); gaim_debug(GAIM_DEBUG_ERROR, "MSN log read", "Error reading log\n"); if (error) g_error_free(error); return list; } g_free(path); /* Reading the file was successful... * Save its name if it involves the crazy numbers. The idea here is that you could * then tweak the blist.xml file by hand if need be. This would be the case if two * buddies have the same username at different domains. One set of logs would get * detected for both buddies. * * I can't think of how buddy would be NULL. */ if (buddy && logfile) { gaim_blist_node_set_string(&buddy->node, "log_reader_msn_log_filename", logfile); g_free(logfile); } root = xmlnode_from_str(contents, length); g_free(contents); if (!root) return list; for (message = xmlnode_get_child(root, "Message"); message; message = xmlnode_get_next_twin(message)) { const char *session_id; session_id = xmlnode_get_attrib(message, "SessionID"); if (!session_id) { gaim_debug(GAIM_DEBUG_ERROR, "MSN log parse", "Error parsing message: %s\n", "SessionID missing"); continue; } if (strcmp(session_id, old_session_id)) { /* * The session ID differs from the last message. * Thus, this is the start of a new conversation. */ GaimLog *log; data = g_new0(struct msn_logger_data, 1); data->root = root; data->message = message; data->session_id = session_id; data->text = NULL; data->last_log = FALSE; log = gaim_log_new(GAIM_LOG_IM, sn, account, NULL, msn_logger_parse_timestamp(message)); log->logger = msn_logger; log->logger_data = data; list = g_list_append(list, log); } old_session_id = session_id; } if (data) data->last_log = TRUE; return list; } static char * msn_logger_read (GaimLog *log, GaimLogReadFlags *flags) { struct msn_logger_data *data; GString *text = NULL; xmlnode *message; g_return_val_if_fail(log != NULL, g_strdup("")); data = log->logger_data; if (data->text) { /* The GTK code which displays the logs g_free()s whatever is * returned from this function. Thus, we can't reuse the str * part of the GString. The only solution is to free it and * start over. */ g_string_free(data->text, FALSE); } text = g_string_new(""); if (!data->root || !data->message || !data->session_id) { /* Something isn't allocated correctly. */ gaim_debug(GAIM_DEBUG_ERROR, "MSN log parse", "Error parsing message: %s\n", "Internal variables inconsistent"); data->text = text; return text->str; } for (message = data->message; message; message = xmlnode_get_next_twin(message)) { const char *new_session_id; xmlnode *text_node; const char *from_name = NULL; const char *to_name = NULL; xmlnode *from; xmlnode *to; enum name_guesses name_guessed = NAME_GUESS_UNKNOWN; const char *their_name; time_t time_unix; struct tm *tm_new; char *timestamp; const char *style; new_session_id = xmlnode_get_attrib(message, "SessionID"); /* If this triggers, something is wrong with the XML. */ if (!new_session_id) { gaim_debug(GAIM_DEBUG_ERROR, "MSN log parse", "Error parsing message: %s\n", "New SessionID missing"); break; } if (strcmp(new_session_id, data->session_id)) { /* The session ID differs from the first message. * Thus, this is the start of a new conversation. */ break; } text_node = xmlnode_get_child(message, "Text"); if (!text_node) continue; from = xmlnode_get_child(message, "From"); if (from) { xmlnode *user = xmlnode_get_child(from, "User"); if (user) { from_name = xmlnode_get_attrib(user, "FriendlyName"); /* This saves a check later. */ if (!*from_name) from_name = NULL; } } to = xmlnode_get_child(message, "To"); if (to) { xmlnode *user = xmlnode_get_child(to, "User"); if (user) { to_name = xmlnode_get_attrib(user, "FriendlyName"); /* This saves a check later. */ if (!*to_name) to_name = NULL; } } their_name = from_name; if (from_name && gaim_prefs_get_bool("/plugins/core/log_reader/use_name_heuristics")) { const char *friendly_name = gaim_connection_get_display_name(log->account->gc); if (friendly_name != NULL) { int friendly_name_length = strlen(friendly_name); int alias_length = strlen(log->account->alias); GaimBuddy *buddy = gaim_find_buddy(log->account, log->name); gboolean from_name_matches; gboolean to_name_matches; if (buddy && buddy->alias) their_name = buddy->alias; /* Try to guess which user is me. * The first step is to determine if either of the names matches either my * friendly name or alias. For this test, "match" is defined as: * ^(friendly_name|alias)([^a-zA-Z0-9].*)?$ */ from_name_matches = ((g_str_has_prefix( from_name, friendly_name) && !isalnum(*(from_name + friendly_name_length))) || (g_str_has_prefix(from_name, log->account->alias) && !isalnum(*(from_name + alias_length)))); to_name_matches = ((g_str_has_prefix( to_name, friendly_name) && !isalnum(*(to_name + friendly_name_length))) || (g_str_has_prefix(to_name, log->account->alias) && !isalnum(*(to_name + alias_length)))); if (from_name_matches) { if (!to_name_matches) { name_guessed = NAME_GUESS_ME; } } else if (to_name_matches) { name_guessed = NAME_GUESS_THEM; } else { if (buddy && buddy->alias) { char *alias = g_strdup(buddy->alias); /* "Truncate" the string at the first non-alphanumeric * character. The idea is to relax the comparison. */ char *temp; for (temp = alias; *temp ; temp++) { if (!isalnum(*temp)) { *temp = '\0'; break; } } alias_length = strlen(alias); /* Try to guess which user is them. * The first step is to determine if either of the names * matches their alias. For this test, "match" is * defined as: ^alias([^a-zA-Z0-9].*)?$ */ from_name_matches = (g_str_has_prefix( from_name, alias) && !isalnum(*(from_name + alias_length))); to_name_matches = (g_str_has_prefix( to_name, alias) && !isalnum(*(to_name + alias_length))); g_free(alias); if (from_name_matches) { if (!to_name_matches) { name_guessed = NAME_GUESS_THEM; } } else if (to_name_matches) { name_guessed = NAME_GUESS_ME; } else if (buddy->server_alias) { friendly_name_length = strlen(buddy->server_alias); /* Try to guess which user is them. * The first step is to determine if either of * the names matches their friendly name. For * this test, "match" is defined as: * ^friendly_name([^a-zA-Z0-9].*)?$ */ from_name_matches = (g_str_has_prefix( from_name, buddy->server_alias) && !isalnum(*(from_name + friendly_name_length))); to_name_matches = (g_str_has_prefix( to_name, buddy->server_alias) && !isalnum(*(to_name + friendly_name_length))); if (from_name_matches) { if (!to_name_matches) { name_guessed = NAME_GUESS_THEM; } } else if (to_name_matches) { name_guessed = NAME_GUESS_ME; } } } } } } if (name_guessed != NAME_GUESS_UNKNOWN) { text = g_string_append(text, "<span style=\"color: #"); if (name_guessed == NAME_GUESS_ME) text = g_string_append(text, "16569E"); else text = g_string_append(text, "A82F2F"); text = g_string_append(text, ";\">"); } time_unix = msn_logger_parse_timestamp(message); tm_new = localtime(&time_unix); timestamp = g_strdup_printf("<font size=\"2\">(%02u:%02u:%02u)</font> ", tm_new->tm_hour, tm_new->tm_min, tm_new->tm_sec); text = g_string_append(text, timestamp); g_free(timestamp); if (from_name) { text = g_string_append(text, "<b>"); if (name_guessed == NAME_GUESS_ME) text = g_string_append(text, log->account->alias); else if (name_guessed == NAME_GUESS_THEM) text = g_string_append(text, their_name); else text = g_string_append(text, from_name); text = g_string_append(text, ":</b> "); } if (name_guessed != NAME_GUESS_UNKNOWN) text = g_string_append(text, "</span>"); style = xmlnode_get_attrib(text_node, "Style"); if (style && *style) { text = g_string_append(text, "<span style=\""); text = g_string_append(text, style); text = g_string_append(text, "\">"); text = g_string_append(text, xmlnode_get_data(text_node)); text = g_string_append(text, "</span>\n"); } else { text = g_string_append(text, xmlnode_get_data(text_node)); text = g_string_append(text, "\n"); } } data->text = text; return text->str; } static int msn_logger_size (GaimLog *log) { char *text; size_t size; g_return_val_if_fail(log != NULL, 0); if (gaim_prefs_get_bool("/plugins/core/log_reader/fast_sizes")) return 0; text = msn_logger_read(log, NULL); size = strlen(text); g_free(text); return size; } static void msn_logger_finalize(GaimLog *log) { struct msn_logger_data *data; g_return_if_fail(log != NULL); data = log->logger_data; if (data->last_log) xmlnode_free(data->root); if (data->text) g_string_free(data->text, FALSE); } /***************************************************************************** * Trillian Logger * *****************************************************************************/ /* The trillian logger doesn't write logs, only reads them. This is to include * Trillian logs in the log viewer transparently. */ static GaimLogLogger *trillian_logger; static void trillian_logger_finalize(GaimLog *log); struct trillian_logger_data { char *path; /* FIXME: Change this to use GaimStringref like log.c:old_logger_list */ int offset; int length; char *their_nickname; }; static GList *trillian_logger_list(GaimLogType type, const char *sn, GaimAccount *account) { GList *list = NULL; const char *logdir; GaimPlugin *plugin; GaimPluginProtocolInfo *prpl_info; char *prpl_name; const char *buddy_name; char *filename; char *path; GError *error = NULL; gchar *contents = NULL; gsize length; gchar *line; gchar *c; g_return_val_if_fail(sn != NULL, list); g_return_val_if_fail(account != NULL, list); logdir = gaim_prefs_get_string("/plugins/core/log_reader/trillian/log_directory"); /* By clearing the log directory path, this logger can be (effectively) disabled. */ if (!*logdir) return list; plugin = gaim_find_prpl(gaim_account_get_protocol_id(account)); if (!plugin) return NULL; prpl_info = GAIM_PLUGIN_PROTOCOL_INFO(plugin); if (!prpl_info->list_icon) return NULL; prpl_name = g_ascii_strup(prpl_info->list_icon(account, NULL), -1); buddy_name = gaim_normalize(account, sn); filename = g_strdup_printf("%s.log", buddy_name); path = g_build_filename( logdir, prpl_name, filename, NULL); gaim_debug(GAIM_DEBUG_INFO, "Trillian log list", "Reading %s\n", path); /* FIXME: There's really no need to read the entire file at once. * See src/log.c:old_logger_list for a better approach. */ if (!g_file_get_contents(path, &contents, &length, &error)) { if (error) { g_error_free(error); error = NULL; } g_free(path); path = g_build_filename( logdir, prpl_name, "Query", filename, NULL); gaim_debug(GAIM_DEBUG_INFO, "Trillian log list", "Reading %s\n", path); if (!g_file_get_contents(path, &contents, &length, &error)) { if (error) g_error_free(error); } } g_free(filename); if (contents) { struct trillian_logger_data *data = NULL; int offset = 0; int last_line_offset = 0; line = contents; c = contents; while (*c) { offset++; if (*c != '\n') { c++; continue; } *c = '\0'; if (g_str_has_prefix(line, "Session Close ")) { if (data && !data->length) data->length = last_line_offset - data->offset; if (!data->length) { /* This log had no data, so we remove it. */ GList *last = g_list_last(list); gaim_debug(GAIM_DEBUG_INFO, "Trillian log list", "Empty log. Offset %i\n", data->offset); trillian_logger_finalize((GaimLog *)last->data); list = g_list_delete_link(list, last); } } else if (line[0] && line[1] && line [3] && g_str_has_prefix(&line[3], "sion Start ")) { char *their_nickname = line; char *timestamp; if (data && !data->length) data->length = last_line_offset - data->offset; while (*their_nickname && (*their_nickname != ':')) their_nickname++; their_nickname++; /* This code actually has nothing to do with * the timestamp YET. I'm simply using this * variable for now to NUL-terminate the * their_nickname string. */ timestamp = their_nickname; while (*timestamp && *timestamp != ')') timestamp++; if (*timestamp == ')') { char *month; struct tm tm; *timestamp = '\0'; if (line[0] && line[1] && line[2]) timestamp += 3; /* Now we start dealing with the timestamp. */ /* Skip over the day name. */ while (*timestamp && (*timestamp != ' ')) timestamp++; *timestamp = '\0'; timestamp++; /* Parse out the month. */ month = timestamp; while (*timestamp && (*timestamp != ' ')) timestamp++; *timestamp = '\0'; timestamp++; /* Parse the day, time, and year. */ if (sscanf(timestamp, "%u %u:%u:%u %u", &tm.tm_mday, &tm.tm_hour, &tm.tm_min, &tm.tm_sec, &tm.tm_year) != 5) { gaim_debug(GAIM_DEBUG_ERROR, "Trillian log timestamp parse", "Session Start parsing error\n"); } else { GaimLog *log; tm.tm_year -= 1900; /* Let the C library deal with * daylight savings time. */ tm.tm_isdst = -1; /* Ugly hack, in case current locale * is not English. This code is taken * from log.c. */ if (strcmp(month, "Jan") == 0) { tm.tm_mon= 0; } else if (strcmp(month, "Feb") == 0) { tm.tm_mon = 1; } else if (strcmp(month, "Mar") == 0) { tm.tm_mon = 2; } else if (strcmp(month, "Apr") == 0) { tm.tm_mon = 3; } else if (strcmp(month, "May") == 0) { tm.tm_mon = 4; } else if (strcmp(month, "Jun") == 0) { tm.tm_mon = 5; } else if (strcmp(month, "Jul") == 0) { tm.tm_mon = 6; } else if (strcmp(month, "Aug") == 0) { tm.tm_mon = 7; } else if (strcmp(month, "Sep") == 0) { tm.tm_mon = 8; } else if (strcmp(month, "Oct") == 0) { tm.tm_mon = 9; } else if (strcmp(month, "Nov") == 0) { tm.tm_mon = 10; } else if (strcmp(month, "Dec") == 0) { tm.tm_mon = 11; } data = g_new0( struct trillian_logger_data, 1); data->path = g_strdup(path); data->offset = offset; data->length = 0; data->their_nickname = g_strdup(their_nickname); log = gaim_log_new(GAIM_LOG_IM, sn, account, NULL, mktime(&tm)); log->logger = trillian_logger; log->logger_data = data; list = g_list_append(list, log); } } } c++; line = c; last_line_offset = offset; } g_free(contents); } g_free(path); g_free(prpl_name); return list; } static char * trillian_logger_read (GaimLog *log, GaimLogReadFlags *flags) { struct trillian_logger_data *data; char *read; FILE *file; GaimBuddy *buddy; char *escaped; GString *formatted; char *c; char *line; g_return_val_if_fail(log != NULL, g_strdup("")); data = log->logger_data; g_return_val_if_fail(data->path != NULL, g_strdup("")); g_return_val_if_fail(data->length > 0, g_strdup("")); g_return_val_if_fail(data->their_nickname != NULL, g_strdup("")); gaim_debug(GAIM_DEBUG_INFO, "Trillian log read", "Reading %s\n", data->path); read = g_malloc(data->length + 2); file = fopen(data->path, "rb"); fseek(file, data->offset, SEEK_SET); fread(read, data->length, 1, file); fclose(file); if (read[data->length-1] == '\n') { read[data->length] = '\0'; } else { read[data->length] = '\n'; read[data->length+1] = '\0'; } /* Load miscellaneous data. */ buddy = gaim_find_buddy(log->account, log->name); escaped = g_markup_escape_text(read, -1); g_free(read); read = escaped; /* Apply formatting... */ formatted = g_string_new(""); c = read; line = read; while (*c) { if (*c == '\n') { char *link_temp_line; char *link; char *timestamp; char *footer = NULL; *c = '\0'; /* Convert links. * * The format is (Link: URL)URL * So, I want to find each occurance of "(Link: " and replace that chunk with: * <a href=" * Then, replace the next ")" with: * "> * Then, replace the next " " (or add this if the end-of-line is reached) with: * </a> */ link_temp_line = NULL; while ((link = g_strstr_len(line, strlen(line), "(Link: "))) { GString *temp; if (!*link) continue; *link = '\0'; link++; temp = g_string_new(line); g_string_append(temp, "<a href=\""); if (strlen(link) >= 6) { link += (sizeof("(Link: ") - 1); while (*link && *link != ')') { g_string_append_c(temp, *link); link++; } if (link) { link++; g_string_append(temp, "\">"); while (*link && *link != ' ') { g_string_append_c(temp, *link); link++; } g_string_append(temp, "</a>"); } g_string_append(temp, link); /* Free the last round's line. */ if (link_temp_line) g_free(line); line = temp->str; g_string_free(temp, FALSE); /* Save this memory location so we can free it later. */ link_temp_line = line; } } timestamp = ""; if (*line == '[') { timestamp = line; while (*timestamp && *timestamp != ']') timestamp++; if (*timestamp == ']') { *timestamp = '\0'; line++; /* TODO: Parse the timestamp and convert it to Gaim's format. */ g_string_append_printf(formatted, "<font size=\"2\">(%s)</font> ", line); line = timestamp; if (line[1] && line[2]) line += 2; } if (g_str_has_prefix(line, "*** ")) { line += (sizeof("*** ") - 1); g_string_append(formatted, "<b>"); footer = "</b>"; if (g_str_has_prefix(line, "NOTE: This user is offline.")) { line = _("User is offline."); } else if (g_str_has_prefix(line, "NOTE: Your status is currently set to ")) { line += (sizeof("NOTE: ") - 1); } else if (g_str_has_prefix(line, "Auto-response sent to ")) { g_string_append(formatted, _("Auto-response sent:")); while (*line && *line != ':') line++; if (*line) line++; g_string_append(formatted, "</b>"); footer = NULL; } else if (strstr(line, " signed off ")) { if (buddy->alias) g_string_append_printf(formatted, _("%s logged out."), buddy->alias); else g_string_append_printf(formatted, _("%s logged out."), log->name); line = ""; } else if (strstr(line, " signed on ")) { if (buddy->alias) g_string_append(formatted, buddy->alias); else g_string_append(formatted, log->name); line = " logged in."; } else if (g_str_has_prefix(line, "One or more messages may have been undeliverable.")) { g_string_append(formatted, "<span style=\"color: #ff0000;\">"); g_string_append(formatted, _("One or more messages may have been " "undeliverable.")); line = ""; footer = "</span></b>"; } else if (g_str_has_prefix(line, "You have been disconnected.")) { g_string_append(formatted, "<span style=\"color: #ff0000;\">"); g_string_append(formatted, _("You were disconnected from the server.")); line = ""; footer = "</span></b>"; } else if (g_str_has_prefix(line, "You are currently disconnected.")) { g_string_append(formatted, "<span style=\"color: #ff0000;\">"); line = _("You are currently disconnected. Messages " "will not be received unless you are " "logged in."); footer = "</span></b>"; } else if (g_str_has_prefix(line, "Your previous message has not been sent.")) { g_string_append(formatted, "<span style=\"color: #ff0000;\">"); if (g_str_has_prefix(line, "Your previous message has not been sent. " "Reason: Maximum length exceeded.")) { g_string_append(formatted, _("Message could not be sent because " "the maximum length was exceeded.")); line = ""; } else { g_string_append(formatted, _("Message could not be sent.")); line += (sizeof( "Your previous message " "has not been sent. ") - 1); } footer = "</span></b>"; } } else if (g_str_has_prefix(line, data->their_nickname)) { if (buddy->alias) { line += strlen(data->their_nickname) + 2; g_string_append_printf(formatted, "<span style=\"color: #A82F2F;\">" "<b>%s</b></span>: ", buddy->alias); } } else { char *line2 = line; while (*line2 && *line2 != ':') line2++; if (*line2 == ':') { line2++; line = line2; g_string_append_printf(formatted, "<span style=\"color: #16569E;\">" "<b>%s</b></span>:", log->account->alias); } } } g_string_append(formatted, line); if (footer) g_string_append(formatted, footer); g_string_append_c(formatted, '\n'); if (link_temp_line) g_free(link_temp_line); c++; line = c; } else c++; } g_free(read); read = formatted->str; g_string_free(formatted, FALSE); return read; } static int trillian_logger_size (GaimLog *log) { struct trillian_logger_data *data; char *text; size_t size; g_return_val_if_fail(log != NULL, 0); data = log->logger_data; if (gaim_prefs_get_bool("/plugins/core/log_reader/fast_sizes")) { return data ? data->length : 0; } text = trillian_logger_read(log, NULL); size = strlen(text); g_free(text); return size; } static void trillian_logger_finalize(GaimLog *log) { struct trillian_logger_data *data; g_return_if_fail(log != NULL); data = log->logger_data; g_free(data->path); g_free(data->their_nickname); } /***************************************************************************** * Plugin Code * *****************************************************************************/ static void init_plugin(GaimPlugin *plugin) { char *path; #ifdef _WIN32 char *folder; #endif g_return_if_fail(plugin != NULL); gaim_prefs_add_none("/plugins/core/log_reader"); /* Add general preferences. */ gaim_prefs_add_bool("/plugins/core/log_reader/fast_sizes", FALSE); gaim_prefs_add_bool("/plugins/core/log_reader/use_name_heuristics", TRUE); /* Add Adium log directory preference. */ gaim_prefs_add_none("/plugins/core/log_reader/adium"); /* Calculate default Adium log directory. */ #ifdef _WIN32 path = ""; #else path = g_build_filename(gaim_home_dir(), "Library", "Application Support", "Adium 2.0", "Users", "Default", "Logs", NULL); #endif gaim_prefs_add_string("/plugins/core/log_reader/adium/log_directory", path); #ifndef _WIN32 g_free(path); #endif /* Add Fire log directory preference. */ gaim_prefs_add_none("/plugins/core/log_reader/fire"); /* Calculate default Fire log directory. */ #ifdef _WIN32 path = ""; #else path = g_build_filename(gaim_home_dir(), "Library", "Application Support", "Fire", "Sessions", NULL); #endif gaim_prefs_add_string("/plugins/core/log_reader/fire/log_directory", path); #ifndef _WIN32 g_free(path); #endif /* Add Messenger Plus! log directory preference. */ gaim_prefs_add_none("/plugins/core/log_reader/messenger_plus"); /* Calculate default Messenger Plus! log directory. */ #ifdef _WIN32 folder = wgaim_get_special_folder(CSIDL_PERSONAL); if (folder) { #endif path = g_build_filename( #ifdef _WIN32 folder, #else GAIM_LOG_READER_WINDOWS_MOUNT_POINT, "Documents and Settings", g_get_user_name(), "My Documents", #endif "My Chat Logs", NULL); #ifdef _WIN32 g_free(folder); } else /* !folder */ path = g_strdup(""); #endif gaim_prefs_add_string("/plugins/core/log_reader/messenger_plus/log_directory", path); g_free(path); /* Add MSN Messenger log directory preference. */ gaim_prefs_add_none("/plugins/core/log_reader/msn"); /* Calculate default MSN message history directory. */ #ifdef _WIN32 folder = wgaim_get_special_folder(CSIDL_PERSONAL); if (folder) { #endif path = g_build_filename( #ifdef _WIN32 folder, #else GAIM_LOG_READER_WINDOWS_MOUNT_POINT, "Documents and Settings", g_get_user_name(), "My Documents", #endif "My Received Files", NULL); #ifdef _WIN32 g_free(folder); } else /* !folder */ path = g_strdup(""); #endif gaim_prefs_add_string("/plugins/core/log_reader/msn/log_directory", path); g_free(path); /* Add Trillian log directory preference. */ gaim_prefs_add_none("/plugins/core/log_reader/trillian"); #ifdef _WIN32 /* XXX: While a major hack, this is the most reliable way I could * think of to determine the Trillian installation directory. */ HKEY hKey; char buffer[1024] = ""; DWORD size = (sizeof(buffer) - 1); DWORD type; path = NULL; /* TODO: Test this after removing the trailing "\\". */ if(ERROR_SUCCESS == RegOpenKeyEx(HKEY_CLASSES_ROOT, "Trillian.SkinZip\\shell\\Add\\command\\", 0, KEY_QUERY_VALUE, &hKey)) { if(ERROR_SUCCESS == RegQueryValueEx(hKey, "", NULL, &type, (LPBYTE)buffer, &size)) { char *value = buffer; char *temp; /* Ensure the data is null terminated. */ value[size] = '\0'; /* Break apart buffer. */ if (*value == '"') { value++; temp = value; while (*temp && *temp != '"') temp++; } else { temp = value; while (*temp && *temp != ' ') temp++; } *temp = '\0'; /* Set path. */ if (g_str_has_suffix(value, "trillian.exe")) { value[strlen(value) - (sizeof("trillian.exe") - 1)] = '\0'; path = g_build_filename(value, "users", "default", "talk.ini", NULL); } } RegCloseKey(hKey); } if (!path) { char *folder = wgaim_get_special_folder(CSIDL_PROGRAM_FILES); if (folder) path = g_build_filename(folder, "Trillian", "users", "default", "talk.ini", NULL); g_free(folder); } } gboolean found = FALSE; if (path) { /* Read talk.ini file to find the log directory. */ GError *error = NULL; #if 0 && GTK_CHECK_VERSION(2,6,0) /* FIXME: Not tested yet. */ GKeyFile *key_file; gaim_debug(GAIM_DEBUG_INFO, "Trillian talk.ini read", "Reading %s\n", path); if (!g_key_file_load_from_file(key_file, path, G_KEY_FILE_NONE, GError &error)) { gaim_debug(GAIM_DEBUG_ERROR, "Trillian talk.ini read", "Error reading talk.ini\n"); if (error) g_error_free(error); } else { char *logdir = g_key_file_get_string(key_file, "Logging", "Directory", &error); if (error) { gaim_debug(GAIM_DEBUG_ERROR, "Trillian talk.ini read", "Error reading Directory value from Logging section\n"); g_error_free(error); } if (logdir) { g_strchomp(logdir); gaim_prefs_add_string( "/plugins/core/log_reader/trillian/log_directory", logdir); found = TRUE; } g_key_file_free(key_file); } #else /* !GTK_CHECK_VERSION(2,6,0) */ GError *error = NULL; gsize length; gaim_debug(GAIM_DEBUG_INFO, "Trillian talk.ini read", "Reading %s\n", path); if (!g_file_get_contents(path, &contents, &length, &error)) { gaim_debug(GAIM_DEBUG_ERROR, "Trillian talk.ini read", "Error reading talk.ini\n"); if (error) g_error_free(error); } else { char *line = contents; while (*contents) { if (*contents == '\n') { *contents = '\0'; /* XXX: This assumes the first Directory key is under [Logging]. */ if (g_str_has_prefix(line, "Directory=")) { line += (sizeof("Directory=") - 1); g_strchomp(line); gaim_prefs_add_string( "/plugins/core/log_reader/trillian/log_directory", line); found = TRUE; } contents++; line = contents; } else contents++; } g_free(path); g_free(contents); } #endif /* !GTK_CHECK_VERSION(2,6,0) */ } /* path */ if (!found) { #endif /* defined(_WIN32) */ /* Calculate default Trillian log directory. */ #ifdef _WIN32 folder = wgaim_get_special_folder(CSIDL_PROGRAM_FILES); if (folder) { #endif path = g_build_filename( #ifdef _WIN32 folder, #else GAIM_LOG_READER_WINDOWS_MOUNT_POINT, "Program Files", #endif "Trillian", "users", "default", "logs", NULL); #ifdef _WIN32 g_free(folder); } else /* !folder */ path = g_strdup(""); #endif gaim_prefs_add_string("/plugins/core/log_reader/trillian/log_directory", path); g_free(path); #ifdef _WIN32 } /* !found */ #endif } static gboolean plugin_load(GaimPlugin *plugin) { g_return_val_if_fail(plugin != NULL, FALSE); adium_logger = gaim_log_logger_new("adium", "Adium", 6, NULL, NULL, adium_logger_finalize, adium_logger_list, adium_logger_read, adium_logger_size); gaim_log_logger_add(adium_logger); fire_logger = gaim_log_logger_new("fire", "Fire", 6, NULL, NULL, fire_logger_finalize, fire_logger_list, fire_logger_read, fire_logger_size); gaim_log_logger_add(fire_logger); messenger_plus_logger = gaim_log_logger_new("messenger_plus", "Messenger Plus!", 6, NULL, NULL, messenger_plus_logger_finalize, messenger_plus_logger_list, messenger_plus_logger_read, messenger_plus_logger_size); gaim_log_logger_add(messenger_plus_logger); msn_logger = gaim_log_logger_new("msn", "MSN Messenger", 6, NULL, NULL, msn_logger_finalize, msn_logger_list, msn_logger_read, msn_logger_size); gaim_log_logger_add(msn_logger); trillian_logger = gaim_log_logger_new("trillian", "Trillian", 6, NULL, NULL, trillian_logger_finalize, trillian_logger_list, trillian_logger_read, trillian_logger_size); gaim_log_logger_add(trillian_logger); return TRUE; } static gboolean plugin_unload(GaimPlugin *plugin) { g_return_val_if_fail(plugin != NULL, FALSE); gaim_log_logger_remove(adium_logger); gaim_log_logger_remove(fire_logger); gaim_log_logger_remove(messenger_plus_logger); gaim_log_logger_remove(msn_logger); gaim_log_logger_remove(trillian_logger); return TRUE; } static GaimPluginPrefFrame * get_plugin_pref_frame(GaimPlugin *plugin) { GaimPluginPrefFrame *frame; GaimPluginPref *ppref; g_return_val_if_fail(plugin != NULL, FALSE); frame = gaim_plugin_pref_frame_new(); /* Add general preferences. */ ppref = gaim_plugin_pref_new_with_label(_("General Log Reading Configuration")); gaim_plugin_pref_frame_add(frame, ppref); ppref = gaim_plugin_pref_new_with_name_and_label( "/plugins/core/log_reader/fast_sizes", _("Fast size calculations")); gaim_plugin_pref_frame_add(frame, ppref); ppref = gaim_plugin_pref_new_with_name_and_label( "/plugins/core/log_reader/use_name_heuristics", _("Use name heuristics")); gaim_plugin_pref_frame_add(frame, ppref); /* Add Log Directory preferences. */ ppref = gaim_plugin_pref_new_with_label(_("Log Directory")); gaim_plugin_pref_frame_add(frame, ppref); ppref = gaim_plugin_pref_new_with_name_and_label( "/plugins/core/log_reader/adium/log_directory", _("Adium")); gaim_plugin_pref_frame_add(frame, ppref); ppref = gaim_plugin_pref_new_with_name_and_label( "/plugins/core/log_reader/fire/log_directory", _("Fire")); gaim_plugin_pref_frame_add(frame, ppref); ppref = gaim_plugin_pref_new_with_name_and_label( "/plugins/core/log_reader/messenger_plus/log_directory", _("Messenger Plus!")); gaim_plugin_pref_frame_add(frame, ppref); ppref = gaim_plugin_pref_new_with_name_and_label( "/plugins/core/log_reader/msn/log_directory", _("MSN Messenger")); gaim_plugin_pref_frame_add(frame, ppref); ppref = gaim_plugin_pref_new_with_name_and_label( "/plugins/core/log_reader/trillian/log_directory", _("Trillian")); gaim_plugin_pref_frame_add(frame, ppref); return frame; } static GaimPluginUiInfo prefs_info = { get_plugin_pref_frame }; static GaimPluginInfo info = { GAIM_PLUGIN_MAGIC, GAIM_MAJOR_VERSION, GAIM_MINOR_VERSION, GAIM_PLUGIN_STANDARD, /**< type */ NULL, /**< ui_requirement */ 0, /**< flags */ NULL, /**< dependencies */ GAIM_PRIORITY_DEFAULT, /**< priority */ "core-log_reader", /**< id */ N_("Log Reader"), /**< name */ VERSION, /**< version */ /** summary */ N_("Includes other IM clients' logs in the " "log viewer."), /** description */ N_("When viewing logs, this plugin will include " "logs from other IM clients. Currently, this " "includes Adium, Fire, Messenger Plus!, " "MSN Messenger, and Trillian."), "Richard Laager <rlaager@users.sf.net>", /**< author */ GAIM_WEBSITE, /**< homepage */ plugin_load, /**< load */ plugin_unload, /**< unload */ NULL, /**< destroy */ NULL, /**< ui_info */ NULL, /**< extra_info */ &prefs_info, /**< prefs_info */ NULL /**< actions */ }; GAIM_INIT_PLUGIN(log_reader, init_plugin, info)