Stats now uses MySQL backend
authorDan
Sat, 15 Nov 2008 15:00:52 -0500
changeset 20 e2f6e3af9959
parent 19 eb92dc5d9fb4
child 21 d86ea89358ec
Stats now uses MySQL backend
database.sql
htdocs/24hours.php
htdocs/datafile.php
htdocs/index.php
htdocs/privacy.php
modules/memberlist.php
modules/stats.php
stats-fe.php
statsincludes/stats_core.php
statsincludes/stats_frontend.php
statsincludes/stats_logger.php
--- a/database.sql	Sat Nov 15 14:59:51 2008 -0500
+++ b/database.sql	Sat Nov 15 15:00:52 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/htdocs/24hours.php	Sat Nov 15 14:59:51 2008 -0500
+++ b/htdocs/24hours.php	Sat Nov 15 15:00:52 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 Nov 15 14:59:51 2008 -0500
+++ b/htdocs/datafile.php	Sat Nov 15 15:00:52 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 Nov 15 14:59:51 2008 -0500
+++ b/htdocs/index.php	Sat Nov 15 15:00:52 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>
--- a/htdocs/privacy.php	Sat Nov 15 14:59:51 2008 -0500
+++ b/htdocs/privacy.php	Sat Nov 15 15:00:52 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>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/modules/memberlist.php	Sat Nov 15 15:00:52 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 Nov 15 14:59:51 2008 -0500
+++ b/modules/stats.php	Sat Nov 15 15:00:52 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 Nov 15 14:59:51 2008 -0500
+++ b/stats-fe.php	Sat Nov 15 15:00:52 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/statsincludes/stats_core.php	Sat Nov 15 15:00:52 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:00:52 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:00:52 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());
+}
+