/* fe-log.c : irssi Copyright (C) 1999-2000 Timo Sirainen This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA */ #include "module.h" #include "module-formats.h" #include "signals.h" #include "commands.h" #include "chat-protocols.h" #include "servers.h" #include "levels.h" #include "misc.h" #include "log.h" #include "special-vars.h" #include "settings.h" #include "lib-config/iconfig.h" #include "fe-windows.h" #include "window-items.h" #include "formats.h" #include "themes.h" #include "printtext.h" /* close autologs after 5 minutes of inactivity */ #define AUTOLOG_INACTIVITY_CLOSE (60*5) static int autolog_level; static int autoremove_tag; static const char *autolog_path; static THEME_REC *log_theme; static int skip_next_printtext; static const char *log_theme_name; static int log_dir_create_mode; static char *log_colorizer_strip(const char *str) { return strip_codes(str); } static void log_add_targets(LOG_REC *log, const char *targets, const char *tag) { char **tmp, **items; g_return_if_fail(log != NULL); g_return_if_fail(targets != NULL); items = g_strsplit(targets, " ", -1); for (tmp = items; *tmp != NULL; tmp++) log_item_add(log, LOG_ITEM_TARGET, *tmp, tag); g_strfreev(items); } /* SYNTAX: LOG OPEN [-noopen] [-autoopen] [-window] [-] [-targets ] [-colors] [] */ static void cmd_log_open(const char *data) { SERVER_REC *server; GHashTable *optlist; char *targetarg, *fname, *levels, *servertag; void *free_arg; char window[MAX_INT_STRLEN]; LOG_REC *log; int level; if (!cmd_get_params(data, &free_arg, 2 | PARAM_FLAG_GETREST | PARAM_FLAG_UNKNOWN_OPTIONS | PARAM_FLAG_OPTIONS, "log open", &optlist, &fname, &levels)) return; if (*fname == '\0') cmd_param_error(CMDERR_NOT_ENOUGH_PARAMS); level = level2bits(levels); log = log_create_rec(fname, level != 0 ? level : MSGLEVEL_ALL); /* - */ server = cmd_options_get_server("log open", optlist, NULL); servertag = server == NULL ? NULL : server->tag; if (g_hash_table_lookup(optlist, "window")) { /* log by window ref# */ targetarg = g_hash_table_lookup(optlist, "targets"); if (targetarg == NULL || !is_numeric(targetarg, '\0')) { ltoa(window, active_win->refnum); targetarg = window; } log_item_add(log, LOG_ITEM_WINDOW_REFNUM, targetarg, servertag); } else { targetarg = g_hash_table_lookup(optlist, "targets"); if (targetarg != NULL && *targetarg != '\0') log_add_targets(log, targetarg, servertag); else if (servertag != NULL) log_add_targets(log, "*", servertag); } if (g_hash_table_lookup(optlist, "autoopen")) log->autoopen = TRUE; if (g_hash_table_lookup(optlist, "colors") == NULL) log->colorizer = log_colorizer_strip; log_update(log); if (log->handle == -1 && g_hash_table_lookup(optlist, "noopen") == NULL) { /* start logging */ if (log_start_logging(log)) { printformat(NULL, NULL, MSGLEVEL_CLIENTNOTICE, TXT_LOG_OPENED, fname); } else { log_close(log); } } cmd_params_free(free_arg); } static LOG_REC *log_find_from_data(const char *data) { GSList *tmp; if (!is_numeric(data, ' ')) return log_find(data); /* with index number */ tmp = g_slist_nth(logs, atoi(data)-1); return tmp == NULL ? NULL : tmp->data; } /* SYNTAX: LOG CLOSE | */ static void cmd_log_close(const char *data) { LOG_REC *log; log = log_find_from_data(data); if (log == NULL) printformat(NULL, NULL, MSGLEVEL_CLIENTERROR, TXT_LOG_NOT_OPEN, data); else { log_close(log); printformat(NULL, NULL, MSGLEVEL_CLIENTNOTICE, TXT_LOG_CLOSED, data); } } /* SYNTAX: LOG START | */ static void cmd_log_start(const char *data) { LOG_REC *log; log = log_find_from_data(data); if (log != NULL) { log_start_logging(log); printformat(NULL, NULL, MSGLEVEL_CLIENTNOTICE, TXT_LOG_OPENED, data); } } /* SYNTAX: LOG STOP | */ static void cmd_log_stop(const char *data) { LOG_REC *log; log = log_find_from_data(data); if (log == NULL || log->handle == -1) printformat(NULL, NULL, MSGLEVEL_CLIENTERROR, TXT_LOG_NOT_OPEN, data); else { log_stop_logging(log); printformat(NULL, NULL, MSGLEVEL_CLIENTNOTICE, TXT_LOG_CLOSED, data); } } static char *log_items_get_list(LOG_REC *log) { GSList *tmp; GString *str; char *ret; LOG_ITEM_REC *rec = NULL; g_return_val_if_fail(log != NULL, NULL); g_return_val_if_fail(log->items != NULL, NULL); str = g_string_new(NULL); for (tmp = log->items; tmp != NULL; tmp = tmp->next) { rec = tmp->data; g_string_sprintfa(str, "%s, ", rec->name); } g_string_truncate(str, str->len-2); if(rec->servertag != NULL) g_string_sprintfa(str, " (%s)", rec->servertag); ret = str->str; g_string_free(str, FALSE); return ret; } static void cmd_log_list(void) { GSList *tmp; char *levelstr, *items; int index; printformat(NULL, NULL, MSGLEVEL_CLIENTCRAP, TXT_LOG_LIST_HEADER); for (tmp = logs, index = 1; tmp != NULL; tmp = tmp->next, index++) { LOG_REC *rec = tmp->data; levelstr = bits2level(rec->level); items = rec->items == NULL ? NULL : log_items_get_list(rec); printformat(NULL, NULL, MSGLEVEL_CLIENTCRAP, TXT_LOG_LIST, index, rec->fname, items != NULL ? items : "", levelstr, rec->autoopen ? " -autoopen" : ""); g_free_not_null(items); g_free(levelstr); } printformat(NULL, NULL, MSGLEVEL_CLIENTCRAP, TXT_LOG_LIST_FOOTER); } static void cmd_log(const char *data, SERVER_REC *server, void *item) { if (*data == '\0') cmd_log_list(); else command_runsub("log", data, server, item); } static LOG_REC *logs_find_item(int type, const char *item, const char *servertag, LOG_ITEM_REC **ret_item) { LOG_ITEM_REC *logitem; GSList *tmp; for (tmp = logs; tmp != NULL; tmp = tmp->next) { LOG_REC *log = tmp->data; if (type == LOG_ITEM_TARGET && log->temp == 0) continue; logitem = log_item_find(log, type, item, servertag); if (logitem != NULL) { if (ret_item != NULL) *ret_item = logitem; return log; } } return NULL; } /* SYNTAX: WINDOW LOG on|off|toggle [] */ static void cmd_window_log(const char *data) { LOG_REC *log; char *set, *fname, window[MAX_INT_STRLEN]; void *free_arg; int open_log, close_log; if (!cmd_get_params(data, &free_arg, 2, &set, &fname)) return; ltoa(window, active_win->refnum); log = logs_find_item(LOG_ITEM_WINDOW_REFNUM, window, NULL, NULL); open_log = close_log = FALSE; if (g_strcasecmp(set, "ON") == 0) open_log = TRUE; else if (g_strcasecmp(set, "OFF") == 0) { close_log = TRUE; } else if (g_strcasecmp(set, "TOGGLE") == 0) { open_log = log == NULL; close_log = log != NULL; } else { printformat(NULL, NULL, MSGLEVEL_CLIENTERROR, TXT_NOT_TOGGLE); cmd_params_free(free_arg); return; } if (open_log && log == NULL) { /* irc.log. or irc.log.Window */ fname = *fname != '\0' ? g_strdup(fname) : g_strdup_printf("~/irc.log.%s%s", active_win->name != NULL ? active_win->name : "Window", active_win->name != NULL ? "" : window); log = log_create_rec(fname, MSGLEVEL_ALL); log->colorizer = log_colorizer_strip; log_item_add(log, LOG_ITEM_WINDOW_REFNUM, window, NULL); log_update(log); g_free(fname); } if (open_log && log != NULL) { log_start_logging(log); printformat(NULL, NULL, MSGLEVEL_CLIENTNOTICE, TXT_LOG_OPENED, log->fname); } else if (close_log && log != NULL && log->handle != -1) { log_stop_logging(log); printformat(NULL, NULL, MSGLEVEL_CLIENTNOTICE, TXT_LOG_CLOSED, log->fname); } cmd_params_free(free_arg); } /* Create log file entry to window, but don't start logging */ /* SYNTAX: WINDOW LOGFILE */ static void cmd_window_logfile(const char *data) { LOG_REC *log; char window[MAX_INT_STRLEN]; ltoa(window, active_win->refnum); log = logs_find_item(LOG_ITEM_WINDOW_REFNUM, window, NULL, NULL); if (log != NULL) { printformat(NULL, NULL, MSGLEVEL_CLIENTNOTICE, TXT_WINDOWLOG_FILE_LOGGING); return; } log = log_create_rec(data, MSGLEVEL_ALL); log->colorizer = log_colorizer_strip; log_item_add(log, LOG_ITEM_WINDOW_REFNUM, window, NULL); log_update(log); printformat(NULL, NULL, MSGLEVEL_CLIENTNOTICE, TXT_WINDOWLOG_FILE, data); } /* window's refnum changed - update the logs to log the new window refnum */ static void sig_window_refnum_changed(WINDOW_REC *window, gpointer old_refnum) { char winnum[MAX_INT_STRLEN]; LOG_REC *log; LOG_ITEM_REC *item; ltoa(winnum, GPOINTER_TO_INT(old_refnum)); log = logs_find_item(LOG_ITEM_WINDOW_REFNUM, winnum, NULL, &item); if (log != NULL) { ltoa(winnum, window->refnum); g_free(item->name); item->name = g_strdup(winnum); } } static void sig_server_disconnected(SERVER_REC *server) { LOG_ITEM_REC *logitem; GSList *tmp, *next; for (tmp = logs; tmp != NULL; tmp = next) { LOG_REC *log = tmp->data; next = tmp->next; if (!log->temp || log->items == NULL) continue; logitem = log->items->data; if (logitem->type == LOG_ITEM_TARGET && logitem->servertag != NULL && g_strcasecmp(logitem->servertag, server->tag) == 0 && server_ischannel(server, logitem->name)) /* kludge again.. so we won't close dcc chats */ log_close(log); } } static void autologs_close_all(void) { GSList *tmp, *next; for (tmp = logs; tmp != NULL; tmp = next) { LOG_REC *rec = tmp->data; next = tmp->next; if (rec->temp) log_close(rec); } } /* '%' -> '%%', '/' -> '_' */ static char *escape_target(const char *target) { char *str, *p; p = str = g_malloc(strlen(target)*2+1); while (*target != '\0') { if (*target == '/') *p++ = '_'; else { if (*target == '%') *p++ = '%'; *p++ = *target; } target++; } *p = '\0'; return str; } static void autolog_open(SERVER_REC *server, const char *server_tag, const char *target) { LOG_REC *log; char *fname, *dir, *fixed_target, *params; log = logs_find_item(LOG_ITEM_TARGET, target, server_tag, NULL); if (log != NULL && !log->failed) { log_start_logging(log); return; } /* '/' -> '_' - don't even accidentally try to log to #../../../file if you happen to join to such channel.. '%' -> '%%' - so strftime() won't mess with them */ fixed_target = escape_target(target); if (CHAT_PROTOCOL(server)->case_insensitive) g_strdown(fixed_target); /* $0 = target, $1 = server tag */ params = g_strconcat(fixed_target, " ", server_tag, NULL); g_free(fixed_target); fname = parse_special_string(autolog_path, server, NULL, params, NULL, 0); g_free(params); if (log_find(fname) == NULL) { log = log_create_rec(fname, autolog_level); if (!settings_get_bool("autolog_colors")) log->colorizer = log_colorizer_strip; log_item_add(log, LOG_ITEM_TARGET, target, server_tag); dir = g_dirname(log->real_fname); mkpath(dir, log_dir_create_mode); g_free(dir); log->temp = TRUE; log_update(log); log_start_logging(log); } g_free(fname); } static void autolog_open_check(SERVER_REC *server, const char *server_tag, const char *target, int level) { char **targets, **tmp; /* FIXME: kind of a kludge, but we don't want to reopen logs when we're parting the channel with /WINDOW CLOSE.. Maybe a small timeout would be nice instead of immediately closing the log file after "window item destroyed" */ if (level == MSGLEVEL_PARTS || (autolog_level & level) == 0 || target == NULL || *target == '\0') return; /* there can be multiple targets separated with comma */ targets = g_strsplit(target, ",", -1); for (tmp = targets; *tmp != NULL; tmp++) autolog_open(server, server_tag, *tmp); g_strfreev(targets); } static void log_single_line(WINDOW_REC *window, const char *server_tag, const char *target, int level, const char *text) { char windownum[MAX_INT_STRLEN]; char **targets, **tmp; LOG_REC *log; if (window != NULL) { /* save to log created with /WINDOW LOG */ ltoa(windownum, window->refnum); log = logs_find_item(LOG_ITEM_WINDOW_REFNUM, windownum, NULL, NULL); if (log != NULL) log_write_rec(log, text, level); } if (target == NULL) log_file_write(server_tag, NULL, level, text, FALSE); else { /* there can be multiple items separated with comma */ targets = g_strsplit(target, ",", -1); for (tmp = targets; *tmp != NULL; tmp++) log_file_write(server_tag, *tmp, level, text, FALSE); g_strfreev(targets); } } static void log_line(TEXT_DEST_REC *dest, const char *text) { char **lines, **tmp; if (dest->level == MSGLEVEL_NEVER) return; /* let autolog open the log records */ autolog_open_check(dest->server, dest->server_tag, dest->target, dest->level); if (logs == NULL) return; /* text may contain one or more lines, log wants to eat them one line at a time */ lines = g_strsplit(text, "\n", -1); for (tmp = lines; *tmp != NULL; tmp++) log_single_line(dest->window, dest->server_tag, dest->target, dest->level, *tmp); g_strfreev(lines); } static void sig_printtext(TEXT_DEST_REC *dest, const char *text, const char *stripped) { if (skip_next_printtext) { skip_next_printtext = FALSE; return; } log_line(dest, text); } static void sig_print_format(THEME_REC *theme, const char *module, TEXT_DEST_REC *dest, void *formatnum, char **args) { char *str, *linestart, *tmp; if (log_theme == NULL) { /* theme isn't loaded for some reason (/reload destroys it), reload it. */ log_theme = theme_load(log_theme_name); if (log_theme == NULL) return; } if (theme == log_theme) return; str = format_get_text_theme_charargs(log_theme, module, dest, GPOINTER_TO_INT(formatnum), args); skip_next_printtext = TRUE; if (*str != '\0') { /* add the line start format */ linestart = format_get_level_tag(log_theme, dest); tmp = str; str = format_add_linestart(tmp, linestart); g_free_not_null(linestart); g_free(tmp); /* strip colors from text, log it. */ log_line(dest, str); } g_free(str); } static int sig_autoremove(void) { SERVER_REC *server; LOG_ITEM_REC *logitem; GSList *tmp, *next; time_t removetime; removetime = time(NULL)-AUTOLOG_INACTIVITY_CLOSE; for (tmp = logs; tmp != NULL; tmp = next) { LOG_REC *log = tmp->data; next = tmp->next; if (!log->temp || log->last > removetime || log->items == NULL) continue; /* Close only logs with private messages */ logitem = log->items->data; if (logitem->servertag == NULL) continue; server = server_find_tag(logitem->servertag); if (logitem->type == LOG_ITEM_TARGET && server != NULL && !server_ischannel(server, logitem->name)) log_close(log); } return 1; } static void sig_window_item_remove(WINDOW_REC *window, WI_ITEM_REC *item) { LOG_REC *log; log = logs_find_item(LOG_ITEM_TARGET, item->visible_name, item->server == NULL ? NULL : item->server->tag, NULL); if (log != NULL && log->temp) log_close(log); } static void sig_log_locked(LOG_REC *log) { printformat(NULL, NULL, MSGLEVEL_CLIENTERROR, TXT_LOG_LOCKED, log->fname); } static void sig_log_create_failed(LOG_REC *log) { printformat(NULL, NULL, MSGLEVEL_CLIENTERROR, TXT_LOG_CREATE_FAILED, log->real_fname, g_strerror(errno)); } static void sig_log_new(LOG_REC *log) { if (!settings_get_bool("awaylog_colors") && strcmp(log->fname, settings_get_str("awaylog_file")) == 0) log->colorizer = log_colorizer_strip; } static void sig_log_config_read(LOG_REC *log, CONFIG_NODE *node) { if (!config_node_get_bool(node, "colors", FALSE)) log->colorizer = log_colorizer_strip; } static void sig_log_config_save(LOG_REC *log, CONFIG_NODE *node) { if (log->colorizer == NULL) iconfig_node_set_bool(node, "colors", TRUE); else iconfig_node_set_str(node, "colors", NULL); } static void sig_awaylog_show(LOG_REC *log, gpointer pmsgs, gpointer pfilepos) { char *str; int msgs, filepos; msgs = GPOINTER_TO_INT(pmsgs); filepos = GPOINTER_TO_INT(pfilepos); if (msgs == 0) printformat(NULL, NULL, MSGLEVEL_CLIENTNOTICE, TXT_LOG_NO_AWAY_MSGS, log->fname); else { printformat(NULL, NULL, MSGLEVEL_CLIENTNOTICE, TXT_LOG_AWAY_MSGS, log->fname, msgs); str = g_strdup_printf("\"%s\" %d", log->fname, filepos); signal_emit("command cat", 1, str); g_free(str); } } static void sig_theme_destroyed(THEME_REC *theme) { if (theme == log_theme) log_theme = NULL; } static void read_settings(void) { int old_autolog = autolog_level; int log_file_create_mode; autolog_path = settings_get_str("autolog_path"); autolog_level = !settings_get_bool("autolog") ? 0 : settings_get_level("autolog_level"); if (old_autolog && !autolog_level) autologs_close_all(); /* write to log files with different theme? */ if (log_theme_name != NULL) signal_remove("print format", (SIGNAL_FUNC) sig_print_format); log_theme_name = settings_get_str("log_theme"); if (*log_theme_name == '\0') log_theme_name = NULL; else signal_add("print format", (SIGNAL_FUNC) sig_print_format); log_theme = log_theme_name == NULL ? NULL : theme_load(log_theme_name); log_file_create_mode = octal2dec(settings_get_int("log_create_mode")); log_dir_create_mode = log_file_create_mode; if (log_file_create_mode & 0400) log_dir_create_mode |= 0100; if (log_file_create_mode & 0040) log_dir_create_mode |= 0010; if (log_file_create_mode & 0004) log_dir_create_mode |= 0001; } void fe_log_init(void) { autoremove_tag = g_timeout_add(60000, (GSourceFunc) sig_autoremove, NULL); skip_next_printtext = FALSE; settings_add_bool("log", "awaylog_colors", TRUE); settings_add_bool("log", "autolog", FALSE); settings_add_bool("log", "autolog_colors", FALSE); settings_add_str("log", "autolog_path", "~/irclogs/$tag/$0.log"); settings_add_level("log", "autolog_level", "all -crap -clientcrap -ctcps"); settings_add_str("log", "log_theme", ""); autolog_level = 0; log_theme_name = NULL; read_settings(); command_bind("log", NULL, (SIGNAL_FUNC) cmd_log); command_bind("log open", NULL, (SIGNAL_FUNC) cmd_log_open); command_bind("log close", NULL, (SIGNAL_FUNC) cmd_log_close); command_bind("log start", NULL, (SIGNAL_FUNC) cmd_log_start); command_bind("log stop", NULL, (SIGNAL_FUNC) cmd_log_stop); command_bind("window log", NULL, (SIGNAL_FUNC) cmd_window_log); command_bind("window logfile", NULL, (SIGNAL_FUNC) cmd_window_logfile); signal_add_first("print text", (SIGNAL_FUNC) sig_printtext); signal_add("window item remove", (SIGNAL_FUNC) sig_window_item_remove); signal_add("window refnum changed", (SIGNAL_FUNC) sig_window_refnum_changed); signal_add("server disconnected", (SIGNAL_FUNC) sig_server_disconnected); signal_add("log locked", (SIGNAL_FUNC) sig_log_locked); signal_add("log create failed", (SIGNAL_FUNC) sig_log_create_failed); signal_add("log new", (SIGNAL_FUNC) sig_log_new); signal_add("log config read", (SIGNAL_FUNC) sig_log_config_read); signal_add("log config save", (SIGNAL_FUNC) sig_log_config_save); signal_add("awaylog show", (SIGNAL_FUNC) sig_awaylog_show); signal_add("theme destroyed", (SIGNAL_FUNC) sig_theme_destroyed); signal_add("setup changed", (SIGNAL_FUNC) read_settings); command_set_options("log open", "noopen autoopen -targets window colors"); } void fe_log_deinit(void) { g_source_remove(autoremove_tag); if (log_theme_name != NULL) signal_remove("print format", (SIGNAL_FUNC) sig_print_format); command_unbind("log", (SIGNAL_FUNC) cmd_log); command_unbind("log open", (SIGNAL_FUNC) cmd_log_open); command_unbind("log close", (SIGNAL_FUNC) cmd_log_close); command_unbind("log start", (SIGNAL_FUNC) cmd_log_start); command_unbind("log stop", (SIGNAL_FUNC) cmd_log_stop); command_unbind("window log", (SIGNAL_FUNC) cmd_window_log); command_unbind("window logfile", (SIGNAL_FUNC) cmd_window_logfile); signal_remove("print text", (SIGNAL_FUNC) sig_printtext); signal_remove("window item remove", (SIGNAL_FUNC) sig_window_item_remove); signal_remove("window refnum changed", (SIGNAL_FUNC) sig_window_refnum_changed); signal_remove("server disconnected", (SIGNAL_FUNC) sig_server_disconnected); signal_remove("log locked", (SIGNAL_FUNC) sig_log_locked); signal_remove("log create failed", (SIGNAL_FUNC) sig_log_create_failed); signal_remove("log new", (SIGNAL_FUNC) sig_log_new); signal_remove("log config read", (SIGNAL_FUNC) sig_log_config_read); signal_remove("log config save", (SIGNAL_FUNC) sig_log_config_save); signal_remove("awaylog show", (SIGNAL_FUNC) sig_awaylog_show); signal_remove("theme destroyed", (SIGNAL_FUNC) sig_theme_destroyed); signal_remove("setup changed", (SIGNAL_FUNC) read_settings); }