Welcome, modularization and stats.
authorDan
Mon, 25 Aug 2008 12:34:26 -0400
changeset 8 0acb8d9a3194
parent 7 1d6e762433fe
child 9 4192954c29d1
Welcome, modularization and stats.
config-sample.php
eliza.php
enanobot
enanobot.php
hooks.php
htdocs/24hours.php
htdocs/changetz.php
htdocs/datafile.php
htdocs/index.php
htdocs/json.php
htdocs/privacy.php
htdocs/snippets.php
libirc.php
libjson.php
modules/autoop.php
modules/doctor.php
modules/echo.php
modules/greeting.php
modules/log.php
modules/snippets.php
modules/stats.php
snippets.php
stats-fe.php
timezone.php
--- a/config-sample.php	Fri May 09 22:37:57 2008 -0400
+++ b/config-sample.php	Mon Aug 25 12:34:26 2008 -0400
@@ -2,6 +2,7 @@
 
 // Rename this to config.php and run php ./enanobot.php to start.
 
+$server = 'irc.freenode.net';
 $nick = 'EnanoBot';
 $pass = '';
 $name = 'Enano CMS logging/message bot';
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/eliza.php	Mon Aug 25 12:34:26 2008 -0400
@@ -0,0 +1,626 @@
+<?php
+
+/**
+ * Implementation of ELIZA in PHP. Ported from Javascript by Dan Fuhry
+ * Chat Bot by George Dunlop, www.peccavi.com
+ * May be used/modified if credit line is retained
+ * @author George Dunlop <http://www.peccavi.com/>
+ * @author Dan Fuhry <dan@enanocms.org>
+ * @copyright (c) 1997-2008 George Dunlop. All rights reserved, portions copyright (C) 2008 Dan Fuhry.
+ */
+
+class Psychotherapist
+{
+  private $maxKey = 36;
+  private $keyNotFound = 0;
+  private $keyword = array();
+  private $maxresponses = 116;
+  private $response = array();
+  
+  private $maxConj = 19;
+  private $max2ndConj = 7;
+  
+  private $conj1 = Array();
+  private $conj2 = Array();
+  private $conj3 = Array();
+  private $conj4 = Array();
+  
+  private $punct = Array(".", ",", "!", "?", ":", ";", "&", '"', "@", "#", "(", ")" );
+  
+  /**
+   * Constructor.
+   */
+  
+  public function __construct()
+  {
+    $this->keyNotFound = $this->maxKey - 1;
+    $this->keyword = $this->create_array($this->maxKey);
+    $this->response = $this->create_array($this->maxresponses);
+    $this->conj1 = $this->create_array($this->maxConj);
+    $this->conj2 = $this->create_array($this->maxConj);
+    $this->conj3 = $this->create_array($this->max2ndConj);
+    $this->conj4 = $this->create_array($this->max2ndConj);
+    
+    $this->table_setup();
+  }
+  
+  /**
+   * Replacement for str_replace that provides more options.
+   * if type == 0 straight string replacement
+   * if type == 1 assumes padded strings and replaces whole words only
+   * if type == 2 non case sensitive assumes padded strings to compare whole word only
+   * if type == 3 non case sensitive straight string replacement
+   * @param string Haystack
+   * @param string Needle
+   * @param string Replacement
+   * @param int Mode - defaults to 0
+   */
+
+  private function replaceStr($strng, $substr1, $substr2, $type = 0)
+  {
+    if ( $type == 0 )
+    {  
+      return str_replace($substr1, $substr2, $strng);
+    }
+    else if ( $type == 1 )
+    {
+      return str_replace(" $substr1 ", " $substr2 ", $strng);
+    }
+    else if ( $type == 2 || $type == 3 )
+    {
+      if ( $type == 2 )
+      {
+        $substr1 = " $substr1 ";
+        $substr2 = " $substr2 ";
+      }
+      return preg_replace('/' . preg_quote($substr1) . '/i', $substr2, $strng);
+    }
+    else
+    {
+      throw new Exception("Invalid parameter");
+    }
+  }
+  
+  /**
+   * Function to pad a string. head, tail & punctuation
+   * @param string
+   * @return string
+   */
+  
+  private function padString($strng)
+  {
+    $punct =& $this->punct;
+    
+    $aString = " " . $strng . " ";
+    for ( $i = 0; $i < count($punct); $i++ )
+    {
+      $aString = $this->replaceStr( $aString, $punct[$i], " " . $punct[$i] . " ", 0 );
+    }
+    return $aString;
+  }
+  
+  /**
+   * Function to strip padding
+   */
+  
+  private function unpadString($strng)
+  {
+    $punct =& $this->punct;
+    
+    $aString = $strng;
+    $aString = $this->replaceStr( $aString, "  ", " ", 0 );         // compress spaces
+    
+    $aString = trim($aString, ' ');
+    
+    for ( $i = 0; $i < count($punct); $i++ )
+    {
+      $aString = $this->replaceStr( $aString, " " . $punct[$i], $punct[$i], 0 );
+    }
+    return $aString;
+  }
+  
+  /**
+   * Dress Input formatting i.e leading & trailing spaces and tail punctuation
+   * @param string
+   * @return string
+   */
+
+  function strTrim($strng)
+  {
+    static $ht = 0;
+    
+    if ( $ht == 0 )
+    {
+      $loc = 0;
+    }                                    // head clip
+    else
+    {
+      $loc = strlen($strng) - 1;
+    }                        // tail clip  ht = 1 
+    if ( substr($strng, $loc, 1) == " " )
+    {
+      $aString = substr($strng, - ( $ht - 1 ), strlen($strng) - $ht);
+      $aString = $this->strTrim($aString);
+    }
+    else
+    {
+      $flg = false;
+      for ( $i = 0; $i <= 5; $i++ )
+      {
+        $flg = $flg || ( substr($strng, $loc, 1) == $this->punct[$i]);
+      }
+      if ( $flg )
+      {    
+        $aString = substr($strng, - ( $ht - 1 ), strlen($strng) - $ht);
+      }
+      else
+      {
+        $aString = $strng;
+      }
+      if ( $aString != $strng )
+      {
+        $aString = $this->strTrim($aString);
+      }
+    }
+    if ( $ht == 0 )
+    {
+      $ht = 1;
+      $aString = $this->strTrim($aString);
+    } 
+    else
+    {
+      $ht = 0;
+    }        
+    return $aString;
+  }
+  
+  /**
+   * adjust pronouns and verbs & such
+   * @param string
+   * @return string
+   */
+  
+  private function conjugate($sStrg)
+  {
+    $sString = $sStrg;
+    for ( $i = 0; $i < $this->maxConj; $i++ )
+    {            // decompose
+      $sString = $this->replaceStr( $sString, $this->conj1[$i], "#@&" . $i, 2 );
+    }
+    for( $i = 0; $i < $this->maxConj; $i++ )
+    {            // recompose
+      $sString = $this->replaceStr( $sString, "#@&" . $i, $this->conj2[$i], 2 );
+    }
+    // post process the resulting string
+    for( $i = 0; $i < $this->max2ndConj; $i++ )
+    {            // decompose
+      $sString = $this->replaceStr( $sString, $this->conj3[$i], "#@&" . $i, 2 );
+    }
+    for( $i = 0; $i < $this->max2ndConj; $i++ )
+    {            // recompose
+      $sString = $this->replaceStr( $sString, "#@&" . $i, $this->conj4[$i], 2 );
+    }
+    return $sString;
+  }
+  
+  /**
+   * Build our response string
+   * get a random choice of response based on the key
+   * Then structure the response
+   * @param string
+   * @param int Key index
+   * @return string
+   */
+  
+  function phrase( $sString, $keyidx )
+  {
+    $idxmin  = $this->keyword[$keyidx]->idx;
+    $idrange = $this->keyword[$keyidx]->end - $idxmin + 1;
+    while ( $pass < 5 )
+    {
+      $choice = $this->keyword[$keyidx]->idx + mt_rand(0, $idrange);
+      if ( $choice == $this->keyword[$keyidx]->last )
+      { 
+        $pass++;
+        continue;
+      }
+      break;
+    }
+    $this->keyword[$keyidx]->last = $choice;
+    $rTemp = $this->response[$choice];
+    $tempt = substr($rTemp, strlen($rTemp) - 1, 1);
+    if ( ( $tempt == "*" ) || ( $tempt == "@" ) )
+    {
+      $sTemp = $this->padString($sString);
+      $wTemp = strtoupper($sTemp);
+      $strpstr = intval(strpos($wTemp, " {$this->keyword[$keyidx]->key} "));
+      
+      $strpstr += strlen($this->keyword[$keyidx]->key) + 1;
+      $thisstr = $this->conjugate( substr($sTemp, $strpstr, strlen($sTemp)) );
+      $thisstr = $this->strTrim( $this->unpadString($thisstr) );
+      if( $tempt == "*" )
+      {
+        $sTemp = $this->replaceStr( $rTemp, "<*", " " . $thisstr . "?", 0 );
+      }
+      else
+      {
+        $sTemp = $this->replaceStr( $rTemp, "<@", " " . $thisstr . ".", 0 );
+      }
+    }
+    else
+    {
+      $sTemp = $rTemp;
+    }
+    return $sTemp;
+  }
+  
+  /**
+   * returns array index of first key found
+   * @param string
+   */
+
+  private function testkey($wString)
+  {
+    for ( $keyid = 0; $keyid < count($this->keyword); $keyid++ )
+    {
+      if ( strpos($wString, " {$this->keyword[$keyid]->key} ") !== false )
+      { 
+        return $keyid;
+      }
+    }
+    return false;
+  }
+  
+  /**
+   * 
+   */
+  
+  private function findkey($wString)
+  { 
+    $keyid = $this->testkey($wString);
+    if( !$keyid )
+    {
+      $keyid = $this->keyNotFound;
+    }
+    return $keyid;
+  }
+  
+  /**
+   * Process a line from the user.
+   * @param string User input
+   * @return string AI output
+   */
+  
+  function listen($User)
+  {
+    static $wTopic = "";                                            // Last worthy responce
+    static $sTopic = "";                                            // Last worthy responce
+    static $greet = false;
+    static $wPrevious = "";                                    // so we can check for repeats
+    
+    $sInput = $User;
+    $sInput = $this->strTrim($sInput);                            // dress input formating
+    
+    if ( $sInput != "" )
+    { 
+      $wInput = $this->padString(strtoupper($sInput));    // Work copy
+      $foundkey = $this->maxKey;                          // assume it's a repeat input
+      if ( $wInput != $wPrevious )
+      {                       // check if user repeats himself
+        $foundkey = $this->findkey($wInput);               // look for a keyword.
+      }
+      if( $foundkey == $this->keyNotFound )
+      {
+        if( !$greet )
+        {
+          $greet = true;
+          return "Don't you ever say Hello?";
+        }
+        else
+        {
+          $wPrevious = $wInput;                      // save input to check repeats
+          if (( strlen($sInput) < 10 ) && ( $wTopic != "" ) && ( $wTopic != $wPrevious ))
+          {
+            $lTopic = $this->conjugate( $sTopic );
+            $sTopic = "";
+            $wTopic = "";
+            return 'OK... "' + $lTopic + '". Tell me more.';
+          }
+          else
+          {
+            if ( strlen($sInput) < 15 )
+            { 
+              return "Tell me more..."; 
+            }
+            else
+            {
+              return $this->phrase( $sInput, $foundkey );
+            }
+          }
+        }
+      }
+      else
+      { 
+        if ( strlen($sInput) > 12 )
+        {
+          $sTopic = $sInput;
+          $wTopic = $wInput;
+        }
+        $greet = true;
+        $wPrevious = $wInput;              // save input to check repeats
+        return $this->phrase( $sInput, $foundkey );            // Get our response
+      }
+    }
+    else
+    {
+      return "I can't help if you will not chat with me!";
+    }
+  }
+  
+  /**
+   * Creates an array of the specified length, and fills it with null values.
+   * @param int Array size
+   * @return array
+   */
+  
+  function create_array($len)
+  {
+    $ret = array();
+    for ( $i = 0; $i < $len; $i++ )
+    {
+      $ret[] = null;
+    }
+    return $ret;
+  }
+  
+  /**
+   * Sets up the tables of phrases, etc.
+   */
+  
+  private function table_setup()
+  {
+    // build our data base here
+							 
+    $this->conj1[0]  = "are";           $this->conj2[0]  = "am";
+    $this->conj1[1]  = "am";            $this->conj2[1]  = "are";
+    $this->conj1[2]  = "were";          $this->conj2[2]  = "was";
+    $this->conj1[3]  = "was";           $this->conj2[3]  = "were";
+    $this->conj1[4]  = "I";             $this->conj2[4]  = "you";    
+    $this->conj1[5]  = "me";            $this->conj2[5]  = "you";    
+    $this->conj1[6]  = "you";           $this->conj2[6]  = "me";
+    $this->conj1[7]  = "my";            $this->conj2[7]  = "your";    
+    $this->conj1[8]  = "your";          $this->conj2[8]  = "my";
+    $this->conj1[9]  = "mine";          $this->conj2[9]  = "your's";    
+    $this->conj1[10] = "your's";        $this->conj2[10] = "mine";    
+    $this->conj1[11] = "I'm";           $this->conj2[11] = "you're";
+    $this->conj1[12] = "you're";        $this->conj2[12] = "I'm";    
+    $this->conj1[13] = "I've";          $this->conj2[13] = "you've";
+    $this->conj1[14] = "you've";        $this->conj2[14] = "I've";
+    $this->conj1[15] = "I'll";          $this->conj2[15] = "you'll";
+    $this->conj1[16] = "you'll";        $this->conj2[16] = "I'll";
+    $this->conj1[17] = "myself";        $this->conj2[17] = "yourself";
+    $this->conj1[18] = "yourself";      $this->conj2[18] = "myself";
+    
+    // array to post process correct our tenses of pronouns such as "I/me"
+    
+    $this->conj3[0]  = "me am";         $this->conj4[0]  = "I am";
+    $this->conj3[1]  = "am me";         $this->conj4[1]  = "am I";
+    $this->conj3[2]  = "me can";        $this->conj4[2]  = "I can";
+    $this->conj3[3]  = "can me";        $this->conj4[3]  = "can I";
+    $this->conj3[4]  = "me have";       $this->conj4[4]  = "I have";
+    $this->conj3[5]  = "me will";       $this->conj4[5]  = "I will";
+    $this->conj3[6]  = "will me";       $this->conj4[6]  = "will I";
+    
+    
+    // Keywords
+    
+    $this->keyword[ 0]=new Psychotherapist_Key( "CAN YOU",          1,  3);
+    $this->keyword[ 1]=new Psychotherapist_Key( "CAN I",            4,  5);
+    $this->keyword[ 2]=new Psychotherapist_Key( "YOU ARE",          6,  9);
+    $this->keyword[ 3]=new Psychotherapist_Key( "YOU'RE",           6,  9);
+    $this->keyword[ 4]=new Psychotherapist_Key( "I DON'T",          10, 13);
+    $this->keyword[ 5]=new Psychotherapist_Key( "I FEEL",           14, 16);
+    $this->keyword[ 6]=new Psychotherapist_Key( "WHY DON'T YOU", 17, 19);
+    $this->keyword[ 7]=new Psychotherapist_Key( "WHY CAN'T I",     20, 21);
+    $this->keyword[ 8]=new Psychotherapist_Key( "ARE YOU",          22, 24);
+    $this->keyword[ 9]=new Psychotherapist_Key( "I CAN'T",          25, 27);
+    $this->keyword[10]=new Psychotherapist_Key( "I AM",             28, 31);
+    $this->keyword[11]=new Psychotherapist_Key( "I'M",              28, 31);
+    $this->keyword[12]=new Psychotherapist_Key( "YOU",              32, 34);
+    $this->keyword[13]=new Psychotherapist_Key( "I WANT",           35, 39);
+    $this->keyword[14]=new Psychotherapist_Key( "WHAT",             40, 48);
+    $this->keyword[15]=new Psychotherapist_Key( "HOW",              40, 48);
+    $this->keyword[16]=new Psychotherapist_Key( "WHO",              40, 48);
+    $this->keyword[17]=new Psychotherapist_Key( "WHERE",            40, 48);
+    $this->keyword[18]=new Psychotherapist_Key( "WHEN",             40, 48);
+    $this->keyword[19]=new Psychotherapist_Key( "WHY",              40, 48);
+    $this->keyword[20]=new Psychotherapist_Key( "NAME",             49, 50);
+    $this->keyword[21]=new Psychotherapist_Key( "CAUSE",            51, 54);
+    $this->keyword[22]=new Psychotherapist_Key( "SORRY",            55, 58);
+    $this->keyword[23]=new Psychotherapist_Key( "DREAM",            59, 62);
+    $this->keyword[24]=new Psychotherapist_Key( "HELLO",            63, 63);
+    $this->keyword[25]=new Psychotherapist_Key( "HI",               63, 63);
+    $this->keyword[26]=new Psychotherapist_Key( "MAYBE",            64, 68);
+    $this->keyword[27]=new Psychotherapist_Key( "NO",               69, 73);
+    $this->keyword[28]=new Psychotherapist_Key( "YOUR",             74, 75);
+    $this->keyword[29]=new Psychotherapist_Key( "ALWAYS",           76, 79);
+    $this->keyword[30]=new Psychotherapist_Key( "THINK",            80, 82);
+    $this->keyword[31]=new Psychotherapist_Key( "ALIKE",            83, 89);
+    $this->keyword[32]=new Psychotherapist_Key( "YES",              90, 92);
+    $this->keyword[33]=new Psychotherapist_Key( "FRIEND",           93, 98);
+    $this->keyword[34]=new Psychotherapist_Key( "COMPUTER",         99, 105);
+    $this->keyword[35]=new Psychotherapist_Key( "NO KEY FOUND",     106, 112);
+    $this->keyword[36]=new Psychotherapist_Key( "REPEAT INPUT",     113, 116);
+    
+    
+    $this->response[  0]="ELIZA - PHP version ported from Javascript (George Dunlop) code by Dan Fuhry";
+    $this->response[  1]="Don't you believe that I can<*";
+    $this->response[  2]="Perhaps you would like to be able to<*";
+    $this->response[  3]="You want me to be able to<*";
+    $this->response[  4]="Perhaps you don't want to<*";
+    $this->response[  5]="Do you want to be able to<*";
+    $this->response[  6]="What makes you think I am<*";
+    $this->response[  7]="Does it please you to believe I am<*";
+    $this->response[  8]="Perhaps you would like to be<*";
+    $this->response[  9]="Do you sometimes wish you were<*";
+    $this->response[ 10]="Don't you really<*";
+    $this->response[ 11]="Why don't you<*";
+    $this->response[ 12]="Do you wish to be able to<*";
+    $this->response[ 13]="Does that trouble you?";
+    $this->response[ 14]="Tell me more about such feelings.";
+    $this->response[ 15]="Do you often feel<*";
+    $this->response[ 16]="Do you enjoy feeling<*";
+    $this->response[ 17]="Do you really believe I don't<*";
+    $this->response[ 18]="Perhaps in good time I will<@";
+    $this->response[ 19]="Do you want me to<*";
+    $this->response[ 20]="Do you think you should be able to<*";
+    $this->response[ 21]="Why can't you<*";
+    $this->response[ 22]="Why are you interested in whether or not I am<*";
+    $this->response[ 23]="Would you prefer if I were not<*";
+    $this->response[ 24]="Perhaps in your fantasies I am<*";
+    $this->response[ 25]="How do you know you can't<*";
+    $this->response[ 26]="Have you tried?";
+    $this->response[ 27]="Perhaps you can now<*";
+    $this->response[ 28]="Did you come to me because you are<*";
+    $this->response[ 29]="How long have you been<*";
+    $this->response[ 30]="Do you believe it is normal to be<*";
+    $this->response[ 31]="Do you enjoy being<*";
+    $this->response[ 32]="We were discussing you, not me.";
+    $this->response[ 33]="Oh... <*";
+    $this->response[ 34]="You're not really talking about me, are you?";
+    $this->response[ 35]="What would it mean to you if you got<*";
+    $this->response[ 36]="Why do you want<*";
+    $this->response[ 37]="Suppose you got<*";
+    $this->response[ 38]="What if you never got<*";
+    $this->response[ 39]="I sometimes also want<@";
+    $this->response[ 40]="Why do you ask?";
+    $this->response[ 41]="Does that question interest you?";
+    $this->response[ 42]="What answer would please you the most?";
+    $this->response[ 43]="What do you think?";
+    $this->response[ 44]="Are such questions on your mind often?";
+    $this->response[ 45]="What is it that you really want to know?";
+    $this->response[ 46]="Have you asked anyone else?";
+    $this->response[ 47]="Have you asked such questions before?";
+    $this->response[ 48]="What else comes to mind when you ask that?";
+    $this->response[ 49]="Names don't interest me.";
+    $this->response[ 50]="I don't care about names, please go on.";
+    $this->response[ 51]="Is that the real reason?";
+    $this->response[ 52]="Don't any other reasons come to mind?";
+    $this->response[ 53]="Does that reason explain anything else?";
+    $this->response[ 54]="What other reasons might there be?";
+    $this->response[ 55]="Please don't apologise!";
+    $this->response[ 56]="Apologies are not necessary.";
+    $this->response[ 57]="What feelings do you have when you apologise?";
+    $this->response[ 58]="Don't be so defensive!";
+    $this->response[ 59]="What does that dream suggest to you?";
+    $this->response[ 60]="Do you dream often?";
+    $this->response[ 61]="What persons appear in your dreams?";
+    $this->response[ 62]="Are you disturbed by your dreams?";
+    $this->response[ 63]="How are you today.. What would you like to discuss?";
+    $this->response[ 64]="You don't seem quite certain.";
+    $this->response[ 65]="Why the uncertain tone?";
+    $this->response[ 66]="Can't you be more positive?";
+    $this->response[ 67]="You aren't sure?";
+    $this->response[ 68]="Don't you know?";
+    $this->response[ 69]="Are you saying no just to be negative?";
+    $this->response[ 70]="You are being a bit negative.";
+    $this->response[ 71]="Why not?";
+    $this->response[ 72]="Are you sure?";
+    $this->response[ 73]="Why no?";
+    $this->response[ 74]="Why are you concerned about my<*";
+    $this->response[ 75]="What about your own<*";
+    $this->response[ 76]="Can you think of a specific example?";
+    $this->response[ 77]="When?";
+    $this->response[ 78]="What are you thinking of?";
+    $this->response[ 79]="Really, always?";
+    $this->response[ 80]="Do you really think so?";
+    $this->response[ 81]="But you are not sure you<*";
+    $this->response[ 82]="Do you doubt you<*";
+    $this->response[ 83]="In what way?";
+    $this->response[ 84]="What resemblence do you see?";
+    $this->response[ 85]="What does the similarity suggest to you?";
+    $this->response[ 86]="What other connections do you see?";
+    $this->response[ 87]="Could there really be some connection?";
+    $this->response[ 88]="How?";
+    $this->response[ 89]="You seem quite positive.";
+    $this->response[ 90]="Are you Sure?";
+    $this->response[ 91]="I see.";
+    $this->response[ 92]="I understand.";
+    $this->response[ 93]="Why do you bring up the topic of friends?";
+    $this->response[ 94]="Do your friends worry you?";
+    $this->response[ 95]="Do your friends pick on you?";
+    $this->response[ 96]="Are you sure you have any friends?";
+    $this->response[ 97]="Do you impose on your friends?";
+    $this->response[ 98]="Perhaps your love for friends worries you.";
+    $this->response[ 99]="Do computers worry you?";
+    $this->response[100]="Are you talking about me in particular?";
+    $this->response[101]="Are you frightened by machines?";
+    $this->response[102]="Why do you mention computers?";
+    $this->response[103]="What do you think machines have to do with your problems?";
+    $this->response[104]="Don't you think computers can help people?";
+    $this->response[105]="What is it about machines that worries you?";
+    $this->response[106]="Say, do you have any psychological problems?";
+    $this->response[107]="What does that suggest to you?";
+    $this->response[108]="I see.";
+    $this->response[109]="I'm not sure I understand you fully.";
+    $this->response[110]="Come, come, elucidate your thoughts.";
+    $this->response[111]="Can you elaborate on that?";
+    $this->response[112]="That is quite interesting.";
+    $this->response[113]="Why did you repeat yourself?";
+    $this->response[114]="Do you expect a different answer by repeating yourself?";
+    $this->response[115]="Come, come, elucidate your thoughts.";
+    $this->response[116]="Please don't repeat yourself!";
+  }
+  
+}
+
+/**
+ * Keyword class
+ */
+
+class Psychotherapist_Key
+{
+  /**
+   * Phrase to match
+   * @var string
+   */
+   
+  public $key = '';
+  
+  /**
+   * First response to use
+   * @var int
+   */
+  
+  public $idx = 0;
+  
+  /**
+   * Last response to use
+   * @var int
+   */
+  
+  public $end = 0;
+  
+  /**
+   * Response last used time
+   * @var int
+   */
+  
+  public $last = 0;
+  
+  /**
+   * Constructor.
+   * @param string Key
+   * @param int Index
+   * @param int End
+   */
+  
+  public function __construct($key, $idx, $end)
+  {
+    $this->key = $key;
+    $this->idx = $idx;
+    $this->end = $end;
+  }
+}
+
+ 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/enanobot	Mon Aug 25 12:34:26 2008 -0400
@@ -0,0 +1,11 @@
+#!/bin/bash
+[ -n "$1" ] &&
+{
+  php $(dirname $0)/enanobot.php $@
+  exit $?
+}
+while true; do
+  php $(dirname $0)/enanobot.php
+  [ x$? = x2 ] && break
+done &
+
--- a/enanobot.php	Fri May 09 22:37:57 2008 -0400
+++ b/enanobot.php	Mon Aug 25 12:34:26 2008 -0400
@@ -1,5 +1,4 @@
 <?php
