Re-merge snippets fix
authorDan
Sat, 15 Nov 2008 15:28:32 -0500
changeset 26 51560f5414a6
parent 25 e5e4ba531f41 (diff)
parent 18 07be890ef627 (current diff)
child 27 4a9469bada05
Re-merge snippets fix
--- a/database.sql	Sat Oct 25 20:24:31 2008 +0000
+++ b/database.sql	Sat Nov 15 15:28:32 2008 -0500
@@ -17,3 +17,47 @@
   PRIMARY KEY  (id)
 );
 
+--
+-- NEW - Late October '08 modifications
+--
+
+CREATE TABLE stats_messages (
+  message_id int(21) NOT NULL auto_increment,
+  channel varchar(30) NOT NULL DEFAULT '',
+  nick varchar(40) NOT NULL DEFAULT '',
+  time int(11) NOT NULL DEFAULT 0,
+  PRIMARY KEY ( message_id )
+);
+
+CREATE TABLE stats_anon (
+  nick varchar(40) NOT NULL DEFAULT '',
+  PRIMARY KEY ( nick )
+);
+
+CREATE TABLE stats_count_cache (
+  cache_id int(21) NOT NULL auto_increment,
+  channel varchar(30) NOT NULL DEFAULT '',
+  time_min int(11) NOT NULL DEFAULT 0,
+  time_max int(11) NOT NULL DEFAULT 0,
+  message_count int(11) NOT NULL DEFAULT 0,
+  PRIMARY KEY ( cache_id )
+);
+
+--
+-- ADDED November 15 2008
+--
+
+CREATE TABLE ip_log (
+  entry_id int(21) NOT NULL auto_increment,
+  nick varchar(40) NOT NULL,
+  basenick varchar(40) NOT NULL,
+  ip varchar(39) NOT NULL,
+  hostname varchar(80) NOT NULL,
+  channel varchar(20) NOT NULL,
+  time int(12) unsigned NOT NULL DEFAULT 0,
+  PRIMARY KEY ( entry_id )
+);
+
+-- Also added Nov. 15 (this DRAMATICALLY speeds things up)
+CREATE INDEX stats_time_idx USING BTREE ON stats_messages (time);
+
--- a/enanobot.php	Sat Oct 25 20:24:31 2008 +0000
+++ b/enanobot.php	Sat Nov 15 15:28:32 2008 -0500
@@ -74,6 +74,7 @@
 require('libirc.php');
 require('hooks.php');
 require('config.php');
+require('database.php');
 
 @ini_set('display_errors', 'on');
 error_reporting(E_ALL);
@@ -88,62 +89,9 @@
   }
 }
 
-$mysql_conn = false;
-$doctor = array();
+mysql_reconnect();
 
-function mysql_reconnect()
-{
-  global $mysql_conn, $mysql_host, $mysql_user, $mysql_pass, $mysql_dbname;
-  if ( $mysql_conn )
-  {
-    @mysql_close($mysql_conn);
-    if ( defined('LIBIRC_DEBUG') )
-    {
-      echo "< > Reconnecting to MySQL\n";
-    }
-  }
-  // connect to MySQL
-  $mysql_conn = @mysql_connect($mysql_host, $mysql_user, $mysql_pass);
-  if ( !$mysql_conn )
-  {
-    $m_e = mysql_error();
-    echo "Error connecting to MySQL: $m_e\n";
-    exit(1);
-  }
-  $q = @mysql_query("USE `$mysql_dbname`;", $mysql_conn);
-  if ( !$q )
-  {
-    $m_e = mysql_error();
-    echo "Error selecting database: $m_e\n";
-    exit(1);
-  }
-}
-
-function eb_mysql_query($sql, $conn = false)
-{
-  global $mysql_conn, $irc;
-  $m_et = false;
-  while ( true )
-  {
-    $q = mysql_query($sql, $mysql_conn);
-    if ( !$q )
-    {
-      $m_e = mysql_error();
-      if ( strpos($m_e, 'gone away') && !$m_et )
-      {
-        mysql_reconnect();
-        continue;
-      }
-      $m_et = true;
-      $irc->close("MySQL query error: $m_e");
-      exit(1);
-    }
-    break;
-  }
-  return $q;
-}
-
-mysql_reconnect();
+eval(eb_fetch_hook('startup_early'));
 
 $libirc_channels = array();
 
@@ -226,10 +174,11 @@
     }
     $part_cache = array();
   }
