diff mcabber/mcabber/histolog.c @ 1668:41c26b7d2890

Install mcabber headers * Change mcabber headers naming scheme * Move 'src/' -> 'mcabber/' * Add missing include <mcabber/config.h>'s * Create and install clean config.h version in 'include/' * Move "dirty" config.h version to 'mcabber/' * Add $(top_srcdir) to compiler include path * Update modules HOWTO
author Myhailo Danylenko <isbear@ukrpost.net>
date Mon, 18 Jan 2010 15:36:19 +0200
parents mcabber/src/histolog.c@f4a2c6f767d1
children d1e8fb14ce2d
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mcabber/mcabber/histolog.c	Mon Jan 18 15:36:19 2010 +0200
@@ -0,0 +1,536 @@
+/*
+ * histolog.c   -- File history handling
+ *
+ * Copyright (C) 2005-2009 Mikael Berthe <mikael@lilotux.net>
+ *
+ * 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
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+ * USA
+ */
+
+#include <ctype.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <time.h>
+#include <unistd.h>
+
+#include "histolog.h"
+#include "hbuf.h"
+#include "utils.h"
+#include "screen.h"
+#include "settings.h"
+#include "utils.h"
+#include "roster.h"
+#include "xmpp.h"
+
+static guint UseFileLogging;
+static guint FileLoadLogs;
+static char *RootDir;
+
+
+//  user_histo_file(jid)
+// Returns history filename for the given jid
+// Note: the caller *must* free the filename after use (if not null).
+static char *user_histo_file(const char *bjid)
+{
+  char *filename;
+  char *lowerid;
+
+  if (!(UseFileLogging || FileLoadLogs))
+    return NULL;
+
+  lowerid = g_strdup(bjid);
+  if (!lowerid)
+    return NULL;
+  mc_strtolower(lowerid);
+
+  filename = g_strdup_printf("%s%s", RootDir, lowerid);
+  g_free(lowerid);
+  return filename;
+}
+
+char *hlog_get_log_jid(const char *bjid)
+{
+  struct stat bufstat;
+  char *path;
+  char *log_jid = NULL;
+
+  path = user_histo_file(bjid);
+  while (path) {
+    if (lstat(path, &bufstat) != 0)
+      break;
+    if (S_ISLNK(bufstat.st_mode)) {
+      g_free(log_jid);
+      log_jid = g_new0(char, bufstat.st_size+1);
+      if (readlink(path, log_jid, bufstat.st_size) < 0) return NULL;
+      g_free(path);
+      path = user_histo_file(log_jid);
+    } else
+      break;
+  }
+
+  g_free(path);
+  return log_jid;
+}
+
+//  write_histo_line()
+// Adds a history (multi-)line to the jid's history logfile
+static void write_histo_line(const char *bjid,
+        time_t timestamp, guchar type, guchar info, const char *data)
+{
+  guint len = 0;
+  FILE *fp;
+  time_t ts;
+  const char *p;
+  char *filename;
+  char str_ts[20];
+  int err;
+
+  if (!UseFileLogging)
+    return;
+
+  // Do not log status messages when 'logging_ignore_status' is set
+  if (type == 'S' && settings_opt_get_int("logging_ignore_status"))
+    return;
+
+  filename = user_histo_file(bjid);
+
+  // If timestamp is null, get current date
+  if (timestamp)
+    ts = timestamp;
+  else
+    time(&ts);
+
+  if (!data)
+    data = "";
+
+  // Count number of extra lines
+  for (p=data ; *p ; p++)
+    if (*p == '\n') len++;
+
+  /* Line format: "TI yyyymmddThh:mm:ssZ LLL [data]"
+   * T=Type, I=Info, yyyymmddThh:mm:ssZ=date, LLL=0-padded-len
+   *
+   * Types:
+   * - M message    Info: S (send) R (receive) I (info)
+   * - S status     Info: [_ofdnai]
+   * We don't check them, we trust the caller.
+   * (Info messages are not sent nor received, they're generated
+   * locally by mcabber.)
+   */
+
+  fp = fopen(filename, "a");
+  g_free(filename);
+  if (!fp) {
+    scr_LogPrint(LPRINT_LOGNORM, "Unable to write history "
+                 "(cannot open logfile)");
+    return;
+  }
+
+  to_iso8601(str_ts, ts);
+  err = fprintf(fp, "%c%c %-18.18s %03d %s\n", type, info, str_ts, len, data);
+  fclose(fp);
+  if (err < 0) {
+    scr_LogPrint(LPRINT_LOGNORM, "Error while writing to log file: %s",
+                 strerror(errno));
+  }
+}
+
+//  hlog_read_history()
+// Reads the jid's history logfile
+void hlog_read_history(const char *bjid, GList **p_buddyhbuf, guint width)
+{
+  char *filename;
+  guchar type, info;
+  char *data, *tail;
+  guint data_size;
+  char *xtext;
+  time_t timestamp;
+  guint prefix_flags;
+  guint len;
+  FILE *fp;
+  struct stat bufstat;
+  guint err = 0;
+  guint ln = 0; // line number
+  time_t starttime;
+  int max_num_of_blocks;
+
+  if (!FileLoadLogs)
+    return;
+
+  if ((roster_gettype(bjid) & ROSTER_TYPE_ROOM) &&
+      (settings_opt_get_int("load_muc_logs") != 1))
+    return;
+
+  data_size = HBB_BLOCKSIZE+32;
+  data = g_new(char, data_size);
+  if (!data) {
+    scr_LogPrint(LPRINT_LOGNORM, "Not enough memory to read history file");
+    return;
+  }
+
+  filename = user_histo_file(bjid);
+
+  fp = fopen(filename, "r");
+  g_free(filename);
+  if (!fp) {
+    g_free(data);
+    return;
+  }
+
+  // If file is large (> 3MB here), display a message to inform the user
+  // (it can take a while...)
+  if (!fstat(fileno(fp), &bufstat)) {
+    if (bufstat.st_size > 3145728) {
+      scr_LogPrint(LPRINT_NORMAL, "Reading <%s> history file...", bjid);
+      scr_DoUpdate();
+    }
+  }
+
+  max_num_of_blocks = get_max_history_blocks();
+
+  starttime = 0L;
+  if (settings_opt_get_int("max_history_age") > 0) {
+    int maxdays = settings_opt_get_int("max_history_age");
+    time(&starttime);
+    if (maxdays >= starttime/86400L)
+      starttime = 0L;
+    else
+      starttime -= maxdays * 86400L;
+  }
+
+  /* See write_histo_line() for line format... */
+  while (!feof(fp)) {
+    guint dataoffset = 25;
+    guint noeol;
+
+    if (fgets(data, data_size-1, fp) == NULL)
+      break;
+    ln++;
+
+    while (1) {
+      for (tail = data; *tail; tail++) ;
+      noeol = (*(tail-1) != '\n');
+      if (!noeol)
+        break;
+      /* TODO: duplicated code... could do better... */
+      if (tail == data + data_size-2) {
+        // The buffer is too small to contain the whole line.
+        // Let's allocate some more space.
+        if (!max_num_of_blocks ||
+            data_size/HBB_BLOCKSIZE < 5U*max_num_of_blocks) {
+          guint toffset = tail - data;
+          // Allocate one more block.
+          data_size = HBB_BLOCKSIZE * (1 + data_size/HBB_BLOCKSIZE);
+          data = g_renew(char, data, data_size);
+          // Update the tail pointer, as the data may have been moved.
+          tail = data + toffset;
+          if (fgets(tail, data_size-1 - (tail-data), fp) == NULL)
+            break;
+        } else {
+          scr_LogPrint(LPRINT_LOGNORM, "Line too long in history file!");
+          ln--;
+          break;
+        }
+      }
+    }
+
+    type = data[0];
+    info = data[1];
+
+    if ((type != 'M' && type != 'S') ||
+        ((data[11] != 'T') || (data[20] != 'Z') ||
+         (data[21] != ' ') ||
+         (data[25] != ' ' && data[26] != ' '))) {
+      if (!err) {
+        scr_LogPrint(LPRINT_LOGNORM,
+                     "Error in history file format (%s), l.%u", bjid, ln);
+        err = 1;
+      }
+      continue;
+    }
+    // The number of lines can be written with 3 or 4 bytes.
+    if (data[25] != ' ') dataoffset = 26;
+    data[21] = data[dataoffset] = 0;
+    timestamp = from_iso8601(&data[3], 1);
+    len = (guint) atoi(&data[22]);
+
+    // Some checks
+    if (((type == 'M') && (info != 'S' && info != 'R' && info != 'I')) ||
+        ((type == 'S') && (!strchr("_OFDNAI", info)))) {
+      if (!err) {
+        scr_LogPrint(LPRINT_LOGNORM, "Error in history file format (%s), l.%u",
+                     bjid, ln);
+        err = 1;
+      }
+      continue;
+    }
+
+    while (len--) {
+      ln++;
+      if (fgets(tail, data_size-1 - (tail-data), fp) == NULL)
+        break;
+
+      while (*tail) tail++;
+      noeol = (*(tail-1) != '\n');
+      if (tail == data + data_size-2 && (len || noeol)) {
+        // The buffer is too small to contain the whole message.
+        // Let's allocate some more space.
+        if (!max_num_of_blocks ||
+            data_size/HBB_BLOCKSIZE < 5U*max_num_of_blocks) {
+          guint toffset = tail - data;
+          // If the line hasn't been read completely and we reallocate the
+          // buffer, we want to read one more time.
+          if (noeol)
+            len++;
+          // Allocate one more block.
+          data_size = HBB_BLOCKSIZE * (1 + data_size/HBB_BLOCKSIZE);
+          data = g_renew(char, data, data_size);
+          // Update the tail pointer, as the data may have been moved.
+          tail = data + toffset;
+        } else {
+          // There will probably be a parse error on next read, because
+          // this message hasn't been read entirely.
+          scr_LogPrint(LPRINT_LOGNORM, "Message too big in history file!");
+        }
+      }
+    }
+    // Remove last CR (we keep it if the line is empty, too)
+    if ((tail > data+dataoffset+1) && (*(tail-1) == '\n'))
+      *(tail-1) = 0;
+
+    // Check if the data is older than max_history_age
+    if (starttime) {
+      if (timestamp > starttime)
+        starttime = 0L; // From now on, load everything
+      else
+        continue;
+    }
+
+    if (type == 'M') {
+      char *converted;
+      if (info == 'S') {
+        prefix_flags = HBB_PREFIX_OUT | HBB_PREFIX_HLIGHT_OUT;
+      } else {
+        prefix_flags = HBB_PREFIX_IN;
+        if (info == 'I')
+          prefix_flags = HBB_PREFIX_INFO;
+      }
+      converted = from_utf8(&data[dataoffset+1]);
+      if (converted) {
+        xtext = ut_expand_tabs(converted); // Expand tabs
+        hbuf_add_line(p_buddyhbuf, xtext, timestamp, prefix_flags, width,
+                      max_num_of_blocks, 0, NULL);
+        if (xtext != converted)
+          g_free(xtext);
+        g_free(converted);
+      }
+      err = 0;
+    }
+  }
+  fclose(fp);
+  g_free(data);
+}
+
+//  hlog_enable()
+// Enable logging to files.  If root_dir is NULL, then $HOME/.mcabber is used.
+// If loadfiles is TRUE, we will try to load buddies history logs from file.
+void hlog_enable(guint enable, const char *root_dir, guint loadfiles)
+{
+  UseFileLogging = enable;
+  FileLoadLogs = loadfiles;
+
+  if (enable || loadfiles) {
+    if (root_dir) {
+      char *xp_root_dir;
+      int l = strlen(root_dir);
+      if (l < 1) {
+        scr_LogPrint(LPRINT_LOGNORM, "Error: logging dir name too short");
+        UseFileLogging = FileLoadLogs = FALSE;
+        return;
+      }
+      xp_root_dir = expand_filename(root_dir);
+      // RootDir must be slash-terminated
+      if (root_dir[l-1] == '/') {
+        RootDir = xp_root_dir;
+      } else {
+        RootDir = g_strdup_printf("%s/", xp_root_dir);
+        g_free(xp_root_dir);
+      }
+    } else {
+      char *home = getenv("HOME");
+      const char *dir = "/.mcabber/histo/";
+      RootDir = g_strdup_printf("%s%s", home, dir);
+    }
+    // Check directory permissions (should not be readable by group/others)
+    if (checkset_perm(RootDir, TRUE) == -1) {
+      // The directory does not actually exists
+      g_free(RootDir);
+      RootDir = NULL;
+      scr_LogPrint(LPRINT_LOGNORM, "ERROR: Cannot access "
+                   "history log directory, logging DISABLED");
+      UseFileLogging = FileLoadLogs = FALSE;
+    }
+  } else {  // Disable history logging
+    g_free(RootDir);
+    RootDir = NULL;
+  }
+}
+
+guint hlog_is_enabled(void)
+{
+  return UseFileLogging;
+}
+
+inline void hlog_write_message(const char *bjid, time_t timestamp, int sent,
+        const char *msg)
+{
+  guchar info;
+  /* sent=1   message sent by mcabber
+   * sent=0   message received by mcabber
+   * sent=-1  local info message
+   */
+  if (sent == 1)
+    info = 'S';
+  else if (sent == 0)
+    info = 'R';
+  else
+    info = 'I';
+  write_histo_line(bjid, timestamp, 'M', info, msg);
+}
+
+inline void hlog_write_status(const char *bjid, time_t timestamp,
+        enum imstatus status, const char *status_msg)
+{
+  // XXX Check status value?
+  write_histo_line(bjid, timestamp, 'S', toupper(imstatus2char[status]),
+          status_msg);
+}
+
+
+//  hlog_save_state()
+// If enabled, save the current state of the roster
+// (i.e. pending messages) to a temporary file.
+void hlog_save_state(void)
+{
+  gpointer unread_ptr, first_unread;
+  const char *bjid;
+  char *statefile_xp;
+  FILE *fp;
+  const char *statefile = settings_opt_get("statefile");
+
+  if (!statefile || !UseFileLogging)
+    return;
+
+  statefile_xp = expand_filename(statefile);
+  fp = fopen(statefile_xp, "w");
+  if (!fp) {
+    scr_LogPrint(LPRINT_NORMAL, "Cannot open state file [%s]",
+                 strerror(errno));
+    goto hlog_save_state_return;
+  }
+
+  if (!lm_connection_is_authenticated(lconnection)) {
+    // We're not connected.  Let's use the unread_jids hash.
+    GList *unread_jid = unread_jid_get_list();
+    unread_ptr = unread_jid;
+    for ( ; unread_jid ; unread_jid = g_list_next(unread_jid))
+      fprintf(fp, "%s\n", (char*)unread_jid->data);
+    g_list_free(unread_ptr);
+    goto hlog_save_state_return;
+  }
+
+  if (!current_buddy) // Safety check -- shouldn't happen.
+    goto hlog_save_state_return;
+
+  // We're connected.  Let's use unread_msg().
+  unread_ptr = first_unread = unread_msg(NULL);
+  if (!first_unread)
+    goto hlog_save_state_return;
+
+  do {
+    guint type = buddy_gettype(unread_ptr);
+    if (type & (ROSTER_TYPE_USER|ROSTER_TYPE_AGENT)) {
+      bjid = buddy_getjid(unread_ptr);
+      if (bjid)
+        fprintf(fp, "%s\n", bjid);
+    }
+    unread_ptr = unread_msg(unread_ptr);
+  } while (unread_ptr && unread_ptr != first_unread);
+
+hlog_save_state_return:
+  if (fp) {
+    long filelen = ftell(fp);
+    fclose(fp);
+    if (!filelen)
+      unlink(statefile_xp);
+  }
+  g_free(statefile_xp);
+}
+
+//  hlog_load_state()
+// If enabled, load the current state of the roster
+// (i.e. pending messages) from a temporary file.
+// This function adds the JIDs to the unread_jids hash table,
+// so it should only be called at startup.
+void hlog_load_state(void)
+{
+  char bjid[1024];
+  char *statefile_xp;
+  FILE *fp;
+  const char *statefile = settings_opt_get("statefile");
+
+  if (!statefile || !UseFileLogging)
+    return;
+
+  statefile_xp = expand_filename(statefile);
+  fp = fopen(statefile_xp, "r");
+  if (fp) {
+    char *eol;
+    while (!feof(fp)) {
+      if (fgets(bjid, sizeof bjid, fp) == NULL)
+        break;
+      // Let's remove the trailing newline.
+      // Also remove whitespace, if the file as been (badly) manually modified.
+      for (eol = bjid; *eol; eol++) ;
+      for (eol--; eol >= bjid && (*eol == '\n' || *eol == ' '); *eol-- = 0) ;
+      // Safety checks...
+      if (!bjid[0])
+        continue;
+      if (check_jid_syntax(bjid)) {
+        scr_LogPrint(LPRINT_LOGNORM,
+                     "ERROR: Invalid JID in state file.  Corrupted file?");
+        break;
+      }
+      // Display a warning if there are pending messages but the user
+      // won't see them because load_log isn't set.
+      if (!FileLoadLogs) {
+        scr_LogPrint(LPRINT_LOGNORM, "WARNING: unread message from <%s>.",
+                     bjid);
+        scr_setmsgflag_if_needed(SPECIAL_BUFFER_STATUS_ID, TRUE);
+      }
+      // Add the JID to unread_jids.  It will be used when the contact is
+      // added to the roster.
+      unread_jid_add(bjid);
+    }
+    fclose(fp);
+  }
+  g_free(statefile_xp);
+}
+
+/* vim: set expandtab cindent cinoptions=>2\:2(0:  For Vim users... */