-
 /**
  * EnanoBot - the Enano CMS IRC logging and help automation bot
  * GPL and no warranty, see the LICENSE file for more info
@@ -53,12 +52,44 @@
   }
 }
 
+$censored_words = array('cock', 'fuck', 'cuck', 'funt', 'cunt', 'bitch');
+$_shutdown = false;
+
+function eb_censor_words($text)
+{
+  // return $text;
+  
+  global $censored_words;
+  foreach ( $censored_words as $word )
+  {
+    $replacement = substr($word, 0, 1) . preg_replace('/./', '*', substr($word, 1));
+    while ( stristr($text, $word) )
+    {
+      $text = preg_replace("/$word/i", $replacement, $text);
+    }
+  }
+  return $text;
+}
+
 require('libirc.php');
+require('hooks.php');
 require('config.php');
 
 @ini_set('display_errors', 'on');
+error_reporting(E_ALL);
+
+// load modules
+foreach ( $modules as $module )
+{
+  $modulefile = "modules/$module.php";
+  if ( file_exists($modulefile) )
+  {
+    require($modulefile);
+  }
+}
 
 $mysql_conn = false;
+$doctor = array();
 
 function mysql_reconnect()
 {
@@ -116,7 +147,7 @@
 
 $libirc_channels = array();
 
-$irc = new Request_IRC('irc.freenode.net');
+$irc = new Request_IRC($server);
 $irc->connect($nick, $user, $name, $pass);
 $irc->set_privmsg_handler('enanobot_privmsg_event');
 
@@ -138,15 +169,16 @@
   $sockdata = trim($sockdata);
   $message = Request_IRC::parse_message($sockdata);
   $channelname = $chan->get_channel_name();
-  enanobot_log_message($chan, $message);
+  
+  eval(eb_fetch_hook('event_raw_message'));
+  
   switch ( $message['action'] )
   {
     case 'JOIN':
-      // if a known op joins the channel, send mode +o
-      if ( in_array($message['nick'], $privileged_list) )
-      {
-        $chan->parent->put("MODE $channelname +o {$message['nick']}\r\n");
-      }
+      eval(eb_fetch_hook('event_join'));
+      break;
+    case 'PART':
+      eval(eb_fetch_hook('event_part'));
       break;
     case 'PRIVMSG':
       enanobot_process_channel_message($sockdata, $chan, $message);
@@ -158,113 +190,14 @@
 {
   global $irc, $nick, $mysql_conn, $privileged_list;
   
-  if ( preg_match('/^\!echo /', $message['message']) && in_array($message['nick'], $privileged_list) )
-  {
-    $chan->msg(preg_replace('/^\!echo /', '', $message['message']), true);
-  }
-  else if ( preg_match('/^\![\s]*([a-z0-9_-]+)([\s]*\|[\s]*([^ ]+))?$/', $message['message'], $match) )
+  if ( strpos($message['message'], $nick) && !in_array($message['nick'], $privileged_list) && $message['nick'] != $nick )
   {
-    $snippet =& $match[1];
-    if ( @$match[3] === 'me' )
-      $match[3] = $message['nick'];
-    $target_nick = ( !empty($match[3]) ) ? "{$match[3]}, " : "{$message['nick']}, ";
-    if ( $snippet == 'snippets' )
-    {
-      // list available snippets
-      $m_et = false;
-      $q = eb_mysql_query('SELECT snippet_code, snippet_channels FROM snippets;');
-      if ( mysql_num_rows($q) < 1 )
-      {
-        $chan->msg("{$message['nick']}, I couldn't find that snippet (\"$snippet\") in the database.", true);
-      }
-      else
-      {
-        $snippets = array();
-        while ( $row = mysql_fetch_assoc($q) )
-        {
-          $channels = explode('|', $row['snippet_channels']);
-          if ( in_array($chan->get_channel_name(), $channels) )
-          {
-            $snippets[] = $row['snippet_code'];
-          }
-        }
-        $snippets = implode(', ', $snippets);
-        $chan->msg("{$message['nick']}, the following snippets are available: $snippets", true);
-      }
-      @mysql_free_result($q);
-    }
-    else
-    {
-      // Look for the snippet...
-      $q = eb_mysql_query('SELECT snippet_text, snippet_channels FROM snippets WHERE snippet_code = \'' . mysql_real_escape_string($snippet) . '\';');
-      if ( mysql_num_rows($q) < 1 )
-      {
-        $chan->msg("{$message['nick']}, I couldn't find that snippet (\"$snippet\") in the database.", true);
-      }
-      else
-      {
-        $row = mysql_fetch_assoc($q);
-        $channels = explode('|', $row['snippet_channels']);
-        if ( in_array($chan->get_channel_name(), $channels) )
-        {
-          $chan->msg("{$target_nick}{$row['snippet_text']}", true);
-        }
-        else
-        {
-          $chan->msg("{$message['nick']}, I couldn't find that snippet (\"$snippet\") in the database.", true);
-        }
-      }
-      @mysql_free_result($q);
-    }
+    //$target_nick =& $message['nick'];
+    //$chan->msg("{$target_nick}, I'm only a bot. :-) You should probably rely on the advice of humans if you need further assistance.", true);
   }
-  else if ( strpos($message['message'], $nick) && !in_array($message['nick'], $privileged_list) && $message['nick'] != $nick )
-  {
-    $target_nick =& $message['nick'];
-    $chan->msg("{$target_nick}, I'm only a bot. :-) You should probably rely on the advice of humans if you need further assistance.", true);
-  }
-}
-
-function enanobot_log_message($chan, $message)
-{
-  global $nick;
-  
-  // Log the message
-  $chan_db = mysql_real_escape_string($chan->get_channel_name());
-  $nick_db = mysql_real_escape_string($message['nick']);
-  $line_db = mysql_real_escape_string($message['message']);
-  $day     = date('Y-m-d');
-  $time    = time();
-  $m_et = false;
-  $sql = false;
-  switch($message['action'])
+  else
   {
-    case 'PRIVMSG':
-      if ( substr($line_db, 0, 5) != '[off]' )
-      {
-        $sql = "INSERT INTO irclog(channel, day, nick, timestamp, line) VALUES
-                  ( '$chan_db', '$day', '$nick_db', '$time', '$line_db' );";
-      }
-      break;
-    case 'JOIN':
-      $sql = "INSERT INTO irclog(channel, day, nick, timestamp, line) VALUES
-                ( '$chan_db', '$day', '', '$time', '$nick_db has joined $chan_db' );";
-      break;
-    case 'PART':
-      $sql = "INSERT INTO irclog(channel, day, nick, timestamp, line) VALUES
-                ( '$chan_db', '$day', '', '$time', '$nick_db has left $chan_db' );";
-      break;
-    case 'MODE':
-      list($mode, $target_nick) = explode(' ', $line_db);
-      if ( $message['nick'] != 'ChanServ' && $target_nick != $nick )
-      {
-        $sql = "INSERT INTO irclog(channel, day, nick, timestamp, line) VALUES
-                  ( '$chan_db', '$day', '', '$time', '$nick_db set mode $mode on $target_nick' );";
-      }
-      break;
-  }
-  if ( $sql )
-  {
-    eb_mysql_query($sql);
+    eval(eb_fetch_hook('event_channel_msg'));
   }
 }
 
@@ -277,7 +210,7 @@
     foreach ( $irc->channels as $channel )
     {
       $part_cache[] = array($channel->get_channel_name(), $channel->get_handler());
-      $channel->msg("I've received a request to stop logging messages and responding to requests from {$message['nick']}. Don't forget to unsuspend me with /msg $nick Resume when finished.", true);
+      $channel->msg("I've received a request from {$message['nick']} to stop responding to requests, messages, and activities. Don't forget to unsuspend me with /msg $nick Resume when finished.", true);
       $channel->part("Logging and presence suspended by {$message['nick']}", true);
     }
   }
@@ -295,17 +228,21 @@
   }
   else if ( in_array($message['nick'], $privileged_list) && $message['message'] == 'Shutdown' && $message['action'] == 'PRIVMSG' )
   {
+    $GLOBALS['_shutdown'] = true;
     $irc->close("Remote bot shutdown ordered by {$message['nick']}", true);
     return 'BREAK';
   }
-  else if ( in_array($message['nick'], $privileged_list) && preg_match("/^\!echo-([^\007, \r\n\a\t]+) /", $message['message'], $match) )
+  else if ( $message['action'] == 'PRIVMSG' )
   {
-    global $libirc_channels;
-    $channel_name =& $match[1];
-    if ( isset($libirc_channels[$channel_name]) && is_object($libirc_channels[$channel_name]) )
-    {
-      $libirc_channels[$channel_name]->msg(preg_replace("/^\!echo-([^\007, \r\n\a\t]+) /", '', $message['message']), true);
-    }
+    eval(eb_fetch_hook('event_privmsg'));
+  }
+  else
+  {
+    eval(eb_fetch_hook('event_other'));
   }
 }
 
+if ( $_shutdown )
+{
+  exit(2);
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/hooks.php	Mon Aug 25 12:34:26 2008 -0400
@@ -0,0 +1,25 @@
+<?php
+
+global $eb_hooks;
+$eb_hooks = array();
+
+function eb_hook($hook_name, $code)
+{
+  global $eb_hooks;
+  if ( !isset($eb_hooks[$hook_name]) )
+    $eb_hooks[$hook_name] = array();
+  
+  $eb_hooks[$hook_name][] = $code;
+}
+
+function eb_fetch_hook($hook_name)
+{
+  global $eb_hooks;
+  return ( isset($eb_hooks[$hook_name]) ) ? implode("\n", $eb_hooks[$hook_name]) : 'eb_void();';
+}
+
+// null function for filling empty hooks
+function eb_void()
+{
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/htdocs/24hours.php	Mon Aug 25 12:34:26 2008 -0400
@@ -0,0 +1,60 @@
+<?php
+
+require('../stats-fe.php');
+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;
+
+// generate the data
+// we're doing this by absolute hours, not by strictly "24 hours ago", e.g. on-the-hour stats
+$this_hour = gmmktime(gmdate('H'), 0, 0);
+$graphdata = array();
+
+for ( $i = 23; $i >= 0; $i-- )
+{
+  $basetime = $this_hour - ( $i * 3600 );
+  $ts = date('H:i', $basetime);
+  $basetime += 3600;
+  $graphdata[$ts] = stats_message_count($channel, 60, $basetime);
+}
+
+$max = max($graphdata);
+
+// Determine axis interval
+$interval = 2;
+if ( $max > 20 )
+  $interval = 4;
+if ( $max > 25 )
+  $interval = 5;
+if ( $max > 50 )
+  $interval = 10;
+if ( $max > 200 )
+  $interval = 40;
+if ( $max > 500 )
+  $interval = 80;
+if ( $max > 1000 )
+  $interval = 100;
+if ( $max > 2000 )
+  $interval = 200;
+if ( $max > 5000 )
+  $interval = 1000;
+if ( $max > 15000 )
+  $interval = 1500;
+if ( $max > 30000 )
+  $interval = round($max / 10);
+
+$g = new GraphMaker(); // _Compat();
+
+$g->SetGraphPadding(20, 30, 20, 15);
+$g->SetGraphAreaHeight(200);
+$g->SetBarPadding(10);
+$g->SetBarData($graphdata);
+$g->SetGraphBackgroundTransparent(240, 250, 255, 0);
+$g->SetGraphTransparency(25);
+$g->SetAxisStep($interval);
+$g->SetGraphTitle($channel . ' message count - last 24 hours');
+
+$g->DrawGraph();
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/htdocs/changetz.php	Mon Aug 25 12:34:26 2008 -0400
@@ -0,0 +1,99 @@
+<?php
+require('../timezone.php');
+$set_zone = false;
+if ( isset($_POST['tz']) )
+{
+  if ( in_array($_POST['tz'], $zones) )
+  {
+    setcookie(COOKIE_NAME, $_POST['tz'], time() + ( 365 * 24 * 60 * 60 ));
+    $tz = $_POST['tz'];
+    date_default_timezone_set($_POST['tz']);
+    $set_zone = str_replace('_', ' ', str_replace('/', ': ', $tz));
+  }
+}
+?><html>
+  <head>
+    <title>Change time zone</title>
+    <style type="text/css">
+    select, option {
+      background-color: white;
+    }
+    option.other {
+      color: black;
+      font-weight: normal;
+    }
+    option.region {
+      color: black;
+      font-weight: bold;
+    }
+    option.area {
+      color: black;
+      font-weight: normal;
+      padding-left: 1em;
+    }
+    option.country {
+      color: black;
+      font-weight: bold;
+      padding-left: 1em;
+    }
+    option.city {
+      color: black;
+      font-weight: normal;
+      padding-left: 2em;
+    }
+    div.success {
+      border: 1px solid #006300;
+      background-color: #d3ffd3;
+      padding: 10px;
+      margin: 10px 0;
+    }
+    </style>
+  </head>
+  <body>
+    <?php
+    if ( $set_zone )
+    {
+      $target = dirname($_SERVER['PHP_SELF']) . '/';
+      echo '<div class="success">' . "Successfully set time zone to <b>{$set_zone}</b>. <a href=\"$target\">Return to the stats page</a>." . '</div>';
+    }
+    ?>
+    <form action="<?php echo $_SERVER['PHP_SELF']; ?>" method="post">
+    Select time zone:
+    <select name="tz">
+      <?php
+      $zones = get_timezone_list();
+      foreach ( $zones as $region => $areas )
+      {
+        if ( is_string($areas) )
+        {
+          echo '<option value="' . $areas . '" class="other">' . $areas . '</option>' . "\n      ";
+          continue;
+        }
+        echo '<option disabled="disabled" class="region">' . $region . '</option>' . "\n      ";
+        foreach ( $areas as $aid => $area )
+        {
+          if ( is_array($area) )
+          {
+            echo '  <option disabled="disabled" class="country">' . str_replace('_', ' ', $aid) . '</option>' . "\n      ";
+            foreach ( $area as $city )
+            {
+              $zoneid = "$region/$aid/$city";
+              $sel = ( $zoneid == $tz ) ? ' selected="selected"' : '';
+              echo '    <option value="' . $zoneid . '" class="city"' . $sel . '>' . str_replace('_', ' ', $city) . '</option>' . "\n      ";
+            }
+          }
+          else
+          {
+            $zoneid = "$region/$area";
+            $sel = ( $zoneid == $tz ) ? ' selected="selected"' : '';
+            echo '  <option value="' . $zoneid . '" class="area"' . $sel . '>' . str_replace('_', ' ', $area) . '</option>' . "\n      ";
+          }
+        }
+      }
+      ?>
+    </select>
+    <input type="submit" value="Save" /><br />
+    <small>Make sure you have cookies enabled.</small>
+    </form>
+  </body>
+</html>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/htdocs/datafile.php	Mon Aug 25 12:34:26 2008 -0400
@@ -0,0 +1,6 @@
+<?php
+header('Content-type: application/force-download');
+header('Content-disposition: attachment; filename=stats-data.php');
+
+echo file_get_contents('../stats-data.php');
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/htdocs/index.php	Mon Aug 25 12:34:26 2008 -0400
@@ -0,0 +1,93 @@
+<?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;
+?>
+
+<html>
+  <head>
+    <title><?php echo $nick; ?> - Statistics</title>
+    <style type="text/css">
+    div.footer {
+      font-size: smaller;
+      padding-top: 10px;
+      margin-top: 10px;
+      border-top: 1px solid #aaa;
+    }
+    </style>
+  </head>
+  <body>
+    <div style="float: right;">
+      <p>
+        <?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', filemtime('../stats-data.php')) . '.</small>';
+        ?>
+      </p>
+      <p>
+        <big><b>Channels:</b></big><br />
+        <?php
+          foreach ( $channels as $i => $c )
+          {
+            $bold = ( $c == $channel );
+            echo $bold ? '<b>' : '';
+            echo $bold ? '' : '<a href="index.php?channel=' . urlencode($c) . '">';
+            echo $c;
+            echo $bold ? '' : '</a>';
+            echo $bold ? '</b>' : '';
+            echo $i == count($channels) - 1 ? '' : ' | ';
+          }
+        ?>
+      </p>
+    </div>
+    <h1>Active members</h1>
+    <p>For the last 1, 5, and 15 minutes:
+        <?php echo count(stats_activity_percent($channel, 1)) . ', ' .
+                   count(stats_activity_percent($channel, 5)) . ', ' .
+                   count(stats_activity_percent($channel, 15)) . ' (respectively)';
+        ?>
+        </p>
+    <h1>Currently active members:</h1>
+    <p>These people have posted in the last 3 minutes:</p>
+    <ul>
+      <?php
+      $datum = stats_activity_percent($channel, 3);
+      $count = stats_message_count($channel, 3);
+      if ( empty($datum) )
+        echo '<li>No recent posts.</li>';
+      foreach ( $datum as $nick => $pct )
+      {
+        $total = round($pct * $count);
+        $pct = round(100 * $pct, 1);
+        echo "<li>$nick - $pct% ($total)</li>\n";
+      }
+      ?>
+    </ul>
+    <p>Last 20 minutes:</p>
+    <ul>
+      <?php
+      $datum = stats_activity_percent($channel, 20);
+      $count = stats_message_count($channel, 20);
+      if ( empty($datum) )
+        echo '<li>No recent posts.</li>';
+      foreach ( $datum as $nick => $pct )
+      {
+        $total = round($pct * $count);
+        $pct = round(100 * $pct, 1);
+        echo "<li>$nick - $pct% ($total)</li>\n";
+      }
+      ?>
+    </ul>
+    <h1>Last 24 hours</h1>
+    <img alt="Graph image" src="24hours.php?channel=<?php echo urlencode($channel); ?>" />
+    
+    <div class="footer">
+    <b><?php echo $nick; ?> is a privacy-respecting bot.</b> <a href="privacy.php">Read about what information <?php echo $nick; ?> collects</a>
+    </div>
+  </body>
+</head>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/htdocs/json.php	Mon Aug 25 12:34:26 2008 -0400
@@ -0,0 +1,6 @@
+<?php
+require('../stats-data.php');
+require('../libjson.php');
+
+header('Content-type: text/plain');
+echo eb_json_encode($stats_data);
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/htdocs/privacy.php	Mon Aug 25 12:34:26 2008 -0400
@@ -0,0 +1,32 @@
+<?php
+require('../config.php');
+
+?><html>
+  <head>
+    <title><?php echo $nick; ?> - privacy info</title>
+    <style type="text/css">
+    p.code {
+      font-family: monospace;
+      margin-left: 1.5em;
+    }
+    </style>
+  </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>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><?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 class="code">/msg <?php echo $nick; ?> anonymize</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>
+  </body>
+</head>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/htdocs/snippets.php	Mon Aug 25 12:34:26 2008 -0400
@@ -0,0 +1,164 @@
+<?php
+
+/**
+ * EnanoBot - copyright (C) 2008 Dan Fuhry
+ * All rights reserved.
+ */
+
+/*****************************************************************
+ * YOU NEED TO SET THE PATH TO THE REST OF THE EnanoBot FILES HERE.
+ * Include a trailing slash.
+ * This script MUST be placed in an Enano installation directory.
+ *****************************************************************/
+
+define('ENANOBOT_ROOT', dirname(__FILE__) . '/');
+
+// load Enano for auth
+/*
+require('includes/common.php');
+if ( $session->user_level < USER_LEVEL_ADMIN )
+{
+  die_friendly('Access denied', '<p>Admin rights needed to use this script.</p>');
+}
+
+$db->close();
+unset($db, $session, $paths, $template, $plugins);
+*/
+
+// We're authed.
+// Load config
+require(ENANOBOT_ROOT . 'config.php');
+
+// check config
+if ( empty($mysql_host) || empty($mysql_user) || empty($mysql_dbname) )
+{
+  die("Bad config file - have a look at config-sample.php.\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 mysql_die()
+{
+  $m_e = mysql_error();
+  die("MySQL error: $m_e");
+}
+
+?><!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
+<html>
+  <head>
+    <title>EnanoBot snippet management</title>
+    <meta http-equiv="Content-type" content="text/html; charset=utf-8" />
+  </head>
+  <body>
+    <h1>EnanoBot snippet management</h1>
+    <form action="snippets.php" method="post" enctype="multipart/form-data">
+      <fieldset>
+        <legend>Add a snippet</legend>
+        <table border="1" cellspacing="0" cellpadding="4">
+          <tr>
+            <td>Snippet code<br />
+                <small>all lowercase, no spaces; ex: mysnippet</small></td>
+            <td><input type="text" name="snippet_add_code" size="100" tabindex="1" /></td>
+          </tr>
+          <tr>
+            <td>Text<br />
+                <small>anything you want, keep it relatively short.</small></td>
+            <td><input type="text" name="snippet_add_text" size="100" tabindex="2" /></td>
+          </tr>
+          <tr>
+            <td>Channels<br />
+                <small>separate with pipe characters, ex: #enano|#enano-dev|#ubuntu</small></td>
+            <td><input type="text" name="snippet_add_channels" size="100" tabindex="3" /></td>
+          </tr>
+        </table>
+      </fieldset>
+      <fieldset>
+        <legend>Edit existing snippets</legend>
+        <table border="1" cellspacing="0" cellpadding="4">
+          <tr>
+            <th>Code</th>
+            <th>Snippet text</th>
+            <th>Channels</th>
+            <th>Delete</th>
+          </tr>
+        <?php
+            if ( !empty($_POST['snippet_add_code']) && !empty($_POST['snippet_add_text']) && !empty($_POST['snippet_add_channels']) )
+            {
+              $code = mysql_real_escape_string($_POST['snippet_add_code']);
+              $text = mysql_real_escape_string($_POST['snippet_add_text']);
+              $channels = mysql_real_escape_string($_POST['snippet_add_channels']);
+              $q2 = @mysql_query("INSERT INTO snippets(snippet_code, snippet_text, snippet_channels) VALUES
+                                    ( '$code', '$text', '$channels' );", $mysql_conn);
+              if ( !$q2 )
+                mysql_die();
+            }
+            $q = @mysql_query('SELECT snippet_id, snippet_code, snippet_text, snippet_channels FROM snippets ORDER BY snippet_code ASC;');
+            if ( !$q )
+              mysql_die();
+            while ( $row = @mysql_fetch_assoc($q) )
+            {
+              if ( isset($_POST['snippet']) && @is_array(@$_POST['snippet']) )
+              {
+                if ( isset($_POST['snippet'][$row['snippet_id']]) )
+                {
+                  // delete it?
+                  if ( isset($_POST['snippet'][$row['snippet_id']]['delete']) )
+                  {
+                    $q2 = mysql_query("DELETE FROM snippets WHERE snippet_id = {$row['snippet_id']};", $mysql_conn);
+                    if ( !$q2 )
+                      mysql_die();
+                    continue;
+                  }
+                  // has it changed?
+                  else if ( $_POST['snippet'][$row['snippet_id']]['code'] != $row['snippet_code'] ||
+                       $_POST['snippet'][$row['snippet_id']]['text'] != $row['snippet_text'] ||
+                       $_POST['snippet'][$row['snippet_id']]['channels'] != $row['snippet_channels'] )
+                  {
+                    // yeah, update it.
+                    $code = mysql_real_escape_string($_POST['snippet'][$row['snippet_id']]['code']);
+                    $text = mysql_real_escape_string($_POST['snippet'][$row['snippet_id']]['text']);
+                    $channels = mysql_real_escape_string($_POST['snippet'][$row['snippet_id']]['channels']);
+                    $q2 = mysql_query("UPDATE snippets SET snippet_code = '$code', snippet_text = '$text', snippet_channels = '$channels' WHERE snippet_id = {$row['snippet_id']};", $mysql_conn);
+                    if ( !$q2 )
+                      mysql_die();
+                    $row = array(
+                        'snippet_id' => $row['snippet_id'],
+                        'snippet_code' => $_POST['snippet'][$row['snippet_id']]['code'],
+                        'snippet_text' => $_POST['snippet'][$row['snippet_id']]['text'],
+                        'snippet_channels' => $_POST['snippet'][$row['snippet_id']]['channels']
+                      );
+                  }
+                }
+              }
+              echo '  <tr>';
+              echo '<td><input type="text" name="snippet[' . $row['snippet_id'] . '][code]" value="' . htmlspecialchars($row['snippet_code']) . '" /></td>';
+              echo '<td><input type="text" size="100" name="snippet[' . $row['snippet_id'] . '][text]" value="' . htmlspecialchars($row['snippet_text']) . '" /></td>';
+              echo '<td><input type="text" name="snippet[' . $row['snippet_id'] . '][channels]" value="' . htmlspecialchars($row['snippet_channels']) . '" /></td>';
+              echo '<td style="text-align: center;"><input type="checkbox" name="snippet[' . $row['snippet_id'] . '][delete]" /></td>';
+              echo '</tr>' . "\n        ";
+            }
+          ?></table>
+      </fieldset>
+      <div style="text-align: center; margin-top: 20px;">
+        <input type="submit" value="Save changes" />
+      </div>
+    </form>
+  </body>
+</html><?php
+
+mysql_close($mysql_conn);
+
--- a/libirc.php	Fri May 09 22:37:57 2008 -0400
+++ b/libirc.php	Mon Aug 25 12:34:26 2008 -0400
@@ -112,44 +112,31 @@
     if ( !$this->sock )
       throw new Exception('Could not make socket connection to host.');
     
-    stream_set_timeout($this->sock, 5);
-    
-    // Wait for initial ident messages
-    while ( $msg = $this->get() )
-    {
-    }
+    stream_set_timeout($this->sock, 1);
     
     // Send nick and username
     $this->put("NICK $nick\r\n");
     $this->put("USER $username 0 * :$realname\r\n");
     
-    // Wait for response and end of motd
-    $motd = '';
-    while ( $msg = $this->get() )
+    // wait for a mode +i or end of the motd
+    while ( true )
     {
-      // Match particles
-      $msg = trim($msg);
-      $mc = preg_match('/^:([A-z0-9\.-]+) ([0-9]+) [A-z0-9_-]+ :(.+)$/', $msg, $match);
-      if ( !$mc )
+      $msg = $this->get();
+      if ( empty($msg) )
+        continue;
+      if ( ( strstr($msg, 'MODE') && strstr($msg, '+i') ) || strstr(strtolower($msg), 'end of /motd') )
       {
-        $mc = preg_match('/^:([A-z0-9_-]+)!([A-z0-9_-]+)@([A-z0-9_\.-]+) NOTICE [A-z0-9_-]+ :(.+)$/', $msg, $match);
-        if ( !$mc )
-          continue;
-        // Look for a response from NickServ
-        if ( $match[1] == 'NickServ' )
-        {
-          // Asking for auth?
-          if ( strpos($match[4], 'IDENTIFY') )
-          {
-            // Yes, send password
-            $this->privmsg('NickServ', "IDENTIFY $pass");
-          }
-        }
+        break;
       }
-      list(, $host, $stat, $msg) = $match;
-      $motd .= "$msg";
+      if ( preg_match('/^PING :(.+?)$/', $msg, $match) )
+      {
+        $this->put("PONG :{$match[1]}\r\n");
+      }
     }
     
+    // identify to nickserv
+    $this->privmsg('NickServ', "IDENTIFY $pass");
+    
     $this->nick = $nick;
     $this->user = $username;
   }
@@ -222,11 +209,12 @@
       if ( preg_match('/^PING :(.+?)$/', $data_trim, $pmatch) )
       {
         $this->put("PONG :{$pmatch[1]}\r\n");
+        eval(eb_fetch_hook('event_ping'));
       }
       else if ( $match )
       {
         // Received PRIVMSG or other mainstream action
-        if ( $match['action'] == 'JOIN' )
+        if ( $match['action'] == 'JOIN' || $match['action'] == 'PART' )
           $channel =& $match['message'];
         else
           $channel =& $match['target'];
@@ -235,7 +223,7 @@
         {
           // Private message from user
           $result = $this->handle_privmsg($data);
-          stream_set_timeout($this->sock, 0xFFFFFFFE);
+          @stream_set_timeout($this->sock, 0xFFFFFFFE);
         }
         else if ( isset($this->channels[strtolower($channel)]) )
         {
@@ -243,7 +231,7 @@
           $chan =& $this->channels[strtolower($channel)];
           $func = $chan->get_handler();
           $result = @call_user_func($func, $data, $chan);
-          stream_set_timeout($this->sock, 0xFFFFFFFE);
+          @stream_set_timeout($this->sock, 0xFFFFFFFE);
         }
         if ( $result == 'BREAK' )
         {
@@ -397,13 +385,14 @@
   {
     $this->parent = $parent;
     $this->parent->put("JOIN $channel\r\n");
-    stream_set_timeout($this->parent->sock, 3);
-    while ( $msg = $this->parent->get() )
-    {
-      // Do nothing
-    }
+    // stream_set_timeout($this->parent->sock, 3);
+    // while ( $msg = $this->parent->get() )
+    // {
+    //   // Do nothing
+    // }
     $this->channel_name = $channel;
     $this->handler = $handler;
+    eval(eb_fetch_hook('event_self_join'));
   }
   
   /**
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libjson.php	Mon Aug 25 12:34:26 2008 -0400
@@ -0,0 +1,1037 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Json
+ * @copyright  Copyright (c) 2005-2007 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+
+
+/**
+ * Encode PHP constructs to JSON
+ *
+ * @category   Zend
+ * @package    Zend_Json
+ * @copyright  Copyright (c) 2005-2007 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Json_Encoder
+{
+    /**
+     * Whether or not to check for possible cycling
+     *
+     * @var boolean
+     */
+    protected $_cycleCheck;
+
+    /**
+     * Array of visited objects; used to prevent cycling.
+     *
+     * @var array
+     */
+    protected $_visited = array();
+
+    /**
+     * Constructor
+     *
+     * @param boolean $cycleCheck Whether or not to check for recursion when encoding
+     * @return void
+     */
+    protected function __construct($cycleCheck = false)
+    {
+        $this->_cycleCheck = $cycleCheck;
+    }
+
+    /**
+     * Use the JSON encoding scheme for the value specified
+     *
+     * @param mixed $value The value to be encoded
+     * @param boolean $cycleCheck Whether or not to check for possible object recursion when encoding
+     * @return string  The encoded value
+     */
+    public static function encode($value, $cycleCheck = false)
+    {
+        $encoder = new Zend_Json_Encoder(($cycleCheck) ? true : false);
+
+        return $encoder->_encodeValue($value);
+    }
+
+    /**
+     * Recursive driver which determines the type of value to be encoded
+     * and then dispatches to the appropriate method. $values are either
+     *    - objects (returns from {@link _encodeObject()})
+     *    - arrays (returns from {@link _encodeArray()})
+     *    - basic datums (e.g. numbers or strings) (returns from {@link _encodeDatum()})
+     *
+     * @param $value mixed The value to be encoded
+     * @return string Encoded value
+     */
+    protected function _encodeValue(&$value)
+    {
+        if (is_object($value)) {
+            return $this->_encodeObject($value);
+        } else if (is_array($value)) {
+            return $this->_encodeArray($value);
+        }
+
+        return $this->_encodeDatum($value);
+    }
+
+
+
+    /**
+     * Encode an object to JSON by encoding each of the public properties
+     *
+     * A special property is added to the JSON object called '__className'
+     * that contains the name of the class of $value. This is used to decode
+     * the object on the client into a specific class.
+     *
+     * @param $value object
+     * @return string
+     * @throws Zend_Json_Exception If recursive checks are enabled and the object has been serialized previously
+     */
+    protected function _encodeObject(&$value)
+    {
+        if ($this->_cycleCheck) {
+            if ($this->_wasVisited($value)) {
+                throw new Zend_Json_Exception(
+                    'Cycles not supported in JSON encoding, cycle introduced by '
+                    . 'class "' . get_class($value) . '"'
+                );
+            }
+
+            $this->_visited[] = $value;
+        }
+
+        $props = '';
+        foreach (get_object_vars($value) as $name => $propValue) {
+            if (isset($propValue)) {
+                $props .= ','
+                        . $this->_encodeValue($name)
+                        . ':'
+                        . $this->_encodeValue($propValue);
+            }
+        }
+
+        return '{"__className":"' . get_class($value) . '"'
+                . $props . '}';
+    }
+
+
+    /**
+     * Determine if an object has been serialized already
+     *
+     * @param mixed $value
+     * @return boolean
+     */
+    protected function _wasVisited(&$value)
+    {
+        if (in_array($value, $this->_visited, true)) {
+            return true;
+        }
+
+        return false;
+    }
+
+
+    /**
+     * JSON encode an array value
+     *
+     * Recursively encodes each value of an array and returns a JSON encoded
+     * array string.
+     *
+     * Arrays are defined as integer-indexed arrays starting at index 0, where
+     * the last index is (count($array) -1); any deviation from that is
+     * considered an associative array, and will be encoded as such.
+     *
+     * @param $array array
+     * @return string
+     */
+    protected function _encodeArray(&$array)
+    {
+        $tmpArray = array();
+
+        // Check for associative array
+        if (!empty($array) && (array_keys($array) !== range(0, count($array) - 1))) {
+            // Associative array
+            $result = '{';
+            foreach ($array as $key => $value) {
+                $key = (string) $key;
+                $tmpArray[] = $this->_encodeString($key)
+                            . ':'
+                            . $this->_encodeValue($value);
+            }
+            $result .= implode(',', $tmpArray);
+            $result .= '}';
+        } else {
+            // Indexed array
+            $result = '[';
+            $length = count($array);
+            for ($i = 0; $i < $length; $i++) {
+                $tmpArray[] = $this->_encodeValue($array[$i]);
+            }
+            $result .= implode(',', $tmpArray);
+            $result .= ']';
+        }
+
+        return $result;
+    }
+
+
+    /**
+     * JSON encode a basic data type (string, number, boolean, null)
+     *
+     * If value type is not a string, number, boolean, or null, the string
+     * 'null' is returned.
+     *
+     * @param $value mixed
+     * @return string
+     */
+    protected function _encodeDatum(&$value)
+    {
+        $result = 'null';
+
+        if (is_int($value) || is_float($value)) {
+            $result = (string)$value;
+        } elseif (is_string($value)) {
+            $result = $this->_encodeString($value);
+        } elseif (is_bool($value)) {
+            $result = $value ? 'true' : 'false';
+        }
+
+        return $result;
+    }
+
+
+    /**
+     * JSON encode a string value by escaping characters as necessary
+     *
+     * @param $value string
+     * @return string
+     */
+    protected function _encodeString(&$string)
+    {
+        // Escape these characters with a backslash:
+        // " \ / \n \r \t \b \f
+        $search  = array('\\', "\n", "\t", "\r", "\b", "\f", '"');
+        $replace = array('\\\\', '\\n', '\\t', '\\r', '\\b', '\\f', '\"');
+        $string  = str_replace($search, $replace, $string);
+
+        // Escape certain ASCII characters:
+        // 0x08 => \b
+        // 0x0c => \f
+        $string = str_replace(array(chr(0x08), chr(0x0C)), array('\b', '\f'), $string);
+
+        return '"' . $string . '"';
+    }
+
+
+    /**
+     * Encode the constants associated with the ReflectionClass
+     * parameter. The encoding format is based on the class2 format
+     *
+     * @param $cls ReflectionClass
+     * @return string Encoded constant block in class2 format
+     */
+    private static function _encodeConstants(ReflectionClass $cls)
+    {
+        $result    = "constants : {";
+        $constants = $cls->getConstants();
+
+        $tmpArray = array();
+        if (!empty($constants)) {
+            foreach ($constants as $key => $value) {
+                $tmpArray[] = "$key: " . self::encode($value);
+            }
+
+            $result .= implode(', ', $tmpArray);
+        }
+
+        return $result . "}";
+    }
+
+
+    /**
+     * Encode the public methods of the ReflectionClass in the
+     * class2 format
+     *
+     * @param $cls ReflectionClass
+     * @return string Encoded method fragment
+     *
+     */
+    private static function _encodeMethods(ReflectionClass $cls)
+    {
+        $methods = $cls->getMethods();
+        $result = 'methods:{';
+
+        $started = false;
+        foreach ($methods as $method) {
+            if (! $method->isPublic() || !$method->isUserDefined()) {
+                continue;
+            }
+
+            if ($started) {
+                $result .= ',';
+            }
+            $started = true;
+
+            $result .= '' . $method->getName(). ':function(';
+
+            if ('__construct' != $method->getName()) {
+                $parameters  = $method->getParameters();
+                $paramCount  = count($parameters);
+                $argsStarted = false;
+
+                $argNames = "var argNames=[";
+                foreach ($parameters as $param) {
+                    if ($argsStarted) {
+                        $result .= ',';
+                    }
+
+                    $result .= $param->getName();
+
+                    if ($argsStarted) {
+                        $argNames .= ',';
+                    }
+
+                    $argNames .= '"' . $param->getName() . '"';
+
+                    $argsStarted = true;
+                }
+                $argNames .= "];";
+
+                $result .= "){"
+                         . $argNames
+                         . 'var result = ZAjaxEngine.invokeRemoteMethod('
+                         . "this, '" . $method->getName()
+                         . "',argNames,arguments);"
+                         . 'return(result);}';
+            } else {
+                $result .= "){}";
+            }
+        }
+
+        return $result . "}";
+    }
+
+
+    /**
+     * Encode the public properties of the ReflectionClass in the class2
+     * format.
+     *
+     * @param $cls ReflectionClass
+     * @return string Encode properties list
+     *
+     */
+    private static function _encodeVariables(ReflectionClass $cls)
+    {
+        $properties = $cls->getProperties();
+        $propValues = get_class_vars($cls->getName());
+        $result = "variables:{";
+        $cnt = 0;
+
+        $tmpArray = array();
+        foreach ($properties as $prop) {
+            if (! $prop->isPublic()) {
+                continue;
+            }
+
+            $tmpArray[] = $prop->getName()
+                        . ':'
+                        . self::encode($propValues[$prop->getName()]);
+        }
+        $result .= implode(',', $tmpArray);
+
+        return $result . "}";
+    }
+
+    /**
+     * Encodes the given $className into the class2 model of encoding PHP
+     * classes into JavaScript class2 classes.
+     * NOTE: Currently only public methods and variables are proxied onto
+     * the client machine
+     *
+     * @param $className string The name of the class, the class must be
+     * instantiable using a null constructor
+     * @param $package string Optional package name appended to JavaScript
+     * proxy class name
+     * @return string The class2 (JavaScript) encoding of the class
+     * @throws Zend_Json_Exception
+     */
+    public static function encodeClass($className, $package = '')
+    {
+        $cls = new ReflectionClass($className);
+        if (! $cls->isInstantiable()) {
+            throw new Zend_Json_Exception("$className must be instantiable");
+        }
+
+        return "Class.create('$package$className',{"
+                . self::_encodeConstants($cls)    .","
+                . self::_encodeMethods($cls)      .","
+                . self::_encodeVariables($cls)    .'});';
+    }
+
+
+    /**
+     * Encode several classes at once
+     *
+     * Returns JSON encoded classes, using {@link encodeClass()}.
+     *
+     * @param array $classNames
+     * @param string $package
+     * @return string
+     */
+    public static function encodeClasses(array $classNames, $package = '')
+    {
+        $result = '';
+        foreach ($classNames as $className) {
+            $result .= self::encodeClass($className, $package);
+        }
+
+        return $result;
+    }
+
+}
+
+/**
+ * Decode JSON encoded string to PHP variable constructs
+ *
+ * @category   Zend
+ * @package    Zend_Json
+ * @copyright  Copyright (c) 2005-2007 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Json_Decoder
+{
+    /**
+     * Parse tokens used to decode the JSON object. These are not
+     * for public consumption, they are just used internally to the
+     * class.
+     */
+    const EOF          = 0;
+    const DATUM        = 1;
+    const LBRACE       = 2;
+    const LBRACKET     = 3;
+    const RBRACE       = 4;
+    const RBRACKET     = 5;
+    const COMMA        = 6;
+    const COLON        = 7;
+
+    /**
+     * Use to maintain a "pointer" to the source being decoded
+     *
+     * @var string
+     */
+    protected $_source;
+
+    /**
+     * Caches the source length
+     *
+     * @var int
+     */
+    protected $_sourceLength;
+
+    /**
+     * The offset within the souce being decoded
+     *
+     * @var int
+     *
+     */
+    protected $_offset;
+
+    /**
+     * The current token being considered in the parser cycle
+     *
+     * @var int
+     */
+    protected $_token;
+
+    /**
+     * Flag indicating how objects should be decoded
+     *
+     * @var int
+     * @access protected
+     */
+    protected $_decodeType;
+
+    /**
+     * Constructor
+     *
+     * @param string $source String source to decode
+     * @param int $decodeType How objects should be decoded -- see
+     * {@link Zend_Json::TYPE_ARRAY} and {@link Zend_Json::TYPE_OBJECT} for
+     * valid values
+     * @return void
+     */
+    protected function __construct($source, $decodeType)
+    {
+        
+        // eliminate comments
+        $source = preg_replace(array(
+
+                  // eliminate single line comments in '// ...' form
+                  '#^\s*//(.+)$#m',
+    
+                  // eliminate multi-line comments in '/* ... */' form, at start of string
+                  '#^\s*/\*(.+)\*/#Us',
+    
+                  // eliminate multi-line comments in '/* ... */' form, at end of string
+                  '#/\*(.+)\*/\s*$#Us'
+    
+              ), '', $source);
+        
+        // Set defaults
+        $this->_source       = $source;
+        $this->_sourceLength = strlen($source);
+        $this->_token        = self::EOF;
+        $this->_offset       = 0;
+
+        // Normalize and set $decodeType
+        if (!in_array($decodeType, array(Zend_Json::TYPE_ARRAY, Zend_Json::TYPE_OBJECT)))
+        {
+            $decodeType = Zend_Json::TYPE_ARRAY;
+        }
+        $this->_decodeType   = $decodeType;
+
+        // Set pointer at first token
+        $this->_getNextToken();
+    }
+
+    /**
+     * Decode a JSON source string
+     *
+     * Decodes a JSON encoded string. The value returned will be one of the
+     * following:
+     *        - integer
+     *        - float
+     *        - boolean
+     *        - null
+     *      - StdClass
+     *      - array
+     *         - array of one or more of the above types
+     *
+     * By default, decoded objects will be returned as associative arrays; to
+     * return a StdClass object instead, pass {@link Zend_Json::TYPE_OBJECT} to
+     * the $objectDecodeType parameter.
+     *
+     * Throws a Zend_Json_Exception if the source string is null.
+     *
+     * @static
+     * @access public
+     * @param string $source String to be decoded
+     * @param int $objectDecodeType How objects should be decoded; should be
+     * either or {@link Zend_Json::TYPE_ARRAY} or
+     * {@link Zend_Json::TYPE_OBJECT}; defaults to TYPE_ARRAY
+     * @return mixed
+     * @throws Zend_Json_Exception
+     */
+    public static function decode($source = null, $objectDecodeType = Zend_Json::TYPE_ARRAY)
+    {
+        if (null === $source) {
+            throw new Zend_Json_Exception('Must specify JSON encoded source for decoding');
+        } elseif (!is_string($source)) {
+            throw new Zend_Json_Exception('Can only decode JSON encoded strings');
+        }
+
+        $decoder = new self($source, $objectDecodeType);
+
+        return $decoder->_decodeValue();
+    }
+
+
+    /**
+     * Recursive driving rountine for supported toplevel tops
+     *
+     * @return mixed
+     */
+    protected function _decodeValue()
+    {
+        switch ($this->_token) {
+            case self::DATUM:
+                $result  = $this->_tokenValue;
+                $this->_getNextToken();
+                return($result);
+                break;
+            case self::LBRACE:
+                return($this->_decodeObject());
+                break;
+            case self::LBRACKET:
+                return($this->_decodeArray());
+                break;
+            default:
+                return null;
+                break;
+        }
+    }
+
+    /**
+     * Decodes an object of the form:
+     *  { "attribute: value, "attribute2" : value,...}
+     *
+     * If ZJsonEnoder or ZJAjax was used to encode the original object
+     * then a special attribute called __className which specifies a class
+     * name that should wrap the data contained within the encoded source.
+     *
+     * Decodes to either an array or StdClass object, based on the value of
+     * {@link $_decodeType}. If invalid $_decodeType present, returns as an
+     * array.
+     *
+     * @return array|StdClass
+     */
+    protected function _decodeObject()
+    {
+        $members = array();
+        $tok = $this->_getNextToken();
+
+        while ($tok && $tok != self::RBRACE) {
+            if ($tok != self::DATUM || ! is_string($this->_tokenValue)) {
+                throw new Zend_Json_Exception('Missing key in object encoding: ' . $this->_source);
+            }
+
+            $key = $this->_tokenValue;
+            $tok = $this->_getNextToken();
+
+            if ($tok != self::COLON) {
+                throw new Zend_Json_Exception('Missing ":" in object encoding: ' . $this->_source);
+            }
+
+            $tok = $this->_getNextToken();
+            $members[$key] = $this->_decodeValue();
+            $tok = $this->_token;
+
+            if ($tok == self::RBRACE) {
+                break;
+            }
+
+            if ($tok != self::COMMA) {
+                throw new Zend_Json_Exception('Missing "," in object encoding: ' . $this->_source);
+            }
+
+            $tok = $this->_getNextToken();
+        }
+
+        switch ($this->_decodeType) {
+            case Zend_Json::TYPE_OBJECT:
+                // Create new StdClass and populate with $members
+                $result = new StdClass();
+                foreach ($members as $key => $value) {
+                    $result->$key = $value;
+                }
+                break;
+            case Zend_Json::TYPE_ARRAY:
+            default:
+                $result = $members;
+                break;
+        }
+
+        $this->_getNextToken();
+        return $result;
+    }
+
+    /**
+     * Decodes a JSON array format:
+     *    [element, element2,...,elementN]
+     *
+     * @return array
+     */
+    protected function _decodeArray()
+    {
+        $result = array();
+        $starttok = $tok = $this->_getNextToken(); // Move past the '['
+        $index  = 0;
+
+        while ($tok && $tok != self::RBRACKET) {
+            $result[$index++] = $this->_decodeValue();
+
+            $tok = $this->_token;
+
+            if ($tok == self::RBRACKET || !$tok) {
+                break;
+            }
+
+            if ($tok != self::COMMA) {
+                throw new Zend_Json_Exception('Missing "," in array encoding: ' . $this->_source);
+            }
+
+            $tok = $this->_getNextToken();
+        }
+
+        $this->_getNextToken();
+        return($result);
+    }
+
+
+    /**
+     * Removes whitepsace characters from the source input
+     */
+    protected function _eatWhitespace()
+    {
+        if (preg_match(
+                '/([\t\b\f\n\r ])*/s',
+                $this->_source,
+                $matches,
+                PREG_OFFSET_CAPTURE,
+                $this->_offset)
+            && $matches[0][1] == $this->_offset)
+        {
+            $this->_offset += strlen($matches[0][0]);
+        }
+    }
+
+
+    /**
+     * Retrieves the next token from the source stream
+     *
+     * @return int Token constant value specified in class definition
+     */
+    protected function _getNextToken()
+    {
+        $this->_token      = self::EOF;
+        $this->_tokenValue = null;
+        $this->_eatWhitespace();
+        
+        if ($this->_offset >= $this->_sourceLength) {
+            return(self::EOF);
+        }
+
+        $str        = $this->_source;
+        $str_length = $this->_sourceLength;
+        $i          = $this->_offset;
+        $start      = $i;
+        
+        switch ($str{$i}) {
+            case '{':
+               $this->_token = self::LBRACE;
+               break;
+            case '}':
+                $this->_token = self::RBRACE;
+                break;
+            case '[':
+                $this->_token = self::LBRACKET;
+                break;
+            case ']':
+                $this->_token = self::RBRACKET;
+                break;
+            case ',':
+                $this->_token = self::COMMA;
+                break;
+            case ':':
+                $this->_token = self::COLON;
+                break;
+            case  '"':
+                $result = '';
+                do {
+                    $i++;
+                    if ($i >= $str_length) {
+                        break;
+                    }
+
+                    $chr = $str{$i};
+                    if ($chr == '\\') {
+                        $i++;
+                        if ($i >= $str_length) {
+                            break;
+                        }
+                        $chr = $str{$i};
+                        switch ($chr) {
+                            case '"' :
+                                $result .= '"';
+                                break;
+                            case '\\':
+                                $result .= '\\';
+                                break;
+                            case '/' :
+                                $result .= '/';
+                                break;
+                            case 'b' :
+                                $result .= chr(8);
+                                break;
+                            case 'f' :
+                                $result .= chr(12);
+                                break;
+                            case 'n' :
+                                $result .= chr(10);
+                                break;
+                            case 'r' :
+                                $result .= chr(13);
+                                break;
+                            case 't' :
+                                $result .= chr(9);
+                                break;
+                            case '\'' :
+                                $result .= '\'';
+                                break;
+                            default:
+                                throw new Zend_Json_Exception("Illegal escape "
+                                    .  "sequence '" . $chr . "'");
+                            }
+                    } elseif ($chr == '"') {
+                        break;
+                    } else {
+                        $result .= $chr;
+                    }
+                } while ($i < $str_length);
+
+                $this->_token = self::DATUM;
+                //$this->_tokenValue = substr($str, $start + 1, $i - $start - 1);
+                $this->_tokenValue = $result;
+                break;
+            case  "'":
+                $result = '';
+                do {
+                    $i++;
+                    if ($i >= $str_length) {
+                        break;
+                    }
+
+                    $chr = $str{$i};
+                    if ($chr == '\\') {
+                        $i++;
+                        if ($i >= $str_length) {
+                            break;
+                        }
+                        $chr = $str{$i};
+                        switch ($chr) {
+                            case "'" :
+                                $result .= "'";
+                                break;
+                            case '\\':
+                                $result .= '\\';
+                                break;
+                            case '/' :
+                                $result .= '/';
+                                break;
+                            case 'b' :
+                                $result .= chr(8);
+                                break;
+                            case 'f' :
+                                $result .= chr(12);
+                                break;
+                            case 'n' :
+                                $result .= chr(10);
+                                break;
+                            case 'r' :
+                                $result .= chr(13);
+                                break;
+                            case 't' :
+                                $result .= chr(9);
+                                break;
+                            case '"' :
+                                $result .= '"';
+                                break;
+                            default:
+                                throw new Zend_Json_Exception("Illegal escape "
+                                    .  "sequence '" . $chr . "'");
+                            }
+                    } elseif ($chr == "'") {
+                        break;
+                    } else {
+                        $result .= $chr;
+                    }
+                } while ($i < $str_length);
+
+                $this->_token = self::DATUM;
+                //$this->_tokenValue = substr($str, $start + 1, $i - $start - 1);
+                $this->_tokenValue = $result;
+                break;
+            case 't':
+                if (($i+ 3) < $str_length && substr($str, $start, 4) == "true") {
+                    $this->_token = self::DATUM;
+                }
+                $this->_tokenValue = true;
+                $i += 3;
+                break;
+            case 'f':
+                if (($i+ 4) < $str_length && substr($str, $start, 5) == "false") {
+                    $this->_token = self::DATUM;
+                }
+                $this->_tokenValue = false;
+                $i += 4;
+                break;
+            case 'n':
+                if (($i+ 3) < $str_length && substr($str, $start, 4) == "null") {
+                    $this->_token = self::DATUM;
+                }
+                $this->_tokenValue = NULL;
+                $i += 3;
+                break;
+              case ' ':
+                break;
+        }
+
+        if ($this->_token != self::EOF) {
+            $this->_offset = $i + 1; // Consume the last token character
+            return($this->_token);
+        }
+
+        $chr = $str{$i};
+        if ($chr == '-' || $chr == '.' || ($chr >= '0' && $chr <= '9')) {
+            if (preg_match('/-?([0-9])*(\.[0-9]*)?((e|E)((-|\+)?)[0-9]+)?/s',
+                $str, $matches, PREG_OFFSET_CAPTURE, $start) && $matches[0][1] == $start) {
+
+                $datum = $matches[0][0];
+
+                if (is_numeric($datum)) {
+                    if (preg_match('/^0\d+$/', $datum)) {
+                        throw new Zend_Json_Exception("Octal notation not supported by JSON (value: $datum)");
+                    } else {
+                        $val  = intval($datum);
+                        $fVal = floatval($datum);
+                        $this->_tokenValue = ($val == $fVal ? $val : $fVal);
+                    }
+                } else {
+                    throw new Zend_Json_Exception("Illegal number format: $datum");
+                }
+
+                $this->_token = self::DATUM;
+                $this->_offset = $start + strlen($datum);
+            }
+        } else {
+            throw new Zend_Json_Exception("Illegal Token at pos $i: $chr\nContext:\n--------------------------------------------------" . substr($str, $i) . "\n--------------------------------------------------");
+        }
+
+        return($this->_token);
+    }
+}
+
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Json
+ * @copyright  Copyright (c) 2005-2007 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+
+
+/**
+ * @category   Zend
+ * @package    Zend_Json
+ * @copyright  Copyright (c) 2005-2007 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Json_Exception extends Zend_Exception
+{}
+
+/**
+ * @category   Zend
+ * @package    Zend
+ * @copyright  Copyright (c) 2005-2007 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Exception extends Exception
+{}
+
+/**
+ * Class for encoding to and decoding from JSON.
+ *
+ * @category   Zend
+ * @package    Zend_Json
+ * @copyright  Copyright (c) 2005-2007 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Json
+{
+    /**
+     * How objects should be encoded -- arrays or as StdClass. TYPE_ARRAY is 1
+     * so that it is a boolean true value, allowing it to be used with
+     * ext/json's functions.
+     */
+    const TYPE_ARRAY  = 1;
+    const TYPE_OBJECT = 0;
+
+    /**
+     * @var bool
+     */
+    public static $useBuiltinEncoderDecoder = true;
+
+    /**
+     * Decodes the given $encodedValue string which is
+     * encoded in the JSON format
+     *
+     * Uses ext/json's json_decode if available.
+     *
+     * @param string $encodedValue Encoded in JSON format
+     * @param int $objectDecodeType Optional; flag indicating how to decode
+     * objects. See {@link ZJsonDecoder::decode()} for details.
+     * @return mixed
+     */
+    public static function decode($encodedValue, $objectDecodeType = Zend_Json::TYPE_ARRAY)
+    {
+        if (function_exists('json_decode') && self::$useBuiltinEncoderDecoder !== true) {
+            return json_decode($encodedValue, $objectDecodeType);
+        }
+
+        return Zend_Json_Decoder::decode($encodedValue, $objectDecodeType);
+    }
+
+
+    /**
+     * Encode the mixed $valueToEncode into the JSON format
+     *
+     * Encodes using ext/json's json_encode() if available.
+     *
+     * NOTE: Object should not contain cycles; the JSON format
+     * does not allow object reference.
+     *
+     * NOTE: Only public variables will be encoded
+     *
+     * @param mixed $valueToEncode
+     * @param boolean $cycleCheck Optional; whether or not to check for object recursion; off by default
+     * @return string JSON encoded object
+     */
+    public static function encode($valueToEncode, $cycleCheck = false)
+    {
+        if (function_exists('json_encode') && self::$useBuiltinEncoderDecoder !== true) {
+            return json_encode($valueToEncode);
+        }
+
+        return Zend_Json_Encoder::encode($valueToEncode, $cycleCheck);
+    }
+}
+
+/**
+ * Wrapper for JSON encoding.
+ * @param mixed Variable to encode
+ * @return string JSON-encoded string
+ */
+
+function eb_json_encode($data)
+{
+  return Zend_Json::encode($data, true);
+}
+
+/**
+ * Wrapper for JSON decoding.
+ * @param string JSON-encoded string
+ * @return mixed Decoded value
+ */
+
+function eb_json_decode($data)
+{
+  return Zend_Json::decode($data, Zend_Json::TYPE_ARRAY);
+}
+
+?>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/modules/autoop.php	Mon Aug 25 12:34:26 2008 -0400
@@ -0,0 +1,16 @@
+<?php
+
+eb_hook('event_join', 'autoop_event($chan, $message);');
+
+function autoop_event(&$chan, &$message)
+{
+  global $privileged_list;
+  
+  $channelname = $chan->get_channel_name();
+  
+  // if a known op joins the channel, send mode +o
+  if ( in_array($message['nick'], $privileged_list) )
+  {
+    $chan->parent->put("MODE $channelname +o {$message['nick']}\r\n");
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/modules/doctor.php	Mon Aug 25 12:34:26 2008 -0400
@@ -0,0 +1,41 @@
+<?php
+require('eliza.php');
+
+eb_hook('event_channel_msg', 'doctor_listen($chan, $message);');
+eb_hook('snippet_dynamic', 'if ( $snippet === "doctor" ) return doctor_go($chan, $message, $snippet);');
+eb_hook('event_greeting', 'doctor_greet($append);');
+
+function doctor_go(&$chan, &$message, &$snippet)
+{
+  global $doctor;
+  
+  if ( $snippet == 'doctor' )
+  {
+    if ( isset($doctor[$message['nick']]) )
+    {
+      unset($doctor[$message['nick']]);
+      $chan->msg(eb_censor_words("{$message['nick']}, thank you for visiting the psychotherapist. Come again soon!"), true);
+    }
+    else
+    {
+      $doctor[$message['nick']] = new Psychotherapist();
+      $chan->msg(eb_censor_words("{$message['nick']}, I am the psychotherapist. Please explain your problems to me. When you are finished talking with me, type !doctor again."), true);
+    }
+    return true;
+  }
+}
+
+function doctor_listen(&$chan, &$message)
+{
+  global $doctor;
+  
+  if ( isset($doctor[$message['nick']]) && $message['message'] != '!doctor' )
+  {
+    $chan->msg(eb_censor_words($doctor[$message['nick']]->listen($message['message'])));
+  }
+}
+
+function doctor_greet(&$append)
+{
+  $append .= ' Type !doctor for the psychotherapist.';
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/modules/echo.php	Mon Aug 25 12:34:26 2008 -0400
@@ -0,0 +1,29 @@
+<?php
+
+eb_hook('event_channel_msg', 'echo_event_channel_msg($chan, $message);');
+eb_hook('event_privmsg', 'echo_event_privmsg($message);');
+
+function echo_event_channel_msg(&$chan, &$message)
+{
+  global $privileged_list;
+  
+  if ( preg_match('/^\!echo /', $message['message']) && in_array($message['nick'], $privileged_list) )
+  {
+    $chan->msg(eb_censor_words(preg_replace('/^\!echo /', '', $message['message'])), true);
+  }
+}
+
+function echo_event_privmsg($message)
+{
+  global $privileged_list;
+  
+  if ( in_array($message['nick'], $privileged_list) && preg_match("/^\!echo-([^\007, \r\n\a\t]+) /", $message['message'], $match) )
+  {
+    global $libirc_channels;
+    $channel_name =& $match[1];
+    if ( isset($libirc_channels[$channel_name]) && is_object($libirc_channels[$channel_name]) )
+    {
+      $libirc_channels[$channel_name]->msg(eb_censor_words(preg_replace("/^\!echo-([^\007, \r\n\a\t]+) /", '', $message['message'])), true);
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/modules/greeting.php	Mon Aug 25 12:34:26 2008 -0400
@@ -0,0 +1,32 @@
+<?php
+
+eb_hook('event_raw_message', 'greeting_event($chan, $message);');
+
+function greeting_event(&$chan, &$message)
+{
+  static $part_list = array();
+  
+  switch($message['action'])
+  {
+    case 'JOIN':
+      $wb = false;
+      if ( isset($part_list[$message['nick']]) )
+      {
+        if ( $part_list[$message['nick']] + 1800 >= time() )
+        {
+          $chan->msg("Welcome back.");
+          $wb = true;
+        }
+      }
+      if ( !$wb )
+      {
+        $append = '';
+        eval(eb_fetch_hook('event_greeting'));
+        $chan->msg(eb_censor_words("Hi, {$message['nick']}.$append"));
+      }
+      break;
+    case 'PART':
+      $part_list[$message['nick']] = time();
+      break;
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/modules/log.php	Mon Aug 25 12:34:26 2008 -0400
@@ -0,0 +1,48 @@
+<?php
+
+eb_hook('event_raw_message', 'enanobot_log_message($chan, $message);');
+
+function enanobot_log_message(&$chan, &$message)
+{
+  global $nick;
+  
+  // Log the message
+  $chan_db = mysql_real_escape_string($chan->get_channel_name());
+  $nick_db = mysql_real_escape_string($message['nick']);
+  $line_db = mysql_real_escape_string($message['message']);
+  $day     = date('Y-m-d');
+  $time    = time();
+  $m_et = false;
+  $sql = false;
+  switch($message['action'])
+  {
+    case 'PRIVMSG':
+      if ( substr($line_db, 0, 5) != '[off]' )
+      {
+        $sql = "INSERT INTO irclog(channel, day, nick, timestamp, line) VALUES
+                  ( '$chan_db', '$day', '$nick_db', '$time', '$line_db' );";
+      }
+      break;
+    case 'JOIN':
+      $sql = "INSERT INTO irclog(channel, day, nick, timestamp, line) VALUES
+                ( '$chan_db', '$day', '', '$time', '$nick_db has joined $chan_db' );";
+      break;
+    case 'PART':
+      $sql = "INSERT INTO irclog(channel, day, nick, timestamp, line) VALUES
+                ( '$chan_db', '$day', '', '$time', '$nick_db has left $chan_db' );";
+      break;
+    case 'MODE':
+      list($mode, $target_nick) = explode(' ', $line_db);
+      if ( $message['nick'] != 'ChanServ' && $target_nick != $nick )
+      {
+        $sql = "INSERT INTO irclog(channel, day, nick, timestamp, line) VALUES
+                  ( '$chan_db', '$day', '', '$time', '$nick_db set mode $mode on $target_nick' );";
+      }
+      break;
+  }
+  if ( $sql )
+  {
+    eb_mysql_query($sql);
+  }
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/modules/snippets.php	Mon Aug 25 12:34:26 2008 -0400
@@ -0,0 +1,67 @@
+<?php
+
+eb_hook('event_channel_msg', 'snippets_event_privmsg($chan, $message);');
+
+function snippets_event_privmsg(&$chan, &$message)
+{
+  if ( preg_match('/^\![\s]*([a-z0-9_-]+)([\s]*\|[\s]*([^ ]+))?$/', $message['message'], $match) )
+  {
+    $snippet =& $match[1];
+    if ( @$match[3] === 'me' )
+      $match[3] = $message['nick'];
+    $target_nick = ( !empty($match[3]) ) ? "{$match[3]}, " : "{$message['nick']}, ";
+    if ( $snippet == 'snippets' )
+    {
+      // list available snippets
+      $m_et = false;
+      $q = eb_mysql_query('SELECT snippet_code, snippet_channels FROM snippets;');
+      if ( mysql_num_rows($q) < 1 )
+      {
+        $chan->msg(eb_censor_words("{$message['nick']}, I couldn't find that snippet (\"$snippet\") in the database."), true);
+      }
+      else
+      {
+        $snippets = array();
+        while ( $row = mysql_fetch_assoc($q) )
+        {
+          $channels = explode('|', $row['snippet_channels']);
+          if ( in_array($chan->get_channel_name(), $channels) )
+          {
+            $snippets[] = $row['snippet_code'];
+          }
+        }
+        $snippets = implode(', ', $snippets);
+        $chan->msg(eb_censor_words("{$message['nick']}, the following snippets are available: $snippets"), true);
+      }
+      @mysql_free_result($q);
+    }
+    else
+    {
+      if ( eval(eb_fetch_hook('snippet_dynamic')) )
+      {
+        return true;
+      }
+      
+      // Look for the snippet...
+      $q = eb_mysql_query('SELECT snippet_text, snippet_channels FROM snippets WHERE snippet_code = \'' . mysql_real_escape_string($snippet) . '\';');
+      if ( mysql_num_rows($q) < 1 )
+      {
+        $chan->msg(eb_censor_words("{$message['nick']}, I couldn't find that snippet (\"$snippet\") in the database."), true);
+      }
+      else
+      {
+        $row = mysql_fetch_assoc($q);
+        $channels = explode('|', $row['snippet_channels']);
+        if ( in_array($chan->get_channel_name(), $channels) )
+        {
+          $chan->msg(eb_censor_words("{$target_nick}{$row['snippet_text']}"), true);
+        }
+        else
+        {
+          $chan->msg(eb_censor_words("{$message['nick']}, I couldn't find that snippet (\"$snippet\") in the database."), true);
+        }
+      }
+      @mysql_free_result($q);
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/modules/stats.php	Mon Aug 25 12:34:26 2008 -0400
@@ -0,0 +1,259 @@
+<?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());
+@include('./stats-data.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;
+}
+
+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;
+  ob_start();
+  var_export($stats_data);
+  $stats_data_exported = ob_get_contents();
+  ob_end_clean();
+  
+  $fp = @fopen('./stats-data.php', 'w');
+  if ( !$fp )
+    return false;
+  fwrite($fp, "<?php\n\$stats_data = $stats_data_exported;\n");
+  fclose($fp);
+}
+
--- a/snippets.php	Fri May 09 22:37:57 2008 -0400
+++ b/snippets.php	Mon Aug 25 12:34:26 2008 -0400
@@ -11,9 +11,10 @@
  * This script MUST be placed in an Enano installation directory.
  *****************************************************************/
 
-define('ENANOBOT_ROOT', './');
+define('ENANOBOT_ROOT', dirname(__FILE__) . '/');
 
 // load Enano for auth
+/*
 require('includes/common.php');
 if ( $session->user_level < USER_LEVEL_ADMIN )
 {
@@ -22,6 +23,7 @@
 
 $db->close();
 unset($db, $session, $paths, $template, $plugins);
+*/
 
 // We're authed.
 // Load config
@@ -63,7 +65,7 @@
   </head>
   <body>
     <h1>EnanoBot snippet management</h1>
-    <form action="enanobot-snippets.php" method="post" enctype="multipart/form-data">
+    <form action="snippets.php" method="post" enctype="multipart/form-data">
       <fieldset>
         <legend>Add a snippet</legend>
         <table border="1" cellspacing="0" cellpadding="4">
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/stats-fe.php	Mon Aug 25 12:34:26 2008 -0400
@@ -0,0 +1,84 @@
+<?php
+
+/**
+ * Frontend for statistics data. Handles fetching and calculating data from raw statistics stored in stats-data.php.
+ * @package EnanoBot
+ * @subpackage stats
+ * @author Dan Fuhry <dan@enanocms.org>
+ */
+
+if ( !isset($GLOBALS['stats_data']) )
+{
+  require(dirname(__FILE__) . '/stats-data.php');
+  $data =& $stats_data;
+}
+
+define('NOW', time());
+
+/**
+ * Gets the number of messages posted in IRC in the last X minutes.
+ * @param string Channel
+ * @param int Optional - time period for message count. Defaults to 10 minutes.
+ * @param int Optional - Base time, defaults to right now
+ * @return int
+ */
+
+function stats_message_count($channel, $mins = 10, $base = NOW)
+{
+  global $data;
+  
+  $time_min = $base - ( $mins * 60 );
+  $time_max = $base;
+  
+  if ( !isset($data['messages'][$channel]) )
+  {
+    return 0;
+  }
+  
+  $count = 0;
+  foreach ( $data['messages'][$channel] as $message )
+  {
+    if ( $message['time'] >= $time_min && $message['time'] <= $time_max )
+    {
+      $count++;
+    }
+  }
+  
+  return $count;
+}
+
+/**
+ * Gets the percentages as to who's posted the most messages in the last X minutes.
+ * @param string Channel name
+ * @param int Optional - How many minutes, defaults to 10
+ * @param int Optional - Base time, defaults to right now
+ * @return array Associative, with floats.
+ */
+
+function stats_activity_percent($channel, $mins = 10, $base = NOW)
+{
+  global $data;
+  if ( !($total = stats_message_count($channel, $mins, $base)) )
+  {
+    return array();
+  }
+  $results = array();
+  $usercounts = array();
+  $time_min = $base - ( $mins * 60 );
+  $time_max = $base;
+  foreach ( $data['messages'][$channel] as $message )
+  {
+    if ( $message['time'] >= $time_min && $message['time'] <= $time_max )
+    {
+      if ( !isset($usercounts[$message['nick']]) )
+        $usercounts[$message['nick']] = 0;
+      $usercounts[$message['nick']]++;
+    }
+  }
+  foreach ( $usercounts as $nick => $count )
+  {
+    $results[$nick] = $count / $total;
+  }
+  arsort($results);
+  return $results;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/timezone.php	Mon Aug 25 12:34:26 2008 -0400
@@ -0,0 +1,510 @@
+<?php
+
+define('COOKIE_NAME', 'db_stats_tz');
+
+$tz = ( isset($_COOKIE[COOKIE_NAME]) ) ? $_COOKIE[COOKIE_NAME] : 'UTC';
+$zones = get_raw_timezone_list();
+if ( in_array($tz, $zones) )
+{
+  date_default_timezone_set($tz);
+}
+
+/**
+ * List of standard time zones.
+ * @return array
+ */
+
+function get_timezone_list()
+{
+  static $zones = false;
+  if ( !$zones )
+  {
+    $zonelist = get_raw_timezone_list();
+    $zones = array();
+    foreach ( $zonelist as $rawzone )
+    {
+      @list($region, $area, $city) = explode('/', $rawzone);
+      if ( empty($area) )
+      {
+        $zones[$region] = $region;
+        continue;
+      }
+      if ( !isset($zones[$region]) )
+        $zones[$region] = array();
+      
+      if ( empty($city) )
+      {
+        $zones[$region][] = $area;
+      }
+      else
+      {
+        if ( !isset($zones[$region][$area]) || ( isset($zones[$region][$area]) && !is_array($zones[$region][$area]) ) )
+        {
+          $zones[$region][$area] = array();
+        }
+        $zones[$region][$area][] = $city;
+      }
+    }
+  }
+  return $zones;
+}
+
+/**
+ * Get the raw, flat array of time zones.
+ * @return array
+ */
+
+function get_raw_timezone_list()
+{
+  return array(
+    'UTC',
+    'Africa/Abidjan',
+    'Africa/Accra',
+    'Africa/Addis_Ababa',
+    'Africa/Algiers',
+    'Africa/Asmara',
+    'Africa/Asmera',
+    'Africa/Bamako',
+    'Africa/Bangui',
+    'Africa/Banjul',
+    'Africa/Bissau',
+    'Africa/Blantyre',
+    'Africa/Brazzaville',
+    'Africa/Bujumbura',
+    'Africa/Cairo',
+    'Africa/Casablanca',
+    'Africa/Ceuta',
+    'Africa/Conakry',
+    'Africa/Dakar',
+    'Africa/Dar_es_Salaam',
+    'Africa/Djibouti',
+    'Africa/Douala',
+    'Africa/El_Aaiun',
+    'Africa/Freetown',
+    'Africa/Gaborone',
+    'Africa/Harare',
+    'Africa/Johannesburg',
+    'Africa/Kampala',
+    'Africa/Khartoum',
+    'Africa/Kigali',
+    'Africa/Kinshasa',
+    'Africa/Lagos',
+    'Africa/Libreville',
+    'Africa/Lome',
+    'Africa/Luanda',
+    'Africa/Lubumbashi',
+    'Africa/Lusaka',
+    'Africa/Malabo',
+    'Africa/Maputo',
+    'Africa/Maseru',
+    'Africa/Mbabane',
+    'Africa/Mogadishu',
+    'Africa/Monrovia',
+    'Africa/Nairobi',
+    'Africa/Ndjamena',
+    'Africa/Niamey',
+    'Africa/Nouakchott',
+    'Africa/Ouagadougou',
+    'Africa/Porto-Novo',
+    'Africa/Sao_Tome',
+    'Africa/Timbuktu',
+    'Africa/Tripoli',
+    'Africa/Tunis',
+    'Africa/Windhoek',
+    'America/Adak',
+    'America/Anchorage',
+    'America/Anguilla',
+    'America/Antigua',
+    'America/Araguaina',
+    'America/Argentina/Buenos_Aires',
+    'America/Argentina/Catamarca',
+    'America/Argentina/ComodRivadavia',
+    'America/Argentina/Cordoba',
+    'America/Argentina/Jujuy',
+    'America/Argentina/La_Rioja',
+    'America/Argentina/Mendoza',
+    'America/Argentina/Rio_Gallegos',
+    'America/Argentina/San_Juan',
+    'America/Argentina/San_Luis',
+    'America/Argentina/Tucuman',
+    'America/Argentina/Ushuaia',
+    'America/Aruba',
+    'America/Asuncion',
+    'America/Atikokan',
+    'America/Atka',
+    'America/Bahia',
+    'America/Barbados',
+    'America/Belem',
+    'America/Belize',
+    'America/Blanc-Sablon',
+    'America/Boa_Vista',
+    'America/Bogota',
+    'America/Boise',
+    'America/Buenos_Aires',
+    'America/Cambridge_Bay',
+    'America/Campo_Grande',
+    'America/Cancun',
+    'America/Caracas',
+    'America/Catamarca',
+    'America/Cayenne',
+    'America/Cayman',
+    'America/Chicago',
+    'America/Chihuahua',
+    'America/Coral_Harbour',
+    'America/Cordoba',
+    'America/Costa_Rica',
+    'America/Cuiaba',
+    'America/Curacao',
+    'America/Danmarkshavn',
+    'America/Dawson',
+    'America/Dawson_Creek',
+    'America/Denver',
+    'America/Detroit',
+    'America/Dominica',
+    'America/Edmonton',
+    'America/Eirunepe',
+    'America/El_Salvador',
+    'America/Ensenada',
+    'America/Fort_Wayne',
+    'America/Fortaleza',
+    'America/Glace_Bay',
+    'America/Godthab',
+    'America/Goose_Bay',
+    'America/Grand_Turk',
+    'America/Grenada',
+    'America/Guadeloupe',
+    'America/Guatemala',
+    'America/Guayaquil',
+    'America/Guyana',
+    'America/Halifax',
+    'America/Havana',
+    'America/Hermosillo',
+    'America/Indiana/Indianapolis',
+    'America/Indiana/Knox',
+    'America/Indiana/Marengo',
+    'America/Indiana/Petersburg',
+    'America/Indiana/Tell_City',
+    'America/Indiana/Vevay',
+    'America/Indiana/Vincennes',
+    'America/Indiana/Winamac',
+    'America/Indianapolis',
+    'America/Inuvik',
+    'America/Iqaluit',
+    'America/Jamaica',
+    'America/Jujuy',
+    'America/Juneau',
+    'America/Kentucky/Louisville',
+    'America/Kentucky/Monticello',
+    'America/Knox_IN',
+    'America/La_Paz',
+    'America/Lima',
+    'America/Los_Angeles',
+    'America/Louisville',
+    'America/Maceio',
+    'America/Managua',
+    'America/Manaus',
+    'America/Marigot',
+    'America/Martinique',
+    'America/Mazatlan',
+    'America/Mendoza',
+    'America/Menominee',
+    'America/Merida',
+    'America/Mexico_City',
+    'America/Miquelon',
+    'America/Moncton',
+    'America/Monterrey',
+    'America/Montevideo',
+    'America/Montreal',
+    'America/Montserrat',
+    'America/Nassau',
+    'America/New_York',
+    'America/Nipigon',
+    'America/Nome',
+    'America/Noronha',
+    'America/North_Dakota/Center',
+    'America/North_Dakota/New_Salem',
+    'America/Panama',
+    'America/Pangnirtung',
+    'America/Paramaribo',
+    'America/Phoenix',
+    'America/Port-au-Prince',
+    'America/Port_of_Spain',
+    'America/Porto_Acre',
+    'America/Porto_Velho',
+    'America/Puerto_Rico',
+    'America/Rainy_River',
+    'America/Rankin_Inlet',
+    'America/Recife',
+    'America/Regina',
+    'America/Resolute',
+    'America/Rio_Branco',
+    'America/Rosario',
+    'America/Santiago',
+    'America/Santo_Domingo',
+    'America/Sao_Paulo',
+    'America/Scoresbysund',
+    'America/Shiprock',
+    'America/St_Barthelemy',
+    'America/St_Johns',
+    'America/St_Kitts',
+    'America/St_Lucia',
+    'America/St_Thomas',
+    'America/St_Vincent',
+    'America/Swift_Current',
+    'America/Tegucigalpa',
+    'America/Thule',
+    'America/Thunder_Bay',
+    'America/Tijuana',
+    'America/Toronto',
+    'America/Tortola',
+    'America/Vancouver',
+    'America/Virgin',
+    'America/Whitehorse',
+    'America/Winnipeg',
+    'America/Yakutat',
+    'America/Yellowknife',
+    'Antarctica/Casey',
+    'Antarctica/Davis',
+    'Antarctica/DumontDUrville',
+    'Antarctica/Mawson',
+    'Antarctica/McMurdo',
+    'Antarctica/Palmer',
+    'Antarctica/Rothera',
+    'Antarctica/South_Pole',
+    'Antarctica/Syowa',
+    'Antarctica/Vostok',
+    'Arctic/Longyearbyen',
+    'Asia/Aden',
+    'Asia/Almaty',
+    'Asia/Amman',
+    'Asia/Anadyr',
+    'Asia/Aqtau',
+    'Asia/Aqtobe',
+    'Asia/Ashgabat',
+    'Asia/Ashkhabad',
+    'Asia/Baghdad',
+    'Asia/Bahrain',
+    'Asia/Baku',
+    'Asia/Bangkok',
+    'Asia/Beirut',
+    'Asia/Bishkek',
+    'Asia/Brunei',
+    'Asia/Calcutta',
+    'Asia/Choibalsan',
+    'Asia/Chongqing',
+    'Asia/Chungking',
+    'Asia/Colombo',
+    'Asia/Dacca',
+    'Asia/Damascus',
+    'Asia/Dhaka',
+    'Asia/Dili',
+    'Asia/Dubai',
+    'Asia/Dushanbe',
+    'Asia/Gaza',
+    'Asia/Harbin',
+    'Asia/Ho_Chi_Minh',
+    'Asia/Hong_Kong',
+    'Asia/Hovd',
+    'Asia/Irkutsk',
+    'Asia/Istanbul',
+    'Asia/Jakarta',
+    'Asia/Jayapura',
+    'Asia/Jerusalem',
+    'Asia/Kabul',
+    'Asia/Kamchatka',
+    'Asia/Karachi',
+    'Asia/Kashgar',
+    'Asia/Katmandu',
+    'Asia/Kolkata',
+    'Asia/Krasnoyarsk',
+    'Asia/Kuala_Lumpur',
+    'Asia/Kuching',
+    'Asia/Kuwait',
+    'Asia/Macao',
+    'Asia/Macau',
+    'Asia/Magadan',
+    'Asia/Makassar',
+    'Asia/Manila',
+    'Asia/Muscat',
+    'Asia/Nicosia',
+    'Asia/Novosibirsk',
+    'Asia/Omsk',
+    'Asia/Oral',
+    'Asia/Phnom_Penh',
+    'Asia/Pontianak',
+    'Asia/Pyongyang',
+    'Asia/Qatar',
+    'Asia/Qyzylorda',
+    'Asia/Rangoon',
+    'Asia/Riyadh',
+    'Asia/Saigon',
+    'Asia/Sakhalin',
+    'Asia/Samarkand',
+    'Asia/Seoul',
+    'Asia/Shanghai',
+    'Asia/Singapore',
+    'Asia/Taipei',
+    'Asia/Tashkent',
+    'Asia/Tbilisi',
+    'Asia/Tehran',
+    'Asia/Tel_Aviv',
+    'Asia/Thimbu',
+    'Asia/Thimphu',
+    'Asia/Tokyo',
+    'Asia/Ujung_Pandang',
+    'Asia/Ulaanbaatar',
+    'Asia/Ulan_Bator',
+    'Asia/Urumqi',
+    'Asia/Vientiane',
+    'Asia/Vladivostok',
+    'Asia/Yakutsk',
+    'Asia/Yekaterinburg',
+    'Asia/Yerevan',
+    'Atlantic/Azores',
+    'Atlantic/Bermuda',
+    'Atlantic/Canary',
+    'Atlantic/Cape_Verde',
+    'Atlantic/Faeroe',
+    'Atlantic/Faroe',
+    'Atlantic/Jan_Mayen',
+    'Atlantic/Madeira',
+    'Atlantic/Reykjavik',
+    'Atlantic/South_Georgia',
+    'Atlantic/St_Helena',
+    'Atlantic/Stanley',
+    'Australia/ACT',
+    'Australia/Adelaide',
+    'Australia/Brisbane',
+    'Australia/Broken_Hill',
+    'Australia/Canberra',
+    'Australia/Currie',
+    'Australia/Darwin',
+    'Australia/Eucla',
+    'Australia/Hobart',
+    'Australia/LHI',
+    'Australia/Lindeman',
+    'Australia/Lord_Howe',
+    'Australia/Melbourne',
+    'Australia/North',
+    'Australia/NSW',
+    'Australia/Perth',
+    'Australia/Queensland',
+    'Australia/South',
+    'Australia/Sydney',
+    'Australia/Tasmania',
+    'Australia/Victoria',
+    'Australia/West',
+    'Australia/Yancowinna',
+    'Europe/Amsterdam',
+    'Europe/Andorra',
+    'Europe/Athens',
+    'Europe/Belfast',
+    'Europe/Belgrade',
+    'Europe/Berlin',
+    'Europe/Bratislava',
+    'Europe/Brussels',
+    'Europe/Bucharest',
+    'Europe/Budapest',
+    'Europe/Chisinau',
+    'Europe/Copenhagen',
+    'Europe/Dublin',
+    'Europe/Gibraltar',
+    'Europe/Guernsey',
+    'Europe/Helsinki',
+    'Europe/Isle_of_Man',
+    'Europe/Istanbul',
+    'Europe/Jersey',
+    'Europe/Kaliningrad',
+    'Europe/Kiev',
+    'Europe/Lisbon',
+    'Europe/Ljubljana',
+    'Europe/London',
+    'Europe/Luxembourg',
+    'Europe/Madrid',
+    'Europe/Malta',
+    'Europe/Mariehamn',
+    'Europe/Minsk',
+    'Europe/Monaco',
+    'Europe/Moscow',
+    'Europe/Nicosia',
+    'Europe/Oslo',
+    'Europe/Paris',
+    'Europe/Podgorica',
+    'Europe/Prague',
+    'Europe/Riga',
+    'Europe/Rome',
+    'Europe/Samara',
+    'Europe/San_Marino',
+    'Europe/Sarajevo',
+    'Europe/Simferopol',
+    'Europe/Skopje',
+    'Europe/Sofia',
+    'Europe/Stockholm',
+    'Europe/Tallinn',
+    'Europe/Tirane',
+    'Europe/Tiraspol',
+    'Europe/Uzhgorod',
+    'Europe/Vaduz',
+    'Europe/Vatican',
+    'Europe/Vienna',
+    'Europe/Vilnius',
+    'Europe/Volgograd',
+    'Europe/Warsaw',
+    'Europe/Zagreb',
+    'Europe/Zaporozhye',
+    'Europe/Zurich',
+    'Indian/Antananarivo',
+    'Indian/Chagos',
+    'Indian/Christmas',
+    'Indian/Cocos',
+    'Indian/Comoro',
+    'Indian/Kerguelen',
+    'Indian/Mahe',
+    'Indian/Maldives',
+    'Indian/Mauritius',
+    'Indian/Mayotte',
+    'Indian/Reunion',
+    'Pacific/Apia',
+    'Pacific/Auckland',
+    'Pacific/Chatham',
+    'Pacific/Easter',
+    'Pacific/Efate',
+    'Pacific/Enderbury',
+    'Pacific/Fakaofo',
+    'Pacific/Fiji',
+    'Pacific/Funafuti',
+    'Pacific/Galapagos',
+    'Pacific/Gambier',
+    'Pacific/Guadalcanal',
+    'Pacific/Guam',
+    'Pacific/Honolulu',
+    'Pacific/Johnston',
+    'Pacific/Kiritimati',
+    'Pacific/Kosrae',
+    'Pacific/Kwajalein',
+    'Pacific/Majuro',
+    'Pacific/Marquesas',
+    'Pacific/Midway',
+    'Pacific/Nauru',
+    'Pacific/Niue',
+    'Pacific/Norfolk',
+    'Pacific/Noumea',
+    'Pacific/Pago_Pago',
+    'Pacific/Palau',
+    'Pacific/Pitcairn',
+    'Pacific/Ponape',
+    'Pacific/Port_Moresby',
+    'Pacific/Rarotonga',
+    'Pacific/Saipan',
+    'Pacific/Samoa',
+    'Pacific/Tahiti',
+    'Pacific/Tarawa',
+    'Pacific/Tongatapu',
+    'Pacific/Truk',
+    'Pacific/Wake',
+    'Pacific/Wallis',
+    'Pacific/Yap'
+  );
+}
+
+?>