-  else if ( in_array($message['nick'], $privileged_list) && $message['message'] == 'Shutdown' && $message['action'] == 'PRIVMSG' )
+  else if ( in_array($message['nick'], $privileged_list) && preg_match('/^Shutdown(?: (.+))$/i', $message['message'], $match) && $message['action'] == 'PRIVMSG' )
   {
     $GLOBALS['_shutdown'] = true;
-    $irc->close("Remote bot shutdown ordered by {$message['nick']}", true);
+    $quitmessage = empty($match[1]) ? "Remote bot shutdown ordered by {$message['nick']}" : $match[1];
+    $irc->close($quitmessage, true);
     return 'BREAK';
   }
   else if ( $message['action'] == 'PRIVMSG' )
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/graphs.php	Sat Nov 15 15:28:32 2008 -0500
@@ -0,0 +1,1051 @@
+<?php
+
+/*
+ * Enano - an open-source CMS capable of wiki functions, Drupal-like sidebar blocks, and everything in between
+ * Version 1.1.5 (Caoineag alpha 5)
+ * Copyright (C) 2006-2008 Dan Fuhry
+ *
+ * This program is Free Software; you can redistribute 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 details.
+ */
+
+// BarGraph for PHP
+// Source: http://www.phpclasses.org/browse/package/1567.html
+// License: PHP license, see licenses/phplic.html included with this package
+
+class GraphMaker {
+  /**
+   * GraphMaker::bar_width
+   * Width of bars
+   */
+  var $bar_width = 32;
+  /**
+   * GraphMaker::bar_height
+   * Height of bars
+   */
+  var $bar_height = 8;
+  /**
+   * GraphMaker::bar_data
+   * Data of all bars
+   */
+  var $bar_data = array('a' => 7, 'b' => 3, 'c' => 6, 'd' => 0, 'e' => 2);
+  /**
+   * GraphMaker::bar_padding
+   * Padding of bars
+   */
+  var $bar_padding = 5;
+  /**
+   * GraphMaker::bar_bordercolor
+   * Border color of bars
+   */
+  var $bar_bordercolor = array(39, 78, 120);
+  /**
+   * GraphMaker::bar_bgcolor
+   * Background color of bars
+   */
+  var $bar_bgcolor = array(69, 129, 194);
+  //---------------------------------------------
+  /**
+   * GraphMaker::graph_areaheight
+   * Height of graphic area
+   */
+  var $graph_areaheight = 100;
+  /**
+   * GraphMaker::graph_padding
+   * Paddings of graph
+   */
+  var $graph_padding = array('left' => 50, 'top' => 20, 'right'  => 20, 'bottom' => 20);
+  /**
+   * GraphMaker::graph_title
+   * Title text of graph
+   */
+  var $graph_title = "";
+  /**
+   * GraphMaker::graph_bgcolor
+   * Background color of graph
+   */
+  var $graph_bgcolor = array(255, 255, 255);
+  /**
+   * GraphMaker::graph_bgtransparent
+   * Boolean for background transparency
+   */
+  var $graph_bgtransparent = 0;
+  /**
+   * GraphMaker::graph_transparencylevel
+   * Transparency level (0=opaque, 127=transparent)
+   */
+  var $graph_transparencylevel = 0;
+  /**
+   * GraphMaker::graph_borderwidth
+   * Width of graph border
+   */
+  var $graph_borderwidth = 1;
+  /**
+   * GraphMaker::graph_bordercolor
+   * Border color of graph
+   */
+  var $graph_bordercolor = array(218, 218, 239);
+  /**
+   * GraphMaker::graph_titlecolor
+   * Color of title text of graph
+   */
+  var $graph_titlecolor = array(99, 88, 78);
+  //---------------------------------------------
+  /**
+   * GraphMaker::axis_step
+   * Scale step of axis
+   */
+  var $axis_step = 2;
+  /**
+   * GraphMaker::axis_bordercolor
+   * Border color of axis
+   */
+  var $axis_bordercolor = array(99, 88, 78);
+  /**
+   * GraphMaker::axis_bgcolor
+   * Background color of axis
+   */
+  var $axis_bgcolor = array(152, 137, 124);
+
+  /****************************************************************
+                              GRAPH
+  ****************************************************************/
+
+  /**
+   * GraphMaker::SetGraphAreaHeight()
+   * Sets graph height (not counting top and bottom margins)
+   **/
+  function SetGraphAreaHeight($height) {
+    if ($height > 0) $this->graph_areaheight = $height;
+  }
+
+  /**
+   * GraphMaker::SetGraphPadding()
+   * Sets graph padding (margins)
+   **/
+  function SetGraphPadding($left, $top, $right, $bottom) {
+    $this->graph_padding = array('left'   => (int) $left,
+                                 'top'    => (int) $top,
+                                 'right'  => (int) $right,
+                                 'bottom' => (int) $bottom);
+  }
+
+  /**
+   * GraphMaker::SetGraphTitle()
+   * Set title text
+   **/
+  function SetGraphTitle($title) {
+    $this->graph_title = $title;
+  }
+
+  /**
+   * GraphMaker::SetGraphBorderColor()
+   * Sets border color for graph
+   **/
+  function SetGraphBorderColor($red, $green, $blue) {
+    $this->graph_bordercolor = array($red, $green, $blue);
+  }
+
+  /**
+   * GraphMaker::SetGraphBorderWidth()
+   * Set width of border. 0 disables border
+   **/
+  function SetGraphBorderWidth($width = 0) {
+    $this->graph_borderwidth = $width;
+  }
+
+  /**
+   * GraphMaker::SetGraphBackgroundColor()
+   * Sets background color for graph
+   **/
+  function SetGraphBackgroundColor($red, $green, $blue) {
+    $this->graph_bgcolor = array($red, $green, $blue);
+  }
+
+  /**
+   * GraphMaker::SetGraphBackgroundTransparent()
+   * Sets background color for graph (and set it transparent)
+   **/
+  function SetGraphBackgroundTransparent($red, $green, $blue, $addtransparency = 1) {
+    $this->graph_bgcolor = array($red, $green, $blue);
+    $this->graph_bgtransparent = ($addtransparency ? 1 : 0);
+  }
+
+  /**
+   * GraphMaker::SetGraphTitleColor()
+   * Sets title color for graph
+   **/
+  function SetGraphTitleColor($red, $green, $blue) {
+    $this->graph_titlecolor = array($red, $green, $blue);
+  }
+
+  /**
+   * GraphMaker::SetGraphTransparency()
+   * Sets transparency for graph
+   **/
+  function SetGraphTransparency($percent) {
+    if ($percent < 0) $percent = 0;
+    elseif ($percent > 100) $percent = 127;
+    else $percent = $percent * 1.27;
+    $this->graph_transparencylevel = $percent;
+  }
+
+  /****************************************************************
+                               BAR
+  ****************************************************************/
+
+  /**
+   * GraphMaker::SetBarBorderColor()
+   * Sets border color for bars
+   **/
+  function SetBarBorderColor($red, $green, $blue) {
+    $this->bar_bordercolor = array($red, $green, $blue);
+  }
+
+  /**
+   * GraphMaker::SetBarBackgroundColor()
+   * Sets background color for bars
+   **/
+  function SetBarBackgroundColor($red, $green, $blue) {
+    $this->bar_bgcolor = array($red, $green, $blue);
+  }
+
+  /**
+   * GraphMaker::SetBarData()
+   * Sets data of graph (parameter should be an array with key
+   * being the name of the bar and the value the value of the bar.
+   **/
+  function SetBarData($data) {
+    if (is_array($data)) $this->bar_data = $data;
+  }
+
+  /**
+   * GraphMaker::SetBarDimensions()
+   * Sets with and height of each bar
+   **/
+  function SetBarDimensions($width, $height) {
+    if ($width > 0) $this->bar_width = $width;
+    if ($height > 0) $this->bar_height = $height;
+  }
+
+  /**
+   * GraphMaker::SetBarPadding()
+   * Sets padding (border) around each bar
+   **/
+  function SetBarPadding($padding) {
+    if ($padding > 0) $this->bar_padding = $padding;
+  }
+
+  /****************************************************************
+                               AXIS
+  ****************************************************************/
+
+  /**
+   * GraphMaker::SetAxisBorderColor()
+   * Sets border color for axis
+   **/
+  function SetAxisBorderColor($red, $green, $blue) {
+    $this->axis_bordercolor = array($red, $green, $blue);
+  }
+
+  /**
+   * GraphMaker::SetAxisBackgroundColor()
+   * Sets background color for axis
+   **/
+  function SetAxisBackgroundColor($red, $green, $blue) {
+    $this->axis_bgcolor = array($red, $green, $blue);
+  }
+
+  /**
+   * GraphMaker::SetAxisStep()
+   * Sets axis scale step
+   **/
+  function SetAxisStep($step) {
+    if ($step > 0) $this->axis_step = $step;
+  }
+
+  /**
+   * GraphMaker::GetFinalGraphDimensions()
+   * From the values already setted, it calculates image
+   * width and height
+   **/
+  function GetFinalGraphDimensions() {
+    $w = $this->graph_padding['left'] +
+         (count($this->bar_data) * ($this->bar_width + ($this->bar_padding * 2))) +
+         $this->graph_padding['right'];
+    $h = $this->graph_padding['top'] +
+         $this->graph_areaheight +
+         $this->graph_padding['bottom'];
+    return array($w, $h);
+  }
+
+  /**
+   * GraphMaker::LoadGraph()
+   * Loads definitions from a file
+   **/
+  function LoadGraph($path) {
+    if (($fp = @fopen($path, "r")) !== false) {
+      $content = "";
+      while (!feof($fp)) {              // I do not use filesize() here
+        $content .= fread($fp, 4096);   // because of remote files. If
+      }                                 // there is no problem with them
+      fclose($fp);                      // please let me know
+      $this->__LoadGraphDefinitions($content);
+      return true;
+    } else return false;
+  }
+
+  /**
+   * GraphMaker::DrawGraph()
+   * Draw all the graph: bg, axis, bars, text.. and output it
+   * Optional file parameter turns output to file, and bool on success
+   **/
+  function DrawGraph($file = "") {
+    list($w, $h) = $this->GetFinalGraphDimensions();
+    $this->graph_width = $w;
+    $this->graph_height = $h;
+
+    $this->im = imagecreatetruecolor($w, $h);
+    if ($this->graph_transparencylevel) {
+      imagealphablending($this->im, true);
+    }
+
+    $this->__PaintBackground();
+    $this->__DrawAxis();
+
+    $p = 0;
+    foreach ($this->bar_data as $name => $value) {
+      $p++;
+      $this->__DrawBarText($p, $name);
+      $this->__DrawBar($p, $value);
+    }
+
+    if (strlen($this->graph_title)) {
+      $this->__AllocateColor("im_graph_titlecolor",
+                             $this->graph_titlecolor,
+                             $this->graph_transparencylevel);
+      $this->__DrawText($this->graph_title,
+                        floor($this->graph_width / 2),
+                        $this->graph_borderwidth + 2,
+                        $this->im_graph_titlecolor,
+                        2,
+                        1);
+    }
+
+    if (strlen($file)) {
+      $ret = imagepng($this->im, $file);
+    } else {
+      header('Content-Type: image/png');
+      imagepng($this->im);
+      $ret = true;
+    }
+    imagedestroy($this->im);
+    return $ret;
+  }
+
+  /**
+   * GraphMaker::PaintBackground()
+   * Draw all the graph: bg, axis, bars, text.. and output it
+   * Optional file parameter turns output to file, and bool on success
+   **/
+  function __PaintBackground() {
+    $this->__AllocateColor("im_graph_bgcolor",
+                           $this->graph_bgcolor,
+                           0);
+    imagefilledrectangle($this->im,
+                         0,
+                         0,
+                         $this->graph_width,
+                         $this->graph_height,
+                         $this->im_graph_bgcolor);
+    if ($this->graph_bgtransparent) {
+      imagecolortransparent($this->im, $this->im_graph_bgcolor);
+    }
+    if ($this->graph_borderwidth) {
+      $this->__AllocateColor("im_graph_bordercolor",
+                             $this->graph_bordercolor,
+                             $this->graph_transparencylevel);
+      for ($i = 0; $i < $this->graph_borderwidth; $i++) {
+        imagerectangle($this->im,
+                       $i,
+                       $i,
+                       $this->graph_width - 1 - $i,
+                       $this->graph_height - 1 - $i,
+                       $this->im_graph_bordercolor);
+      }
+    }
+  }
+
+  /**
+   * GraphMaker::__DrawAxis()
+   * Draws all the axis stuff (and scale steps)
+   **/
+  function __DrawAxis() {
+    $this->__AllocateColor("im_axis_bordercolor",
+                           $this->axis_bordercolor,
+                           $this->graph_transparencylevel);
+    $this->__AllocateColor("im_axis_bgcolor",
+                           $this->axis_bgcolor,
+                           $this->graph_transparencylevel);
+    $this->__DrawPolygon($this->graph_padding['left'], $this->graph_height - $this->graph_padding['bottom'],
+                         $this->graph_padding['left'], $this->graph_padding['top'],
+                         $this->graph_padding['left'] + $this->bar_height - 1, $this->graph_padding['top'] - $this->bar_height + 1,
+                         $this->graph_padding['left'] + $this->bar_height - 1, $this->graph_height - $this->graph_padding['bottom'] - $this->bar_height + 1,
+                         $this->im_axis_bgcolor, true);
+    $this->__DrawPolygon($this->graph_padding['left'], $this->graph_height - $this->graph_padding['bottom'],
+                         $this->graph_padding['left'], $this->graph_padding['top'],
+                         $this->graph_padding['left'] + $this->bar_height - 1, $this->graph_padding['top'] - $this->bar_height + 1,
+                         $this->graph_padding['left'] + $this->bar_height - 1, $this->graph_height - $this->graph_padding['bottom'] - $this->bar_height + 1,
+                         $this->im_axis_bordercolor);
+
+    $this->__DrawPolygon($this->graph_padding['left'], $this->graph_height - $this->graph_padding['bottom'],
+                         $this->graph_padding['left'] + $this->bar_height - 1, $this->graph_height - $this->graph_padding['bottom'] - $this->bar_height + 1,
+                         $this->graph_width - $this->graph_padding['right'] + $this->bar_height - 1, $this->graph_height - $this->graph_padding['bottom'] - $this->bar_height + 1,
+                         $this->graph_width - $this->graph_padding['right'], $this->graph_height - $this->graph_padding['bottom'],
+                         $this->im_axis_bgcolor, true);
+    $this->__DrawPolygon($this->graph_padding['left'], $this->graph_height - $this->graph_padding['bottom'],
+                         $this->graph_padding['left'] + $this->bar_height - 1, $this->graph_height - $this->graph_padding['bottom'] - $this->bar_height + 1,
+                         $this->graph_width - $this->graph_padding['right'] + $this->bar_height - 1, $this->graph_height - $this->graph_padding['bottom'] - $this->bar_height + 1,
+                         $this->graph_width - $this->graph_padding['right'], $this->graph_height - $this->graph_padding['bottom'],
+                         $this->im_axis_bordercolor);
+
+    // draw lines that separate bars
+    $total_bars = count($this->bar_data);
+    for ($i = 1; $i < $total_bars; $i++) {
+      $offset = $this->graph_padding['left'] +
+                (($this->bar_width + ($this->bar_padding * 2)) * $i);
+      imageline($this->im,
+                $offset,
+                $this->graph_height - $this->graph_padding['bottom'],
+                $offset + $this->bar_height - 1,
+                $this->graph_height - $this->graph_padding['bottom'] - $this->bar_height + 1,
+                $this->im_axis_bordercolor);
+    }
+
+    // draw scale steps
+    $max_value = $this->__GetMaxGraphValue();
+    if (($max_value % 10) > 0) {
+      $max_value = $max_value + (10 - ($max_value % 10));
+    }
+    $this->axis_max = $max_value;
+    $y = 0;
+    $style = array($this->im_axis_bordercolor, $this->im_graph_bgcolor);
+    imagesetstyle($this->im, $style);
+    while ($y <= $max_value) {
+      if ($max_value == 0) { $max_value=1; } // corrected by Marcelo Trenkenchu
+      $offset = floor($this->graph_height - $this->graph_padding['bottom'] -
+                ($y * $this->graph_areaheight / $max_value));
+      imageline($this->im,
+                $this->graph_padding['left'],
+                $offset,
+                $this->graph_padding['left'] + $this->bar_height - 1,
+                $offset - $this->bar_height + 1,
+                $this->im_axis_bordercolor);
+      $this->__DrawText($y,
+                        $this->graph_padding['left'],
+                        $offset,
+                        $this->im_axis_bordercolor,
+                        1,
+                        2,
+                        1);
+      // gridline
+      if ($y > 0) {
+        imageline($this->im,
+                  $this->graph_padding['left'] + $this->bar_height,
+                  $offset - $this->bar_height + 1,
+                  $this->graph_width - $this->graph_padding['right'] + $this->bar_height - 1,
+                  $offset - $this->bar_height + 1,
+                  IMG_COLOR_STYLED);
+      }
+      $y += $this->axis_step;
+    }
+
+    imageline($this->im,
+              $this->graph_width - $this->graph_padding['right'] + $this->bar_height - 1,
+              $this->graph_padding['top'] - $this->bar_height + 1,
+              $this->graph_width - $this->graph_padding['right'] + $this->bar_height - 1,
+              $this->graph_height - $this->graph_padding['bottom'] - $this->bar_height,
+              IMG_COLOR_STYLED);
+  }
+
+  /**
+   * GraphMaker::__DrawText()
+   * Draws text on image with color, size and alignment options
+   **/
+  function __DrawText($text, $x, $y, $color, $size = 1, $align = 0, $valign = 0) {
+    /*
+     * Align: 0=left | 1=center | 2=right
+     */
+    if ($align == 1) $x -= floor(strlen($text) * imagefontwidth($size) / 2);
+    elseif ($align == 2) $x -= (strlen($text) * imagefontwidth($size));
+    if ($valign == 1) $y -= floor(imagefontheight($size) / 2);
+    elseif ($valign == 2) $y -= imagefontheight($size);
+    imagestring($this->im,
+                $size,
+                $x,
+                $y,
+                $text,
+                $color);
+  }
+
+  /**
+   * GraphMaker::__GetMaxGraphValue()
+   * Returns max bar value
+   **/
+  function __GetMaxGraphValue() {
+    $max_value = 0;
+    foreach ($this->bar_data as $name => $value) {
+      if ($value > $max_value) $max_value = $value;
+    }
+    return $max_value;
+  }
+
+  /**
+   * GraphMaker::__DrawBarText()
+   * Determines top and left to draw text to a choosen bar
+   **/
+  function __DrawBarText($bar, $text) {
+    $this->__DrawText($text,
+                      $this->graph_padding['left'] + (($this->bar_width + ($this->bar_padding * 2)) * ($bar - 0.5)),
+                      $this->graph_height - $this->graph_padding['bottom'] + 1,
+                      $this->axis_bordercolor,
+                      1,
+                      1);
+  }
+
+  /**
+   * GraphMaker::__DrawBar()
+   * Draws a choosen bar with it's value
+   **/
+  function __DrawBar($bar, $value) {
+    $x = $this->graph_padding['left'] +
+         (($this->bar_width + ($this->bar_padding * 2)) * ($bar - 1)) +
+         $this->bar_padding;
+    if ($this->axis_max == 0) { $this->axis_max = 1; } // corrected by Marcelo Trenkenchu
+    $y = $value * $this->graph_areaheight / $this->axis_max;
+    $this->____DrawBar($x,
+                       $this->graph_height - $this->graph_padding['bottom'] - $y,
+                       $x + $this->bar_width,
+                       $this->graph_height - $this->graph_padding['bottom']);
+  }
+
+  /**
+   * GraphMaker::____DrawBar()
+   * Draws the actual rectangles that form a bar
+   **/
+  function ____DrawBar($x1, $y1, $x2, $y2) {
+    $this->__AllocateColor("im_bar_bordercolor",
+                           $this->bar_bordercolor,
+                           $this->graph_transparencylevel);
+    $this->__AllocateColor("im_bar_bgcolor",
+                           $this->bar_bgcolor,
+                           $this->graph_transparencylevel);
+    $this->__DrawPolygon($x1,                         $y1,
+                         $x2,                         $y1,
+                         $x2,                         $y2,
+                         $x1,                         $y2,
+                         $this->im_bar_bgcolor,       true);
+    $this->__DrawPolygon($x1,                         $y1,
+                         $x2,                         $y1,
+                         $x2,                         $y2,
+                         $x1,                         $y2,
+                         $this->im_bar_bordercolor);
+    $this->__DrawPolygon($x1,                         $y1,
+                         $x2,                         $y1,
+                         $x2 + $this->bar_height - 1, $y1 - $this->bar_height + 1,
+                         $x1 + $this->bar_height - 1, $y1 - $this->bar_height + 1,
+                         $this->im_bar_bgcolor,       true);
+    $this->__DrawPolygon($x1,                         $y1,
+                         $x2,                         $y1,
+                         $x2 + $this->bar_height - 1, $y1 - $this->bar_height + 1,
+                         $x1 + $this->bar_height - 1, $y1 - $this->bar_height + 1,
+                         $this->im_bar_bordercolor);
+    $this->__DrawPolygon($x2,                         $y2,
+                         $x2,                         $y1,
+                         $x2 + $this->bar_height - 1, $y1 - $this->bar_height + 1,
+                         $x2 + $this->bar_height - 1, $y2 - $this->bar_height + 1,
+                         $this->im_bar_bgcolor,       true);
+    $this->__DrawPolygon($x2,                         $y2,
+                         $x2,                         $y1,
+                         $x2 + $this->bar_height - 1, $y1 - $this->bar_height + 1,
+                         $x2 + $this->bar_height - 1, $y2 - $this->bar_height + 1,
+                         $this->im_bar_bordercolor);
+  }
+
+  /**
+   * GraphMaker::__DrawPolygon()
+   * Draws a (filled) (ir)regular polygon
+   **/
+  function __DrawPolygon($x1, $y1, $x2, $y2, $x3, $y3, $x4, $y4, $color, $filled = false) {
+    if ($filled) {
+      imagefilledpolygon($this->im, array($x1, $y1, $x2, $y2, $x3, $y3, $x4, $y4), 4, $color);
+    } else {
+      imagepolygon($this->im, array($x1, $y1, $x2, $y2, $x3, $y3, $x4, $y4), 4, $color);
+    }
+  }
+
+  /**
+   * GraphMaker::__LoadGraphDefinitions()
+   * Loads definitions to a graph from text lines (normaly
+   * they come from a file). This function is called by
+   * GraphMaker::LoadGraph()
+   **/
+  function __LoadGraphDefinitions($text) {
+    $text = preg_split("/\r?\n/", $text);
+    $data = array();
+    $section = '';
+    for ($i = 0; $i < count($text); $i++) {
+      if (preg_match("/^\s*#/", $text[$i])) {
+        //ignore.. it's just a comment
+      } elseif (preg_match("/^\s*\}\s*/", $text[$i])) {
+        $section = '';
+      } elseif (preg_match("/^\s*(\w+)\s*\{\s*$/", $text[$i], $r)) {
+        $section = $r[1];
+      } else {
+        $p = strpos($text[$i], "=");
+        if ($p !== false) {
+          $data[$section][trim(substr($text[$i], 0, $p))] = trim(substr($text[$i], $p + 1));
+        }
+      }
+    }
+    if (is_array($data['graph'])) {
+      $this->__LoadGraphValues($data['graph']);
+    }
+    if (is_array($data['bar'])) {
+      $this->__LoadBarValues($data['bar']);
+    }
+    if (is_array($data['axis'])) {
+      $this->__LoadAxisValues($data['axis']);
+    }
+    if (is_array($data['data'])) {
+      $this->bar_data = $data['data'];
+    }
+  }
+
+  /**
+   * GraphMaker::__LoadGraphValues()
+   * Loads definitions to main graph settings
+   **/
+  function __LoadGraphValues($data) {
+    foreach ($data as $name => $value) {
+      $name = strtolower($name);
+      switch ($name) {
+        case 'background-color':
+          $this->__SetColorToValue("graph_bgcolor", $value);
+          break;
+        case 'border-color':
+          $this->__SetColorToValue("graph_bordercolor", $value);
+          break;
+        case 'title-color':
+          $this->__SetColorToValue("graph_titlecolor", $value);
+          break;
+        case 'background-transparent':
+          $this->graph_bgtransparent = ($value == 1 || $value == 'yes' ? 1 : 0);
+          break;
+        case 'transparency':
+          $this->SetGraphTransparency(str_replace('%', '', $value));
+          break;
+        case 'title':
+          $this->graph_title = $value;
+          break;
+        case 'border-width':
+          $this->graph_borderwidth = (int) $value;
+          break;
+        case 'area-height':
+          $this->graph_areaheight = (int) $value;
+          break;
+        default:
+          if (substr($name, 0, 8) == 'padding-' && strlen($name) > 8) {
+            $this->graph_padding[substr($name, 8)] = $value;
+          }
+      }
+    }
+  }
+
+  /**
+   * GraphMaker::__LoadBarValues()
+   * Loads definitions to bar settings
+   **/
+  function __LoadBarValues($data) {
+    foreach ($data as $name => $value) {
+      $name = strtolower($name);
+      switch ($name) {
+        case 'background-color':
+          $this->__SetColorToValue("bar_bgcolor", $value);
+          break;
+        case 'border-color':
+          $this->__SetColorToValue("bar_bordercolor", $value);
+          break;
+        case 'padding':
+          $this->bar_padding = $value;
+          break;
+        case 'width':
+          $this->bar_width = (int) $value;
+          break;
+        case 'height':
+          $this->bar_height = (int) $value;
+          break;
+      }
+    }
+  }
+
+  /**
+   * GraphMaker::__LoadAxisValues()
+   * Loads definitions to axis settings
+   **/
+  function __LoadAxisValues($data) {
+    foreach ($data as $name => $value) {
+      switch (strtolower($name)) {
+        case 'step':
+          $this->SetAxisStep($value);
+          break;
+        case 'background-color':
+          $this->__SetColorToValue("axis_bgcolor", $value);
+          break;
+        case 'border-color':
+          $this->__SetColorToValue("axis_bordercolor", $value);
+      }
+    }
+  }
+
+  /**
+   * GraphMaker::__SetColorToValue()
+   * Sets a color (rgb or in html format) to a variable
+   **/
+  function __SetColorToValue($varname, $color) {
+    if ($color[0] == "#") { // if it's hex (html format), change to rgb array
+      if (strlen($color) == 4) {
+        // if only 3 hex values (I assume it's a shade of grey: #ddd)
+        $color .= substr($color, -3);
+      }
+      $color = array(hexdec($color[1].$color[2]),
+                     hexdec($color[3].$color[4]),
+                     hexdec($color[5].$color[6]));
+    }
+    $this->$varname = $color;
+  }
+
+  function __AllocateColor($varname, $color, $alpha) {
+    $this->$varname = imagecolorallocatealpha($this->im,
+                                              $color[0],
+                                              $color[1],
+                                              $color[2],
+                                              $alpha);
+  }
+}
+
+// Graph Generator for PHP
+// Originally located at http://szewo.com/php/graph, but link was broken, so this file was retrieved from:
+// http://web.archive.org/web/20030130065944/szewo.com/php/graph/graph.class.php3.txt
+// License unknown, however sources on the web have shown this to be either GPL or public domain.
+
+// At this point this class has been very nearly rewritten for Enano.
+
+class GraphMaker_compat {
+  var $_values;
+  var $_ShowLabels;
+  var $_ShowCounts;
+  var $_ShowCountsMode;
+
+  var $_BarWidth;
+  var $_GraphWidth;
+  var $_BarImg;
+  var $_BarBorderWidth;
+  var $_BarBorderColor;
+  var $_BarBackgroundColor;
+  var $_RowSortMode;
+  var $_TDClassHead;
+  var $_TDClassLabel;
+  var $_TDClassCount;
+  var $_GraphTitle;
+
+  function __construct() {
+    $this->_values = array();
+    $this->_ShowLabels = true;
+    $this->_BarWidth = 32;
+    $this->_GraphWidth = 360;
+    $this->_BarImg = scriptPath . "/images/graphbit.png";
+    $this->_BarBorderWidth = 0;
+    $this->_BarBorderColor = "red";
+    $this->_ShowCountsMode = 2;
+    $this->_RowSortMode = 1;
+    $this->_TDClassHead = "graph-title";
+    $this->_TDClassLabel = "graph-label";
+    $this->_TDClassCount = "graph-count";
+    $this->_GraphTitle="Graph title";
+    $this->_BarBackgroundColor = "#456798";
+  }
+
+  function GraphMaker_compat() {
+    $this->__construct();
+  }
+
+  function SetBarBorderWidth($width) {
+    $this->_BarBorderWidth = $width;
+  }
+  function SetBorderColor($color) {
+    $this->_BarBorderColor = $color;
+  }
+  
+  function SetBarBackgroundColor($color)
+  {
+    $this->_BarBackgroundColor = $color;
+  }
+
+//  mode = 1 labels asc, 2 label desc
+  function SetSortMode($mode) {
+    switch ($mode) {
+      case 1:
+        asort($this->_values);
+        break;
+      case 2:
+        arsort($this->_values);
+        break;
+      default:
+        break;
+      }
+
+  }
+
+  function AddValue($labelName, $theValue) {
+    array_push($this->_values, array("label" => $labelName, "value" => $theValue));
+  }
+
+  function SetBarData($data)
+  {
+      foreach ( $data as $name => $value )
+      {
+          $this->AddValue($name, $value);
+      }
+  }
+  function DrawGraph()
+  {
+      $this->BarGraphVert();
+  }
+  function SetBarWidth($width)
+  {
+    $this->_BarWidth = $width;
+  }
+  function SetBarImg($img)
+  {
+    $this->_BarImg = $img;
+  }
+  function SetShowLabels($lables)
+  {
+    $this->_ShowLabels = $labels;
+  }
+  function SetGraphWidth($width)
+  {
+    $this->_GraphWidth = $width;
+  }
+  function SetGraphTitle($title)
+  {
+    $this->_GraphTitle = $title;
+  }
+  //mode = percentage or counts
+  function SetShowCountsMode($mode)
+  {
+    $this->_ShowCountsMode = $mode;
+  }
+  //mode = none(0) label(1) or count(2)
+  function SetRowSortMode($sortmode)
+  {
+    $this->_RowSortMode = $sortmode;
+  }
+
+  function SetTDClassHead($class)
+  {
+    $this->_TDClassHead = $class;
+  }
+  function SetTDClassLabel($class)
+  {
+    $this->_TDClassLabel = $class;
+  }
+  function SetTDClassCount($class)
+  {
+    $this->_TDClassCount = $class;
+  }
+  function GetMaxVal()
+  {
+    $maxval = 0;
+    foreach ( $this->_values as $value )
+    {
+      if ( $maxval < $value["value"] )
+      {
+        $maxval = $value["value"];
+      }
+    }
+    return $maxval;
+  }
+  function BarGraphVert()
+  {
+    $maxval = $this->GetMaxVal();
+    foreach($this->_values as $value)
+    {
+      $sumval += $value["value"];
+    }
+    
+    $this->SetSortMode($this->_RowSortMode);
+    
+    echo "\n<!-- ----------------------------------------- -->\n<div class=\"tblholder\" style=\"width: 100%; clip: rect(0px,auto,auto,0px); overflow: auto;\">\n<table border=\"0\" cellspacing=\"1\" cellpadding=\"4\">\n  ";
+    
+    if ( strlen($this->_GraphTitle) > 0 )
+    {
+      echo "<tr>\n    <th colspan=\"".count($this->_values)."\" class=\"".$this->_TDClassHead."\">".$this->_GraphTitle."</th>\n  </tr>\n  ";
+    }
+    
+    echo "<tr>\n  ";
+    $css_class = 'row1';
+    
+    foreach($this->_values as $value)
+    {
+      $css_class = ( $css_class == 'row1' ) ? 'row3' : 'row1';
+      echo "  <td valign=\"bottom\" align=\"center\" class=\"$css_class\">\n      ";
+      $width = $this->_BarWidth;
+      $height = ceil( $value["value"] * $this->_GraphWidth / $maxval );
+
+      echo "<div style=\"width: {$width}px; height: {$height}px; background-color: {$this->_BarBackgroundColor}; border: ".$this->_BarBorderWidth."px solid ".$this->_BarBorderColor."\">\n      ";
+      echo "</div>\n    ";
+      
+      // echo "<img src=\"".$this->_BarImg."\" height=\"$width\" width=\"$height\" ";
+      // echo "  style=\"border: ".$this->_BarBorderWidth."px solid ".$this->_BarBorderColor."\"";
+      // echo ">";
+
+      echo "</td>\n  ";
+    }
+    echo "</tr>\n  ";
+    if ( $this->_ShowCountsMode > 0 )
+    {
+      $css_class = 'row1';
+      echo "<tr>\n  ";
+      foreach($this->_values as $value)
+      {
+        $css_class = ( $css_class == 'row1' ) ? 'row3' : 'row1';
+        switch ($this->_ShowCountsMode)
+        {
+          case 1:
+            $count = round ( 100 * $value["value"] / $sumval ) . "%";
+            break;
+          case 2:
+            $count = $value["value"];
+            break;
+          default:
+            break;
+        }
+        echo "  <td align=\"center\" class=\"$css_class ".$this->_TDClassCount."\">$count</td>\n  ";
+      }
+      echo "</tr>\n";
+    }
+
+    if ($this->_ShowLabels)
+    {
+      $css_class = 'row1';
+      echo "  <tr>\n  ";
+      foreach($this->_values as $value)
+      {
+        $css_class = ( $css_class == 'row1' ) ? 'row3' : 'row1';
+        echo "  <td align=\"center\" class=\"$css_class ".$this->_TDClassLabel."\"";
+        echo ">".$value["label"]."</td>\n  ";
+      }
+      echo "</tr>\n";
+    }
+
+    echo "</table>";
+  }
+
+  function BarGraphHoriz()
+  {
+    $maxval = $this->GetMaxVal();
+    
+    foreach($this->_values as $value)
+    {
+      $sumval += $value["value"];
+    }
+    
+    $this->SetSortMode($this->_RowSortMode);
+    
+    echo "<table border=\"0\">";
+    
+    if ( strlen($this->_GraphTitle) > 0 )
+    {
+      echo "<tr><td ";
+      if ( $this->_ShowCountsMode > 0 )
+      {
+        echo " colspan=\"2\"";
+      }
+      echo " class=\"".$this->_TDClassHead."\">".$this->_GraphTitle."</td></tr>";
+    }
+    foreach($this->_values as $value)
+    {
+      if ($this->_ShowLabels)
+      {
+        echo "<tr>";
+        echo "<td class=\"".$this->_TDClassLabel."\"";
+        if ( $this->_ShowCountsMode > 0 )
+        {
+          echo " colspan=\"2\"";
+        }
+        echo ">".$value["label"]."</td></tr>";
+      }
+      echo "<tr>";
+      if ( $this->_ShowCountsMode > 0 )
+      {
+        switch ($this->_ShowCountsMode)
+        {
+          case 1:
+            $count = round(100 * $value["value"] / $sumval )."%";
+            break;
+          case 2:
+            $count = $value["value"];
+            break;  /* Exit the switch and the while. */
+          default:
+            break;
+        }
+        echo "<td class=\"".$this->_TDClassCount."\">$count</TD>";
+      }
+      echo "<td>";
+      $height = $this->_BarWidth;
+      $width = ceil( $value["value"] * $this->_GraphWidth / $maxval );
+      echo "<div style=\"width: {$width}px; height: {$height}px; background-color: #456798; border: ".$this->_BarBorderWidth."px solid ".$this->_BarBorderColor."\">\n      ";
+      echo "</div>\n    ";
+      //echo "<img SRC=\"".$this->_BarImg."\" height=$height width=$width ";
+      //echo "  style=\"border: ".$this->_BarBorderWidth."px solid ".$this->_BarBorderColor."\"";
+      //echo ">";
+      echo "</td></tr>";
+    }
+    echo "</table>";
+  }
+  /**
+   * Dummy functions for compatibility with the GD version of the class
+   */
+  
+  function SetGraphPadding($a, $b, $c, $d)
+  {
+    return true;
+  }
+  function SetBarPadding($a)
+  {
+    return true;
+  }
+  function SetAxisStep($a)
+  {
+    return true;
+  }
+  function SetGraphBackgroundTransparent($r, $g, $b, $a)
+  {
+    return true;
+  }
+  function SetGraphTransparency($a)
+  {
+    return true;
+  }
+  function SetGraphAreaHeight($a)
+  {
+    return true;
+  }
+}
+
+
--- a/htdocs/24hours.php	Sat Oct 25 20:24:31 2008 +0000
+++ b/htdocs/24hours.php	Sat Nov 15 15:28:32 2008 -0500
@@ -4,9 +4,9 @@
 require('../graphs.php');
 require('../timezone.php');
 
-$first_channel = array_keys($stats_data['messages']);
-$first_channel = $first_channel[0];
-$channel = ( isset($_REQUEST['channel']) && isset($stats_data['messages'][$_REQUEST['channel']]) ) ? $_REQUEST['channel'] : $first_channel;
+$channel_list = stats_channel_list();
+$first_channel = $channel_list[0];
+$channel = ( isset($_REQUEST['channel']) && in_array($_REQUEST['channel'], $channel_list) ) ? $_REQUEST['channel'] : $first_channel;
 
 // generate the data
 // we're doing this by absolute hours, not by strictly "24 hours ago", e.g. on-the-hour stats
--- a/htdocs/datafile.php	Sat Oct 25 20:24:31 2008 +0000
+++ b/htdocs/datafile.php	Sat Nov 15 15:28:32 2008 -0500
@@ -1,6 +1,16 @@
 <?php
 header('Content-type: application/force-download');
-header('Content-disposition: attachment; filename=stats-data.php');
+header('Content-disposition: attachment; filename=stats-data.csv');
+
+require('../stats-fe.php');
+
+echo "channel,nick,timestamp\n";
 
-echo file_get_contents('../stats-data.php');
+$q = eb_mysql_query('SELECT channel, nick, time FROM stats_messages ORDER BY message_id ASC;');
 
+while ( $row = mysql_fetch_assoc($q) )
+{
+  echo "{$row['channel']},{$row['nick']},{$row['time']}\n";
+}
+
+mysql_free_result($q);
--- a/htdocs/index.php	Sat Oct 25 20:24:31 2008 +0000
+++ b/htdocs/index.php	Sat Nov 15 15:28:32 2008 -0500
@@ -1,11 +1,10 @@
 <?php
 require('../stats-fe.php');
 require('../timezone.php');
-require('../config.php');
 
-$channels = array_keys($stats_data['messages']);
-$first_channel = $channels[0];
-$channel = ( isset($_REQUEST['channel']) && isset($stats_data['messages'][$_REQUEST['channel']]) ) ? $_REQUEST['channel'] : $first_channel;
+$channel_list = stats_channel_list();
+$first_channel = $channel_list[0];
+$channel = ( isset($_REQUEST['channel']) && in_array($_REQUEST['channel'], $channel_list) ) ? $_REQUEST['channel'] : $first_channel;
 ?>
 
 <html>
@@ -26,7 +25,7 @@
         <?php
         $tz_display = str_replace('_', ' ', str_replace('/', ': ', $tz));
         echo 'Time zone: ' . $tz_display . ' [<a href="changetz.php">change</a>]<br />';
-        echo '<small>The time now is ' . date('H:i:s') . '.<br />Statistics last written to disk at ' . date('H:i:s', stats_last_updated()) . '.</small>';
+        echo '<small>The time now is ' . date('H:i:s') . '.<br />Statistics now updated constantly (see <a href="news.php">news</a>)</small>';
         ?>
       </p>
       <p>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/htdocs/news.php	Sat Nov 15 15:28:32 2008 -0500
@@ -0,0 +1,24 @@
+<?php
+require('../config.php');
+
+?><html>
+  <head>
+    <title><?php echo $nick; ?> - updates</title>
+    <style type="text/css">
+    p.code {
+      font-family: monospace;
+      margin-left: 1.5em;
+    }
+    </style>
+  </head>
+  <body>
+    <h1>Updates and changes</h1>
+    <p>I've been updating <?php echo $nick; ?> recently with some really cool enhancements to the back-end. This is more technical stuff
+       so you might want to read on only if you're a geek.</p>
+    <p><?php echo $nick; ?> only stores info with MySQL now. All the stats go into a MySQL table that had an initial size of over 360,000
+       records after the import of the existing flat file database. This means that while querying can be slower (things aren't split up
+       like they were with the flatfile DB) it's a more portable programming technique and it means it can be easily expanded in the
+       future to include more data. The table's indexed so it should be decently fast.</p>
+    <p>In addition, smarter functionality is being included, plus a few bugs here and there have been fixed.</p>
+  </body>
+</head>
--- a/htdocs/privacy.php	Sat Oct 25 20:24:31 2008 +0000
+++ b/htdocs/privacy.php	Sat Nov 15 15:28:32 2008 -0500
@@ -13,20 +13,24 @@
   </head>
   <body>
     <h1>Privacy information</h1>
-    <p><?php echo $nick; ?> is designed to collect IRC statistics. It does this by recording raw data and then letting the frontend (index.php and a
-       few backend functions in stats-fe.php) look at the data and draw graphs and measurements based on it.</p>
+    <p><?php echo $nick; ?> is designed to collect IRC statistics. It does this by recording raw data and then letting the frontend (index.php and the
+       backend access abstraction in stats-fe.php) look at the data and draw graphs and measurements based on it.</p>
     <p>The only information <?php echo $nick; ?> collects is</p>
     <ul>
       <li>The time of each message</li>
       <li>The nick that posted that message</li>
-      <li>Whether that nick has certain flags, like operator/voice</li>
     </ul>
+    <p>In addition, <?php echo $nick; ?> knows whether users currently have permissions such as operator and voice, but this information isn't logged (it's used to determine who can do what). This means that the web interface never knows for sure who is in the channel.</p>
     <p><?php echo $nick; ?> also gives you the ability to disable recording statistics about you. To clear all your past statistics, type in any channel:</p>
     <p class="code">!deluser</p>
-    <p>You can also prevent yourself from being logged in the future with:</p>
+    <p>(Moderators can also type:</p>
+    <p class="code">!deluser | SomeNick</p>
+    <p>to remove statistics for a flooder or spammer)</p>
+    <p>You can prevent yourself from being logged in the future with:</p>
     <p class="code">/msg <?php echo $nick; ?> anonymize</p>
+    <p>You'll be asked if you want to anonymize your past statistics as well.</p>
     <p>Remove yourself from the anonymization list with:</p>
     <p class="code">/msg <?php echo $nick; ?> denonymize</p>
-    <p>Want to know more about the numbers <?php echo $nick; ?> collects? <a href="datafile.php">Download <?php echo $nick; ?>'s data file yourself</a> (<a href="json.php">in JSON format</a>).</p>
+    <p>Want to know more about the numbers <?php echo $nick; ?> collects? <a href="datafile.php">Download a dump of <?php echo $nick; ?>'s database yourself</a>.</p>
   </body>
 </head>
--- a/libirc.php	Sat Oct 25 20:24:31 2008 +0000
+++ b/libirc.php	Sat Nov 15 15:28:32 2008 -0500
@@ -249,6 +249,7 @@
   private function handle_privmsg($message)
   {
     $message = self::parse_message($message);
+    $message['message'] = preg_replace('/^(.+?):/', '', $message['message']);
     $ph = $this->privmsg_handler;
     if ( @function_exists($ph) )
       return @call_user_func($ph, $message);
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libprogress.php	Sat Nov 15 15:28:32 2008 -0500
@@ -0,0 +1,311 @@
+<?php
+
+/**
+ * Class for drawing progress bars in a vt100 console.
+ * @author Dan Fuhry
+ * @license Public domain
+ */
+
+class ProgressBar
+{
+  /**
+   * Shell escape character.
+   * @const string
+   */
+  
+  const SHELL_ESCAPE = "\x1B";
+  
+  /**
+   * Carriage return (0x0D)
+   * @const string
+   */
+  
+  const CARRIAGE_RETURN = "\r";
+  
+  /**
+   * Colors of the foreground, background, foreground text, and background text, respectively
+   * @var int
+   * @var int
+   * @var int
+   * @var int
+   */
+  
+  private $color_bar, $color_empty, $color_text, $color_emptytext;
+  
+  /**
+   * Text to the left of the bar.
+   * @var string
+   */
+  
+  private $bar_left;
+  
+  /**
+   * Text to the right of the bar.
+   * @var string
+   */
+  
+  private $bar_right;
+  
+  /**
+   * Text in the middle of the bar.
+   * @var string
+   */
+  
+  private $bar_text;
+  
+  /**
+   * The current location of the bar in %.
+   * @var int
+   */
+  
+  private $bar_pos = 0;
+  
+  /**
+   * Position where the text should start.
+   * @var int
+   */
+  
+  private $text_pos = 0;
+  
+  /**
+   * Width of the current terminal.
+   * @var int
+   */
+  
+  private $term_width = 80;
+  
+  /**
+   * Width of the actual bar.
+   * @var int
+   */
+  
+  private $bar_width = 0;
+  
+  /**
+   * State of the bar's color. Used to avoid echoing tons of color codes.
+   * @var int
+   */
+  
+  private $color_state = 0;
+  
+  /**
+   * Color state constants
+   * @const int
+   * @const int
+   * @const int
+   * @const int
+   * @const int
+   */
+  
+  const COLOR_STATE_RESET = 0;
+  const COLOR_STATE_FULL_HIDE = 1;
+  const COLOR_STATE_FULL_SHOW = 2;
+  const COLOR_STATE_EMPTY_HIDE = 3;
+  const COLOR_STATE_EMPTY_SHOW = 4;
+  
+  /**
+   * Constructor. All parameters are optional. Color choices are defined in color_to_code.
+   * @param string $bar_left
+   * @param string $bar_right
+   * @param string $bar_text
+   * @param string $color_bar
+   * @param string $color_empty
+   * @param string $color_text
+   * @param string $color_emptytext
+   */
+  
+  public function __construct($bar_left = '[', $bar_right = ']', $bar_text = '', $color_bar = 'red', $color_empty = 'black', $color_text = 'white', $color_emptytext = 'cyan')
+  {
+    $this->bar_left = $bar_left;
+    $this->bar_right = $bar_right;
+    $this->color_bar = $this->color_to_code($color_bar);
+    $this->color_empty = $this->color_to_code($color_empty);
+    $this->color_text = $this->color_to_code($color_text);
+    $this->color_emptytext = $this->color_to_code($color_emptytext);
+    
+    if ( isset($_SERVER['COLUMNS']) )
+    {
+      $this->term_width = intval($_SERVER['COLUMNS']);
+    }
+    $this->bar_width = $this->term_width - strlen($this->bar_left) - strlen($this->bar_right);
+    
+    $this->update_text_quiet($bar_text);
+  }
+  
+  /**
+   * Updates the text on the progress bar and recalculates the position without redrawing.
+   * @param string Text in the bar. If omitted, blanked.
+   */
+  
+  public function update_text_quiet($bar_text = '')
+  {
+    $this->bar_text = strval($bar_text);
+    
+    if ( !empty($this->bar_text) )
+    {
+      $this->text_pos = round(( $this->bar_width / 2 ) - ( strlen($this->bar_text) / 2 ));
+    }
+  }
+  
+  /**
+   * Updates the text on the progress bar, recalculates the position, and redraws.
+   * @param string Text in the bar. If omitted, blanked.
+   */
+  
+  function update_text($bar_text = '')
+  {
+    $this->update_text_quiet($bar_text);
+    $this->set($this->bar_pos);
+  }
+  
+  /**
+   * Starts output of the bar.
+   */
+  
+  function start()
+  {
+    echo self::CARRIAGE_RETURN;
+    echo $this->bar_left;
+  }
+  
+  /**
+   * Closes the bar.
+   */
+  
+  function end()
+  {
+    $this->set($this->bar_pos, $this->bar_width);
+    echo "\n";
+  }
+  
+  /**
+   * Sets the position of the bar.
+   * @param int Position in %. If a second parameter is set, this is treated as a numerator with the second parameter being the denominator and that is used to calculate position.
+   * @param int Optional. Total number of units to allow fraction usage instead of percentage.
+   */
+  
+  function set($pos, $max = 100)
+  {
+    // if our pos is higher than 100%, reduce it
+    if ( $pos > $max )
+      $pos = $max;
+    
+    // arithmetic one-liner
+    // this is where we should stop showing the "full" color and instead use "empty"
+    $bar_pos = round($this->bar_width * ( $pos / $max ));
+    $this->bar_pos = 100 * ( $pos / $max );
+    
+    // reset the cursor
+    echo self::CARRIAGE_RETURN . $this->bar_left;
+    
+    // print everything out
+    for ( $i = 0; $i < $this->bar_width; $i++ )
+    {
+      $char = ' ';
+      $hide = true;
+      if ( !empty($this->bar_text) )
+      {
+        // we have some text to display in the middle; see where we are.
+        $show_text = ( $i >= $this->text_pos && $i < ( $this->text_pos + strlen($this->bar_text) ) );
+        if ( $show_text )
+        {
+          $char = substr($this->bar_text, $i - $this->text_pos, 1);
+          if ( strlen($char) < 1 )
+            $char = ' ';
+          else
+            $hide = false;
+        }
+      }
+      // determine color
+      if ( $i > $bar_pos )
+      {
+        $hide ? $this->set_color_empty_hide() : $this->set_color_empty_show();
+      }
+      else
+      {
+        $hide ? $this->set_color_full_hide() : $this->set_color_full_show();
+      }
+      echo $char;
+    }
+    $this->set_color_reset();
+    echo $this->bar_right;
+  }
+  
+  #
+  # PRIVATE METHODS
+  #
+  
+  function set_color_full_hide()
+  {
+    if ( $this->color_state == self::COLOR_STATE_FULL_HIDE )
+      return;
+    $this->color_state = self::COLOR_STATE_FULL_HIDE;
+    
+    $fgcolor = 30 + $this->color_bar;
+    $bgcolor = $fgcolor + 10;
+    echo self::SHELL_ESCAPE . "[0;{$fgcolor};{$bgcolor};8m";
+  }
+  
+  function set_color_full_show()
+  {
+    if ( $this->color_state == self::COLOR_STATE_FULL_SHOW )
+      return;
+    $this->color_state = self::COLOR_STATE_FULL_SHOW;
+    
+    $fgcolor = 30 + $this->color_text;
+    $bgcolor = 40 + $this->color_bar;
+    echo self::SHELL_ESCAPE . "[0;1;{$fgcolor};{$bgcolor}m";
+  }
+  
+  function set_color_empty_hide()
+  {
+    if ( $this->color_state == self::COLOR_STATE_EMPTY_HIDE )
+      return;
+    $this->color_state = self::COLOR_STATE_EMPTY_HIDE;
+    
+    $fgcolor = 30 + $this->color_empty;
+    $bgcolor = $fgcolor + 10;
+    echo self::SHELL_ESCAPE . "[0;{$fgcolor};{$bgcolor};8m";
+  }
+  
+  function set_color_empty_show()
+  {
+    if ( $this->color_state == self::COLOR_STATE_EMPTY_SHOW )
+      return;
+    $this->color_state = self::COLOR_STATE_EMPTY_SHOW;
+    
+    $fgcolor = 30 + $this->color_emptytext;
+    $bgcolor = 40 + $this->color_empty;
+    echo self::SHELL_ESCAPE . "[0;1;{$fgcolor};{$bgcolor}m";
+  }
+  
+  function set_color_reset()
+  {
+    if ( $this->color_state == self::COLOR_STATE_RESET )
+      return;
+    $this->color_state = self::COLOR_STATE_RESET;
+    
+    echo self::SHELL_ESCAPE . "[0m";
+  }
+  
+  /**
+   * Converts a color name to an ASCII color code. Valid color names are black, red, green, yellow, blue, magenta, cyan, and white.
+   * @param string Color name
+   * @return int
+   */
+  
+  private function color_to_code($color)
+  {
+    static $colors = array(
+      'black' => 0,
+      'red' => 1,
+      'green' => 2,
+      'yellow' => 3,
+      'blue' => 4,
+      'magenta' => 5,
+      'cyan' => 6,
+      'white' => 7
+    );
+    return ( isset($colors[$color]) ) ? $colors[$color] : $colors['white'];
+  }
+}
--- a/modules/doctor.php	Sat Oct 25 20:24:31 2008 +0000
+++ b/modules/doctor.php	Sat Nov 15 15:28:32 2008 -0500
@@ -1,4 +1,6 @@
 <?php
+$doctor = array();
+
 require('eliza.php');
 
 eb_hook('event_channel_msg', 'doctor_listen($chan, $message);');
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/modules/iplogger.php	Sat Nov 15 15:28:32 2008 -0500
@@ -0,0 +1,107 @@
+<?php
+
+eb_hook('event_raw_message', 'iplogger_log_join($message);');
+
+function iplogger_log_join($message)
+{
+  if ( $message['action'] == 'JOIN' )
+  {
+    $nick = mysql_real_escape_string($message['nick']);
+    $basenick = basenick($nick);
+    $host = mysql_real_escape_string($message['host']);
+    $ip = ( $_ = @resolve_ip($message['host'], $message['user']) ) ? mysql_real_escape_string($_) : '0.0.0.0';
+    $channel = mysql_real_escape_string($message['message']);
+    $time = time();
+    $query = "DELETE FROM ip_log WHERE
+                nick = '$nick' AND
+                basenick = '$basenick' AND
+                ip = '$ip' AND
+                hostname = '$host' AND
+                channel = '$channel';";
+    eb_mysql_query($query);
+    
+    $query = "INSERT INTO ip_log ( nick, basenick, ip, hostname, channel, time ) VALUES(
+                '$nick',
+                '$basenick',
+                '$ip',
+                '$host',
+                '$channel',
+                $time
+              );";
+    eb_mysql_query($query);
+  }
+  else if ( $message['ACTION'] == 'NICK' )
+  {
+    // this is for cross-referencing purposes.
+    $nick = mysql_real_escape_string($message['message']);
+    $basenick = basenick($message['nick']);
+    $host = mysql_real_escape_string($message['host']);
+    $ip = ( $_ = @resolve_ip($message['host'], $message['user']) ) ? mysql_real_escape_string($_) : '0.0.0.0';
+    $channel = '__nickchange';
+    $time = time();
+    $query = "DELETE FROM ip_log WHERE
+                nick = '$nick' AND
+                basenick = '$basenick' AND
+                ip = '$ip' AND
+                hostname = '$host' AND
+                channel = '$channel';";
+    eb_mysql_query($query);
+    
+    $query = "INSERT INTO ip_log ( nick, basenick, ip, hostname, channel, time ) VALUES(
+                '$nick',
+                '$basenick',
+                '$ip',
+                '$host',
+                '$channel',
+                $time
+              );";
+    eb_mysql_query($query);
+  }
+}
+
+/**
+ * Attempt to eliminate mini-statuses and such from nicknames.
+ * @example
+ <code>
+ $basenick = basenick('enanobot|debug');
+ // $basenick = 'enanobot'
+ </code>
+ * @param string Nickname
+ * @return string
+ */
+
+function basenick($nick)
+{
+  if ( preg_match('/^`/', $nick) )
+  {
+    $nick = substr($nick, 1);
+  }
+  return preg_replace('/(`|\|)(.+?)$/', '', $nick);
+}
+
+/**
+ * Resolve an IP address. First goes by checking if it's a mibbit or CGI-IRC IP/user, then performs lookups accordingly.
+ * @param string Hostname
+ * @param string Username
+ * @return string IP address
+ */
+
+function resolve_ip($host, $user)
+{
+  if ( $host == 'webchat.mibbit.com' )
+  {
+    return hex2ipv4($user);
+  }
+  return gethostbyname($host);
+}
+
+function hex2ipv4($ip)
+{
+  $ip = preg_replace('/^0x/', '', $ip);
+  $ip = str_split($ip, 2);
+  foreach ( $ip as &$byte )
+  {
+    $byte = hexdec($byte);
+  }
+  return implode('.', $ip);
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/modules/memberlist.php	Sat Nov 15 15:28:32 2008 -0500
@@ -0,0 +1,123 @@
+<?php
+
+// most of the code in here goes towards keeping track of the list of members currently in the various channels we're in.
+
+$eb_memberlist = array();
+$userflags = array(
+  'o' => '@',
+  'v' => '+'
+);
+
+eb_hook('event_self_join', 'mlist_init_channel($this);');
+eb_hook('event_raw_message', 'mlist_process_message($chan, $message);');
+eb_hook('snippet_dynamic', 'if ( $snippet === "memberlist" ) return mlist_list_members($chan, $message);');
+eb_hook('event_other', 'mlist_handle_other_event($message);');
+
+function mlist_init_channel(&$chan)
+{
+  global $eb_memberlist, $userflags;
+  
+  $channel_name = $chan->get_channel_name();
+  $eb_memberlist[$channel_name] = array();
+  $prefixes_regexp = '/^([' . preg_quote(implode('', $userflags)) . '])+/';
+  $prefixes_flipped = array_flip($userflags);
+  $prefixes_regexp_notlist = '/[^' . preg_quote(implode('', $prefixes_flipped)) . ']/';
+  
+  // read list of members from channel
+  @stream_set_timeout($chan->parent->sock, 3);
+  while ( $msg = $chan->parent->get() )
+  {
+    if ( $ml = strstr($msg, ' 353 ') )
+    {
+      $memberlist = trim(substr(strstr($ml, ':'), 1));
+      $eb_memberlist[$channel_name] = explode(' ', $memberlist);
+      $eb_memberlist[$channel_name] = array_flip($eb_memberlist[$channel_name]);
+      foreach ( $eb_memberlist[$channel_name] as $nick => $_ )
+      {
+        $eb_memberlist[$channel_name][$nick] = '';
+        while ( preg_match($prefixes_regexp, $nick) )
+        {
+          $prefix = substr($nick, 0, 1);
+          $add = preg_replace($prefixes_regexp_notlist, '', strval($eb_memberlist[$channel_name][$nick]));
+          unset($eb_memberlist[$channel_name][$nick]);
+          $nick = substr($nick, 1);
+          $eb_memberlist[$channel_name][$nick] = $prefixes_flipped[$prefix] . $add;
+        }
+      }
+      break;
+    }
+  }
+}
+
+function mlist_process_message(&$chan, $message)
+{
+  global $eb_memberlist;
+  $channel_name = $chan->get_channel_name();
+  if ( !isset($eb_memberlist[$channel_name]) )
+  {
+    return false;
+  }
+  
+  $ml =& $eb_memberlist[$channel_name];
+  
+  // we need to change statistics accordingly depending on the event
+  if ( $message['action'] == 'JOIN' )
+  {
+    // member joined - init their flags and up the member count by one
+    $ml[$message['nick']] = '';
+  }
+  else if ( $message['action'] == 'PART' )
+  {
+    // member left - clear flags and decrement the total member count
+    unset($ml[$message['nick']]);
+  }
+  else if ( $message['action'] == 'MODE' )
+  {
+    // update member list (not sure why this would be useful, but export it anyway - display scripts might find it useful)
+    list($mode, $target) = explode(' ', $message['message']);
+    $action = substr($mode, 0, 1);
+    
+    global $userflags;
+    $ml[$target] = str_replace(substr($mode, 1), '', $ml[$target]);
+    if ( $action == '+' )
+    {
+      $ml[$target] .= substr($mode, 1);
+    }
+  }
+}
+
+function mlist_list_members(&$chan, &$message)
+{
+  global $eb_memberlist;
+  $channel_name = $chan->get_channel_name();
+  if ( !isset($eb_memberlist[$channel_name]) )
+  {
+    return false;
+  }
+  
+  $ml =& $eb_memberlist[$channel_name];
+  
+  $mlt = implode("\n", str_split(str_replace("\n", ' ', print_r($ml, true)), 400));
+  $chan->parent->privmsg($message['nick'], "memberlist:\n" . $mlt);
+  
+  return true;
+}
+
+function mlist_handle_other_event(&$message)
+{
+  global $eb_memberlist;
+  
+  if ( $message['action'] == 'NICK' )
+  {
+    // we have a nick change; go through all channels and replace the old nick with the new
+    foreach ( $eb_memberlist as &$ml )
+    {
+      if ( isset($ml[$message['nick']]) )
+      {
+        $ml[$message['message']] = $ml[$message['nick']];
+        unset($ml[$message['nick']]);
+      }
+    }
+  }
+}
+
--- a/modules/stats.php	Sat Oct 25 20:24:31 2008 +0000
+++ b/modules/stats.php	Sat Nov 15 15:28:32 2008 -0500
@@ -1,272 +1,6 @@
 <?php
 
-// most of the code in here goes towards keeping track of the list of members currently in the various channels we're in.
-
-$stats_memberlist = array();
-$stats_prefixes = array(
-  'o' => '@',
-  'v' => '+'
-);
-$stats_data = array('anonymous' => array(), 'messages' => array());
-$stats_day = gmdate('Ymd');
-@include("./stats/stats-data-$stats_day.php");
-unset($stats_data['members']);
-$stats_data['members'] =& $stats_memberlist;
-
-eb_hook('event_self_join', 'stats_init_channel($this);');
-eb_hook('event_raw_message', 'stats_process_message($chan, $message);');
-eb_hook('snippet_dynamic', 'if ( $snippet === "memberlist" ) return stats_list_members($chan, $message); if ( $snippet === "deluser" ) return stats_del_user($chan, $message);');
-eb_hook('event_other', 'stats_handle_other_event($message);');
-eb_hook('event_privmsg', 'stats_handle_privmsg($message);');
-
-function stats_init_channel(&$chan)
-{
-  global $stats_memberlist, $stats_prefixes, $stats_data;
-  
-  $channel_name = $chan->get_channel_name();
-  $stats_memberlist[$channel_name] = array();
-  $prefixes_regexp = '/^([' . preg_quote(implode('', $stats_prefixes)) . '])+/';
-  $prefixes_flipped = array_flip($stats_prefixes);
-  $prefixes_regexp_notlist = '/[^' . preg_quote(implode('', $prefixes_flipped)) . ']/';
-  
-  if ( !isset($stats_data['messages'][$channel_name]) )
-  {
-    $stats_data['messages'][$channel_name] = array();
-  }
-  
-  // read list of members from channel
-  @stream_set_timeout($chan->parent->sock, 3);
-  while ( $msg = $chan->parent->get() )
-  {
-    if ( $ml = strstr($msg, ' 353 ') )
-    {
-      $memberlist = trim(substr(strstr($ml, ':'), 1));
-      $stats_memberlist[$channel_name] = explode(' ', $memberlist);
-      $stats_memberlist[$channel_name] = array_flip($stats_memberlist[$channel_name]);
-      foreach ( $stats_memberlist[$channel_name] as $nick => $_ )
-      {
-        $stats_memberlist[$channel_name][$nick] = '';
-        while ( preg_match($prefixes_regexp, $nick) )
-        {
-          $prefix = substr($nick, 0, 1);
-          $add = preg_replace($prefixes_regexp_notlist, '', strval($stats_memberlist[$channel_name][$nick]));
-          unset($stats_memberlist[$channel_name][$nick]);
-          $nick = substr($nick, 1);
-          $stats_memberlist[$channel_name][$nick] = $prefixes_flipped[$prefix] . $add;
-        }
-      }
-      break;
-    }
-  }
-}
-
-function stats_process_message(&$chan, $message)
-{
-  global $stats_memberlist, $stats_data;
-  $channel_name = $chan->get_channel_name();
-  if ( !isset($stats_memberlist[$channel_name]) )
-  {
-    return false;
-  }
-  
-  $ml =& $stats_memberlist[$channel_name];
-  
-  // we need to change statistics accordingly depending on the event
-  if ( $message['action'] == 'JOIN' )
-  {
-    // member joined - init their flags and up the member count by one
-    $ml[$message['nick']] = '';
-  }
-  else if ( $message['action'] == 'PART' )
-  {
-    // member left - clear flags and decrement the total member count
-    unset($ml[$message['nick']]);
-    $ml = array_values($ml);
-  }
-  else if ( $message['action'] == 'MODE' )
-  {
-    // update member list (not sure why this would be useful, but export it anyway - display scripts might find it useful)
-    list($mode, $target) = explode(' ', $message['message']);
-    $action = substr($mode, 0, 1);
-    
-    global $stats_prefixes;
-    $ml[$target] = str_replace(substr($mode, 1), '', $ml[$target]);
-    if ( $action == '+' )
-    {
-      $ml[$target] .= substr($mode, 1);
-    }
-  }
-  else if ( $message['action'] == 'PRIVMSG' )
-  {
-    // private message into $channel_name - mark the user active and log the message time
-    if ( isset($stats_data['anonymous'][$message['nick']]) )
-      $message['nick'] = 'Anonymous';
-    
-    $messages =& $stats_data['messages'][$channel_name];
-    
-    $messages[] = array(
-        'time' => time(),
-        'nick' => $message['nick']
-      );
-  }
-  
-  stats_cron();
-}
-
-function stats_list_members(&$chan, &$message)
-{
-  global $stats_memberlist;
-  $channel_name = $chan->get_channel_name();
-  if ( !isset($stats_memberlist[$channel_name]) )
-  {
-    return false;
-  }
-  
-  $ml =& $stats_memberlist[$channel_name];
-  
-  $chan->parent->privmsg($message['nick'], "memberlist:\n" . str_replace("\n", ' ', print_r($ml, true)));
-  
-  return true;
-}
+require('statsincludes/stats_core.php');
+require('statsincludes/stats_logger.php');
+require('statsincludes/stats_frontend.php');
 
-function stats_del_user(&$chan, &$message)
-{
-  global $stats_memberlist, $privileged_list, $irc, $stats_data;
-  
-  // remove a user from the DB
-  $targetuser = trim(substr(strstr($message['message'], '|'), 1));
-  if ( empty($targetuser) )
-    $targetuser = $message['nick'];
-  
-  if ( $targetuser != $message['nick'] && !in_array($message['nick'], $privileged_list) )
-  {
-    $irc->privmsg($message['nick'], "Sorry, you need to be a moderator to delete statistics for users other than yourself.");
-    return true;
-  }
-  
-  // we should be good - delete the user
-  foreach ( $stats_data['messages'] as $channel => &$messages )
-  {
-    foreach ( $messages as $i => &$currentmessage )
-    {
-      if ( $currentmessage['nick'] == $targetuser )
-      {
-        unset($messages[$i]);
-      }
-    }
-    $messages = array_values($messages);
-  }
-  unset($users, $currentmessage, $messages);
-  
-  global $nick;
-  $greeting = ( $targetuser == $message['nick'] ) ? "All of your statistics data" : "All of {$targetuser}'s statistic data";
-  $irc->privmsg($message['nick'], "$greeting has been removed from the database for all channels. The changes will show up in the next commit to disk, which is usually no more than once every two minutes.");
-  $irc->privmsg($message['nick'], "Want your stats to be anonymized in the future? Type /msg $nick anonymize to make me keep all your stats anonymous in the future. This only applies to your current nick though - for example if you change your nick to \"{$message['nick']}|sleep\" or similar your information will not be anonymous.");
-  $irc->privmsg($message['nick'], "You can't clear your logs if you're anonymous. Type /msg $nick denonymize to remove yourself from the anonymization list. Anonymized logs can't be converted back to their original nicks.");
-  
-  return true;
-}
-
-function stats_handle_privmsg(&$message)
-{
-  global $irc, $stats_data, $nick;
-  static $poll_list = array();
-  
-  $message['message'] = strtolower($message['message']);
-  
-  if ( trim($message['message']) === 'anonymize' )
-  {
-    $stats_data['anonymous'][$message['nick']] = true;
-    $poll_list[$message['nick']] = true;
-    $irc->privmsg($message['nick'], "Anonymization complete. Any further statistics recorded about you will be anonymous.");
-    $irc->privmsg($message['nick'], "Do you want to also anonymize any past statistics about you? (type \"yes\" or \"no\")");
-  }
-  else if ( trim($message['message']) === 'denonymize' )
-  {
-    $stats_data['anonymous'][$message['nick']] = false;
-    unset($stats_data['anonymous'][$message['nick']]);
-    $irc->privmsg($message['nick'], "Denonymization complete. Any further statistics recorded about you will bear your nick. Remember that you can always change this with /msg $nick anonymize.");
-  }
-  else if ( trim($message['message']) === 'yes' && isset($poll_list[$message['nick']]) )
-  {
-    // anonymize logs for this user
-    // we should be good - delete the user
-    $targetuser = $message['nick'];
-    
-    foreach ( $stats_data['messages'] as $channel => &$messages )
-    {
-      foreach ( $messages as $i => &$currentmessage )
-      {
-        if ( $currentmessage['nick'] == $targetuser )
-        {
-          $currentmessage['nick'] = 'Anonymous';
-        }
-      }
-      $messages = array_values($messages);
-    }
-    unset($users, $currentmessage, $messages);
-    $irc->privmsg($message['nick'], "Anonymization complete. All past statistics on your nick are now anonymous.");
-    
-    unset($poll_list[$message['nick']]);
-  }
-  stats_cron();
-}
-
-function stats_handle_other_event(&$message)
-{
-  global $stats_memberlist;
-  
-  if ( $message['action'] == 'NICK' )
-  {
-    // we have a nick change; go through all channels and replace the old nick with the new
-    foreach ( $stats_memberlist as &$ml )
-    {
-      if ( isset($ml[$message['nick']]) )
-      {
-        $ml[$message['message']] = $ml[$message['nick']];
-        unset($ml[$message['nick']]);
-      }
-    }
-  }
-  stats_cron();
-}
-
-function stats_cron()
-{
-  static $commit_time = 0;
-  $now = time();
-  // commit to disk every 1 minute
-  if ( $commit_time + 60 < $now )
-  {
-    $commit_time = $now;
-    stats_commit();
-  }
-}
-
-function stats_commit()
-{
-  global $stats_data, $stats_day;
-  
-  ob_start();
-  var_export($stats_data);
-  $stats_data_exported = ob_get_contents();
-  ob_end_clean();
-  
-  $fp = @fopen("./stats/stats-data-$stats_day.php", 'w');
-  if ( !$fp )
-    return false;
-  fwrite($fp, "<?php\n\$stats_data = $stats_data_exported;\n");
-  fclose($fp);
-  
-  if ( $stats_day != gmdate('Ymd') )
-  {
-    // it's a new day! flush all our logs
-    foreach ( $stats_data['messages'] as &$data )
-    {
-      $data = array();
-    }
-  }
-  
-  $stats_day = gmdate('Ymd');
-}
-
--- a/stats-fe.php	Sat Oct 25 20:24:31 2008 +0000
+++ b/stats-fe.php	Sat Nov 15 15:28:32 2008 -0500
@@ -7,12 +7,25 @@
  * @author Dan Fuhry <dan@enanocms.org>
  */
 
-$stats_merged_data = array('counts' => array(), 'messages' => array());
-$stats_data =& $stats_merged_data;
-
 define('ENANOBOT_ROOT', dirname(__FILE__));
 define('NOW', time());
 
+require(ENANOBOT_ROOT . '/config.php');
+require(ENANOBOT_ROOT . '/hooks.php');
+require(ENANOBOT_ROOT . '/database.php');
+
+mysql_reconnect();
+
+/**
+ * Gets ths list of channels.
+ * @return array
+ */
+
+function stats_channel_list()
+{
+  return $GLOBALS['channels'];
+}
+
 /**
  * Gets the number of messages posted in IRC in the last X minutes.
  * @param string Channel
@@ -23,26 +36,32 @@
 
 function stats_message_count($channel, $mins = 10, $base = NOW)
 {
-  global $stats_merged_data;
-  
+  $channel = db_escape($channel);
   $time_min = $base - ( $mins * 60 );
-  $time_max = $base;
-  
-  if ( !isset($stats_merged_data['messages'][$channel]) )
+  $time_max =& $base;
+  if ( $q = eb_mysql_query("SELECT message_count FROM stats_count_cache WHERE time_min = $time_min AND time_max = $time_max AND channel = '$channel';") )
   {
-    return 0;
+    if ( mysql_num_rows($q) > 0 )
+    {
+      $row = mysql_fetch_assoc($q);
+      mysql_free_result($q);
+      return intval($row['message_count']);
+    }
+    mysql_free_result($q);
   }
-  
-  $count = 0;
-  foreach ( $stats_merged_data['messages'][$channel] as $message )
+  if ( $q = eb_mysql_query("SELECT COUNT(message_id) FROM stats_messages WHERE channel = '$channel' AND time >= $time_min AND time <= $time_max;") )
   {
-    if ( $message['time'] >= $time_min && $message['time'] <= $time_max )
+    $row = mysql_fetch_row($q);
+    $count = $row[0];
+    mysql_free_result($q);
+    // avoid caching future queries
+    if ( $base <= NOW )
     {
-      $count++;
+      eb_mysql_query("INSERT INTO stats_count_cache(channel, time_min, time_max, message_count) VALUES('$channel', $time_min, $time_max, $count);");
     }
+    return $count;
   }
-  
-  return $count;
+  return false;
 }
 
 /**
@@ -55,50 +74,34 @@
 
 function stats_activity_percent($channel, $mins = 10, $base = NOW)
 {
-  global $stats_merged_data;
-  if ( !($total = stats_message_count($channel, $mins, $base)) )
-  {
-    return array();
-  }
-  $results = array();
-  $usercounts = array();
+  $channel = db_escape($channel);
   $time_min = $base - ( $mins * 60 );
-  $time_max = $base;
-  foreach ( $stats_merged_data['messages'][$channel] as $message )
+  $time_max =& $base;
+  
+  if ( $q = eb_mysql_query("SELECT nick FROM stats_messages WHERE channel = '$channel' AND time >= $time_min AND time <= $time_max;") )
   {
-    if ( $message['time'] >= $time_min && $message['time'] <= $time_max )
+    $userdata = array();
+    while ( $row = @mysql_fetch_assoc($q) )
     {
-      if ( !isset($usercounts[$message['nick']]) )
-        $usercounts[$message['nick']] = 0;
-      $usercounts[$message['nick']]++;
+      $total++;
+      if ( isset($userdata[ $row['nick'] ]) )
+      {
+        $userdata[ $row['nick'] ]++;
+      }
+      else
+      {
+        $userdata[ $row['nick'] ] = 1;
+      }
     }
-  }
-  foreach ( $usercounts as $nick => $count )
-  {
-    $results[$nick] = $count / $total;
+    foreach ( $userdata as &$val )
+    {
+      $val = $val / $total;
+    }
+    mysql_free_result($q);
+    arsort($userdata);
+    return $userdata;
   }
-  arsort($results);
-  return $results;
-}
-
-/**
- * Loads X days of statistics, minimum.
- * @param int Days to load, default is 1
- */
- 
-function load_stats_data($days = 1)
-{
-  $days++;
-  for ( $i = 0; $i < $days; $i++ )
-  {
-    $day = NOW - ( $i * 86400 );
-    $day = gmdate('Ymd', $day);
-    if ( file_exists(ENANOBOT_ROOT . "/stats/stats-data-$day.php") )
-    {
-      require(ENANOBOT_ROOT . "/stats/stats-data-$day.php");
-      stats_merge($stats_data);
-    }
-  }
+  return false;
 }
 
 /**
@@ -108,59 +111,8 @@
 
 function stats_last_updated()
 {
-  $day = gmdate('Ymd');
-  $file = ENANOBOT_ROOT . "/stats/stats-data-$day.php";
-  return ( file_exists($file) ) ? filemtime($file) : 0;
+  // :-D
+  return NOW;
 }
 
-/**
- * Merges a newly loaded stats array with the current cache in RAM.
- * @param array Data to merge
- * @access private
- */
 
-function stats_merge($data)
-{
-  global $stats_merged_data;
-  if ( isset($data['counts']) )
-  {
-    foreach ( $data['counts'] as $channel => $chaninfo )
-    {
-      if ( isset($stats_merged_data['counts'][$channel]) )
-      {
-        foreach ( $stats_merged_data['counts'][$channel] as $key => &$value )
-        {
-          if ( is_int($value) )
-          {
-            $value = max($value, $chaninfo[$key]);
-          }
-          else if ( is_array($value) )
-          {
-            $value = array_merge($value, $chaninfo[$key]);
-          }
-        }
-      }
-      else
-      {
-        $stats_merged_data['counts'][$channel] = $chaninfo;
-      }
-    }
-  }
-  foreach ( $data['messages'] as $channel => $chandata )
-  {
-    if ( isset($stats_merged_data['messages'][$channel]) )
-    {
-      foreach ( $chandata as $message )
-      {
-        $stats_merged_data['messages'][$channel][] = $message;
-      }
-    }
-    else
-    {
-      $stats_merged_data['messages'][$channel] = $chandata;
-    }
-  }
-}
-
-load_stats_data();
-
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/stats/import-stats.php	Sat Nov 15 15:28:32 2008 -0500
@@ -0,0 +1,53 @@
+<?php
+
+require('../config.php');
+require('../database.php');
+require('../hooks.php');
+require('../libprogress.php');
+require('../statsincludes/stats_core.php');
+mysql_reconnect();
+
+// PASS 1 - build file list
+$filelist = array();
+if ( $dr = @opendir('.') )
+{
+  while ( $dh = @readdir($dr) )
+  {
+    if ( preg_match('/^stats-data-[0-9]{8}\.php$/', $dh) )
+    {
+      $filelist[] = $dh;
+    }
+  }
+  closedir($dr);
+}
+
+asort($filelist);
+
+// PASS 2 - import
+$pbar = new ProgressBar('Importing: [', ']', '');
+$pbar->start();
+$pbar->set(0);
+$i = 0;
+foreach ( $filelist as $dh )
+{
+  $pbar->update_text_quiet($dh);
+  $pbar->set(++$i, count($filelist));
+  require($dh);
+  foreach ( $stats_data['messages'] as $channel => &$data )
+  {
+    foreach ( $data as &$message )
+    {
+      stats_log_message($channel, $message['nick'], $message['time']);
+    }
+  }
+  if ( isset($stats_data['anonymous']) )
+  {
+    foreach ( $stats_data['anonymous'] as $user => $_ )
+    {
+      stats_anonymize_user_now($user);
+    }
+  }
+}
+
+$pbar->end();
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/statsincludes/stats_core.php	Sat Nov 15 15:28:32 2008 -0500
@@ -0,0 +1,85 @@
+<?php
+
+$stats_anonymize_list = array();
+
+eb_hook('startup_early', 'stats_core_cache_anons();');
+
+function stats_core_cache_anons()
+{
+  global $stats_anonymize_list;
+  if ( $q = eb_mysql_query('SELECT nick FROM stats_anon;') )
+  {
+    while ( $row = mysql_fetch_assoc($q) )
+    {
+      $stats_anonymize_list[] = $row['nick'];
+    }
+  }
+}
+
+function stats_log_message($channel, $nick, $timestamp)
+{
+  // anonymize message?
+  global $stats_anonymize_list;
+  if ( in_array($nick, $stats_anonymize_list) )
+  {
+    $nick = 'Anonymous';
+  }
+  
+  $channel = db_escape($channel);
+  $nick = db_escape($nick);
+  $sql = 'INSERT INTO stats_messages(channel, nick, time) ' . "VALUES('$channel', '$nick', " . intval($timestamp) . ");";
+  eb_mysql_query($sql);
+}
+
+function stats_anonymize_user_now($nick)
+{
+  global $stats_anonymize_list;
+  // anonymize list is cached in RAM
+  if ( in_array($nick, $stats_anonymize_list) )
+  {
+    return false;
+  }
+  
+  $stats_anonymize_list[] = $nick;
+  
+  $nick = db_escape($nick);
+  eb_mysql_query("INSERT INTO stats_anon(nick) VALUES('$nick');");
+  
+  return true;
+}
+
+function stats_anonymize_user_past($nick)
+{
+  global $stats_anonymize_list;
+  if ( !in_array($nick, $stats_anonymize_list) )
+  {
+    return false;
+  }
+  
+  $nick = db_escape($nick);
+  eb_mysql_query("UPDATE stats_messages SET nick = 'Anonymous' WHERE nick = '$nick';");
+  return true;
+}
+
+function stats_denonymize_user($nick)
+{
+  global $stats_anonymize_list;
+  if ( !in_array($nick, $stats_anonymize_list) )
+  {
+    return false;
+  }
+  
+  $nick = db_escape($nick);
+  eb_mysql_query("DELETE FROM stats_anon WHERE nick = '$nick';");
+  
+  unset($stats_anonymize_list[ array_search($nick, $stats_anonymize_list) ]);
+  return true;
+}
+
+function stats_del_user($chan, $nick)
+{
+  $chan = db_escape($chan);
+  $nick = db_escape($nick);
+  eb_mysql_query("DELETE FROM stats_messages WHERE channel = '$chan' AND nick = '$nick';");
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/statsincludes/stats_frontend.php	Sat Nov 15 15:28:32 2008 -0500
@@ -0,0 +1,86 @@
+<?php
+
+##
+## Deletion requests
+##
+
+eb_hook('snippet_dynamic', 'if ( $snippet === "deluser" ) return stats_handle_delete_request($chan, $message);');
+
+function stats_handle_delete_request($chan, $message)
+{
+  global $privileged_list, $irc, $stats_data;
+  
+  // remove a user from the DB
+  $targetuser = trim(substr(strstr($message['message'], '|'), 1));
+  if ( empty($targetuser) )
+    $targetuser = $message['nick'];
+  
+  if ( $targetuser != $message['nick'] && !in_array($message['nick'], $privileged_list) )
+  {
+    $irc->privmsg($message['nick'], "Sorry, you need to be a moderator to delete statistics for users other than yourself.");
+    return true;
+  }
+  
+  // we should be good - delete the user
+  stats_del_user($chan->get_channel_name(), $targetuser);
+  
+  global $nick;
+  $greeting = ( $targetuser == $message['nick'] ) ? "All of your statistics data" : "All of {$targetuser}'s statistic data";
+  $irc->privmsg($message['nick'], "$greeting has been removed from the database for all channels. The changes will show up in the next commit to disk, which is usually no more than once every two minutes.");
+  $irc->privmsg($message['nick'], "Want your stats to be anonymized in the future? Type /msg $nick anonymize to make me keep all your stats anonymous in the future. This only applies to your current nick though - for example if you change your nick to \"{$message['nick']}|sleep\" or similar your information will not be anonymous.");
+  $irc->privmsg($message['nick'], "You can't clear your logs if you're anonymous. Type /msg $nick denonymize to remove yourself from the anonymization list. Anonymized logs can't be converted back to their original nicks.");
+  
+  return true;
+}
+
+##
+## Anonymization
+##
+
+eb_hook('event_privmsg', 'stats_handle_privmsg($message);');
+
+function stats_handle_privmsg($message)
+{
+  global $irc, $stats_data, $nick;
+  static $poll_list = array();
+  
+  $message['message'] = strtolower($message['message']);
+  
+  if ( trim($message['message']) === 'anonymize' )
+  {
+    if ( stats_anonymize_user_now($message['nick']) )
+    {
+      $irc->privmsg($message['nick'], "Anonymization complete. Any further statistics recorded about you will be anonymous.");
+      $irc->privmsg($message['nick'], "Do you want to also anonymize any past statistics about you? (type \"yes\" or \"no\")");
+      $poll_list[$message['nick']] = true;
+    }
+    else
+    {
+      $irc->privmsg($message['nick'], "You're already marked as anonymous.");
+    }
+  }
+  else if ( trim($message['message']) === 'denonymize' )
+  {
+    if ( stats_denonymize_user($message['nick']) )
+    {
+      $irc->privmsg($message['nick'], "Denonymization complete. Any further statistics recorded about you will bear your nick. Remember that you can always change this with /msg $nick anonymize.");
+    }
+    else
+    {
+      $irc->privmsg($message['nick'], "You're not marked as anonymous.");
+    }
+  }
+  else if ( trim($message['message']) === 'yes' && isset($poll_list[$message['nick']]) )
+  {
+    // anonymize logs for this user
+    stats_anonymize_user_past($message['nick']);
+    $irc->privmsg($message['nick'], "Anonymization complete. All past statistics on your nick are now anonymous.");
+    
+    unset($poll_list[$message['nick']]);
+  }
+  else if ( isset($poll_list[$message['nick']]) )
+  {
+    unset($poll_list[$message['nick']]);
+  }
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/statsincludes/stats_logger.php	Sat Nov 15 15:28:32 2008 -0500
@@ -0,0 +1,10 @@
+<?php
+
+eb_hook('event_channel_msg', 'stats_event_privmsg($chan, $message);');
+
+function stats_event_privmsg($chan, $message)
+{
+  $channel = $chan->get_channel_name();
+  stats_log_message($channel, $message['nick'], time());
+}
+