Login   Register  
PHP Classes
elePHPant
Icontem

File: joker.class.php

Recommend this page to a friend!
Stumble It! Stumble It! Bookmark in del.icio.us Bookmark in del.icio.us
  Classes of Sergei Miami  >  Joker the IRC bot  >  joker.class.php  >  Download  
File: joker.class.php
Role: Class source
Content type: text/plain
Description: Joker class (base)
Class: Joker the IRC bot
Base for building IRC bot
Author: By
Last change: Added Joker instance into the plugin initialisation (YourPlugin::__construct(Joker $joker) ). Testing new parser (commented in Joker::main() )
Date: 4 years ago
Size: 19,393 bytes
 

Contents

Class file image Download
<?php

/**
 * @package    Joker
 * @subpackage Classes
 * @author     Miami <miami@blackcrystal.net>
 * @version    1.1 on 26/05/2010
 * @license    Released under the MIT License
 * @link       www.blackcrystal.net
 *
 * Joker the IRC bot
 *
 * This is main class - a base for bot. It contains methods, that can be executed
 * from your plugins. Bot has built-in flood protection, timers and powerful
 * plugin system, that allows you to reload plugin classes on-fly without restart.
 */

class Joker {
  
  // Log level: false - no output, true - full output, array(..) - selected types:
  // 'e' - events, 'p' - parser, 'i' - incoming, 
  // 'o' - outgoing, '+' - information, 'p' - plugin
  public $loglevel = array('i','o');

  public $plugins = array();       // hold plugins
  public $socket  = null;          // hold socket
  public $buffer  = array();       // hold outgoing buffer
  public $flood   = null;          // hold antiflood timer

  public $me       = null;
  public $altnicks = array();
  public $autojoin = array();
  public $adminss  = array();


  public $server,$port,
         $event,$addr,$nick,$user, // event  information
         $host,$chan,$raw,$param;  // event  information

  const STOP = 'stop';             // send this signal to stop plugins loop

  /**
   * Add and process event, by calling active plugins
   * that have desired method.
   * @param string $event
   */
  public function event($event=null)
  {
    if (!is_null($event)) $this->event = $event;

    // do nog log timers, cuz it's too much messages
    if ($this->event != 'TIMER')
      @$this->log('e', "M:$this->me E:$this->event N:$this->nick C:$this->chan T:$this->text") ;

    //now we run method on all plugins that has is
    foreach ($this->plugins as $pluginName => $instance)
    {
      // skip, if plugin is removed by another plugin
      if (!isset($this->plugins[$pluginName])) continue;
      
      // run method on plugin
      $eventName = $this->event;
      if (method_exists($instance,$this->event))
      {
        $result = $instance->$eventName($this);
        // stop processing on special signal
        if ($result === self::STOP) break;
      }      
    }
    $this->clearEvent();
  }

  /**
   * Clear current event and all related info
   */
  public function clearEvent() 
  {
  	$this->addr  = $this->nick  = $this->user  = $this->host  = 
  	$this->chan  = $this->event = $this->text = $this->raw  = '';
  	$this->param = array();
  }

  /**
   * Load plugin
   * @param string $name 
   */
  public function load($name)
  {

    $name = strtolower($name);
    $filename = dirname(__FILE__) . "/plugins/$name.class.php";
    if (!file_exists($filename))
    {
      $this->log('p', "$filename is not exists");
      return "$filename is not exists";
    }
    $error = trim(`php -l $filename`);
    if (stripos($error, 'No syntax errors detected') === FALSE )
    {
      $this->log('p', $error);
      return $error;
    }
    $file = file_get_contents($filename);
    $rev  = 'plugin'.uniqid();
    //set fake classname (one replace only)
    $file = preg_replace("@class\s+(\w+)@i", "class {$rev}", $file,1);
    $this->log('p', "Loading $name as $rev from $filename");
    eval('?>'.$file );
    $this->plugins[$name] = new $rev($this);
    return "$name loaded from $filename as $rev";
  }

  /**
   * Unload plugin
   * @param string $name 
   */
  public function unload($name)
  {
    $name = strtolower($name);
    unset($this->plugins[$name]);
    $this->log('p', "$name unloaded");
    return "$name unloaded";
    
  }
  
  /**
   * Main loop
   */
  private function main()
  {
    // this is infinitive loop, that reads incoming
    // messages, sends outgoing and runs timers
    while (!feof($this->socket))
    { 
      $this->clearEvent(); //clear event
      $this->raw = trim(fgets($this->socket, 2048));             //read incoming raw
      if ($this->raw != '') {
        $this->log('i', $this->raw); 
  	    $this->parse(); //parse raw
  	    // $this->parseNew(); //test new parser
      }
      if ($this->event) $this->event();                          //run event if exisis
      if (count($this->buffer)>0 && time() > $this->flood) // check flood protection and message in buffer
              $this->send(array_shift($this->buffer));           //send from buffer
      $this->event('TIMER');                                     //process timer
      usleep(100);

    }
  }

  /**
   * Log messages to console
   * @param string $type 'e' - events, 'p' - parser, 'i' - incoming, 'o' - outgoing, '+' - information, 'p' - plugin
   * @param string $text
   */
  public function log($type='+', $text='')
  {
    if ($this->loglevel === false) return;
    if ( ($this->loglevel === true) ||
            ( is_array( $this->loglevel ) && in_array( $type, $this->loglevel) ) )
      echo "\n[".date('H:i:s')."] $type $text";
  }

  /**
   * this method you can use for sending commands
   * it will be sent to server using antiflood system
   * @param string $raw
   */
  public function queue($raw) 
  {
    $this->buffer[] = $raw;
  }

  /**
   * Send command to server
   * please avoid sending something by this method directly, cuz this will not prevent
   * excess flood by your bot. You can use this directly ONLY for PING reply
   * otherwise, use $this->queue
   * @param string $raw
   */
  public function send($raw)
  {
    $this->log('o', $raw);
    fwrite($this->socket, "$raw\r\n"); //send command
    $this->flood = time()+1;     //set antiflood timer to current time+ 1
  }

  /**
   * Incoming IRC commands parser
   */
  private function parse()
  {
    
    $matches = array();
    
  	// :nick!user@host PRIVMSG #chan :text
  	if (    preg_match('/^:(\S*) (\S*) (#\S*) :(.*)$/Ui',$this->raw,$matches))
  	list(,$this->addr,$this->event,$this->chan, $this->text) = $matches;
  	 
  	// :nick!user@host PRIVMSG #chan :\001ACTION text\001
  	if (    preg_match('/^:(\S*) PRIVMSG (#\S*) :\001(ACTION) (.*)\001$/Ui',$this->raw,$matches))
  	list(,$this->addr,$this->chan, $this->event, $this->text) = $matches;
  	
  	// :nick!user@host JOIN #chan
  	elseif (preg_match('/^:(\S*) (\S*) (#\S*)$/Ui',$this->raw,$matches))
  	list(,$this->addr,$this->event,$this->chan) = $matches;
  	 
  	// :server 376 BC^j0k3r :End of /MOTD command.
  	elseif (preg_match('/^:(\S*) (\S*) \S* :(.*)$/Ui',$this->raw,$matches))
  	list(,$this->addr,$this->event,$this->text) = $matches;

  	// :server 254 me 88735 :channels formed
  	elseif (preg_match('/^:(\S*) (\S*) \S* (\S* :.*)$/Ui',$this->raw,$matches))
  	list(,$this->addr,$this->event,$this->text) = $matches;
  		
  	// :wserver 433 * newnick :Nickname is already in use.
  	elseif (preg_match('/^:(\S*) (\S*) \* \S* :(.*)$/Ui',$this->raw,$matches))
  	list(,$this->addr,$this->event,$this->text) = $matches;
  		
  	// :server 366 me #bctest :End of /NAMES list.
  	elseif (preg_match('/^:(\S*) (\S*) \S* (#\S*) :(.*)$/Ui',$this->raw,$matches))
  	list(,$this->addr,$this->event,$this->chan, $this->text) = $matches;

  	// :nick!user@host NICK :newnick
  	elseif (preg_match('/^:(\S*) (\S*) :(.*)$/Ui',$this->raw,$matches))
  	list(,$this->addr,$this->event, $this->text) = $matches;
  	  	
  	// PING :text
  	elseif (preg_match('/^([^:]*) :(.*)$/Ui',$this->raw,$matches))
  	list(,$this->event,$this->text) = $matches;

  	// :server 005 me WHOX WALLCH....
  	elseif (preg_match('/^:(\S*) (\S*) \S* (.*)$/Ui',$this->raw,$matches))
  	list(,$this->addr,$this->event,$this->text) = $matches;
  	 
  	else {
  	  //else output a message and stop processing
  	  $this->log('e',"No matches for '$this->raw'");
  	  return;
  	}
  	
  	$this->event = $this->eventName($this->event); //convert numeric event to string
  	@list($this->nick,$this->user,$this->host) = @explode('@',str_replace('!','@',$this->addr)); //get nick|user|host from addr
  	$this->text  = trim($this->text); //trim text
  	$this->param  = preg_split('|\s+|',$this->text); //make param array
  }

  /**
   * New parser of IRC protocol
   */
  public function parseNew()
  {
    $preg = '^(?P<nap>NOTICE AUTH|PING)?' .   // notice auth or ping command are some awful shit,
                                       // dunno why they have another syntax then other commands
                                       // anyways, if these commands fired, subpattern will not fire
            '(?:^:' .                  // start of subpattern. It starts if this is beginning of line and :
              '(?P<addr>(?P<nick>.*)!(?P<user>.*)@(?P<host>.*)|.*) ' . // address, nick, user, host, or just address (if this is server message)
              '(?P<event>\d* |\S* )' .          // event number or name
              '(?P<param>[^:]*)' .              // all until :
            ')?' .                     // end of subpattern
            '(?P<text>?: :(.*?))?$';           // this fires for NOTICE AUTH and PING too.
                                       // Getting all the text, removing ' :' from start.
                                       // 06 may 2009 02:05am Sergei Miami <miami@blackcrystal.net>
                                       // deep night, but I made it in one line, hooray!

    /*
    Here is the sample result of parsing:
    [0] => full RAW string             :BC^joker!joker@85.14.235.80.sta.estpak.ee MODE #blackcrystal +l 100500
    [1] => NOTICE AUTH or PING command
    [2] => full address or server      BC^joker!joker@85.14.235.80.sta.estpak.ee
    [3] => Nick                        BC^joker
    [4] => Username                    joker
    [5] => Hostname                    85.14.235.80.sta.estpak.ee
    [6] => Event (number or string)    MODE
    [7] => Event params                #blackcrystal +l 100500
    [8] => Text
    Tests on @see tests/parser
    */
    $names = array('RAW', 'NAP', 'ADDR', 'NICK', 'USER', 'HOST', 'EVENT', 'PARAMS', 'TEXT');
    if (preg_match('_'.$preg.'_Ui',$this->raw,$matches)) {
      // $matches = array_combine($names, $matches);
      $matches['preg'] =$preg;
      file_put_contents( dirname(__FILE__).'/data/parser.log', print_r($matches,true) , FILE_APPEND);
    }

  }






  /**
   * Convert numeric commands to readable analog.
   * If no translation for event found, convert to NUMBER_xxx
   * @param string $numeric
   * @return string
   */
  private function eventName($numeric)
  {
    // if event already named, replace spaces with _ and return
    // here we check only first char, cuz method names must start from letter
    if (preg_match('|[A-Z]|',$numeric{0})) return str_replace(' ','_',$numeric);

    // thanks to yapircl for this great RFC-1459 names list
    // @see http://projects.gtk.mine.nu
    // @see http://rfc.sunsite.dk/rfc/rfc1459.html
    static $events = array(
      '001' => 'RPL_WELCOME', '002' => 'RPL_YOURHOST', '003' => 'RPL_CREATED', '004' => 'RPL_MYINFO',
      '005' => 'RPL_ISUPPORT', 221 => 'RPL_UMODEIS', 250 => 'RPL_STATSDLINE', 251 => 'RPL_LUSERCLIENT',
      252 => 'RPL_LUSEROP', 253 => 'RPL_LUSERUNKNOWN', 254 => 'RPL_LUSERCHANNELS', 255 => 'RPL_LUSERME',
      301 => 'RPL_AWAY', 302 => 'RPL_USERHOST', 303 => 'RPL_ISON', 305 => 'RPL_UNAWAY', 306 => 'RPL_NOWAWAY',
      311 => 'RPL_WHOISUSER', 312 => 'RPL_WHOISSERVER', 313 => 'RPL_WHOISOPERATOR', 315 => 'RPL_ENDOFWHO',
      317 => 'RPL_WHOISIDLE', 318 => 'RPL_ENDOFWHOIS', 319 => 'RPL_WHOISCHANNELS', 321 => 'RPL_LISTSTART',
      322 => 'RPL_LIST', 323 => 'RPL_LISTEND', 324 => 'RPL_CHANNELMODEIS', 329 => 'RPL_CREATIONTIME',
      331 => 'RPL_NOTOPIC', 332 => 'RPL_TOPIC', 333 => 'RPL_TOPICWHOTIME', 341 => 'RPL_INVITING',
      351 => 'RPL_VERSION', 352 => 'RPL_WHOREPLY', 353 => 'RPL_NAMREPLY', 366 => 'RPL_ENDOFNAMES',
      367 => 'RPL_BANLIST', 368 => 'RPL_ENDOFBANLIST', 371 => 'RPL_INFO', 372 => 'RPL_MOTD',
      375 => 'RPL_MOTDSTART', 376 => 'RPL_ENDOFMOTD', 381 => 'RPL_YOUREOPER', 391 => 'RPL_TIME',
      412 => 'ERR_NOTEXTTOSEND', 422 => 'ERR_NOMOTD', 433 => 'ERR_NICKNAMEINUSE', 441 => 'ERR_USERNOTINCHANNEL',
      462 => 'ERR_ALREADYREGISTRED', 462 => 'ERR_NOPERMFORHOST', 464 => 'ERR_PASSWDMISMATCH',
      465 => 'ERR_YOUREBANNEDCREEP', 467 => 'ERR_KEYSET', 471 => 'ERR_CHANNELISFULL', 472 => 'ERR_UNKNOWNMODE',
      473 => 'ERR_INVITEONLYCHAN', 474 => 'ERR_BANNEDFROMCHAN', 475 => 'ERR_BADCHANNELKEY',
      481 => 'ERR_NOPRIVILEGES', 482 => 'ERR_CHANOPRIVSNEEDED', 491 => 'ERR_NOOPERHOST'
    );

    if (isset($events[$numeric])) return $events[$numeric];
    $this->log('e',"No name fo event $numeric, converted to NUMBER_$numeric");
    return 'NUMBER_'.$numeric;
  }

 /**
   * Connect command
   * @param string $server
   * @param string $port
   */
  public function connect($server=null, $port=null)
  {
    //disconnect if connected
    if ($this->socket) $this->disconnect();
    $this->log('+','Connecting '.$this->server.'...');
    
    //change server|port variables, if given in parameters, otherwise use old
    if (!is_null($server)) $this->server = $server;
    if (!is_null($port)) $this->$port = $port;
    
    //connect
    $erno = $errstr = 0;
    $this->socket = fsockopen($server, $port, $erno, $errstr, 30);
    if(!$this->socket) die("Could not connect $erno $errstr");
    
    //this option allows our bot to process timers and other 
    //stuff while waiting for commands
    stream_set_blocking($this->socket,0);
    
    //run event CONNECTED
    $this->event('CONNECTED');
    
    //start main loop
    $this->main();

    /*
    I made this system to be maximum interesting for developers, like I am,
    that's why all processes from this point are controlled by plugins.
    Now your bot is connected to IRC, it sends CONNECTED event
    to all the plugins, that accepts it, and Startup plugin doing this.
    @see plugins/startup.class.php if you really interested in startup sequence
    @see plugins/helloworld.class.php if you want something easy
    */
    
  }

  /**
   * Disconnect command
   * @return string
   */
  public function disconnect()
  {
    if (!$this->socket) return;
    fclose($this->socket);
    $this->event('DISCONNECTED');
    $this->log('+','Disconnected from '.$this->server);
    return 'Disconnected from '.$this->server;
  }

  /**
   * Nick command
   * @param string $nick
   */
  public function nick($nick=null)
  {
    if (is_null($nick)) $nick = $this->me;
    $this->queue("NICK $nick");
    // @see startup plugin, where nick changing catches
  }

  /**
   * User command
   * @param string $username
   * @param string $hostname
   * @param string $servername
   * @param string $realname
   */
  public function user($username='joker', $hostname='joker', $servername='blackcrystal.net', $realname='BC^joker the IRC bot') { $this->queue("USER $username $hostname $servername :$realname"); } 

  /**
   * PASS command
   * @param string $password
   */
  public function pass($password='NOPASS') { $this->queue("PASS $password"); } 

  /**
   * JOIN command
   * @param string $chan
   */
  public function join($chan) { $this->queue("JOIN $chan") ; } 

  /**
   * PART command
   * @param string $chan
   */
  public function part($chan) { $this->queue("PART $chan") ; } 

  /**
   * MSG command
   * @param string $target
   * @param string $msg
   */
  public function msg($target,$msg)
  {
    $msg = implode(' ',array_slice(func_get_args(), 1));
    $msg = wordwrap($msg, 430, "\n", true);
    foreach (explode("\n", $msg) as $item)
    {
      $this->queue("PRIVMSG $target :$item");
    }
  }

  /**
   * Shortcut to give quick answer to somebody (channel or nick)
   * @param string $msg
   */
  public function answer($msg)
  {
    $msg = implode(' ',func_get_args());
    $target = $this->chan ? $this->chan : $this->nick;
    $this->msg($target, $msg);
  }


  /**
   * NOTICE command
   * @param string $target
   * @param string $msg
   */
  public function notice($target,$msg) 
  {
    $msg = implode(' ',array_slice(func_get_args(), 1));
    $msg = wordwrap($msg, 430, "\n", true);
    foreach (explode("\n", $msg) as $item)
    {
      $this->queue("NOTICE $target :$item");
    }
  }

  /**
   * CHANLIST command
   * @param string $target
   */
  public function chanlist($target='') { $this->queue("LIST $target"); }

  /**
   * QUIT command
   * @param string $message
   */
  public function quit($msg='buj :p')
  {
    $msg = implode(' ', func_get_args() );
    $this->queue("QUIT :$msg");
  }

  /**
   * WHO command
   * @param string $params
   */
  public function who($params) { $this->queue("WHO $params"); } 

  /**
   * MODE command
   * @param string $params
   */
  public function mode($params) { $this->queue("MODE ". implode(' ',func_get_args())); }

  /**
   * OP is alias for MODE command
   * @param string $chan
   * @param string $nick
   */
  public function op($chan, $nick) { $this->queue("MODE $chan +o $nick"); }

  /**
   * DEOP is alias for MODE command
   * @param string $chan
   * @param string $nick
   */
  public function deop($chan, $nick) { $this->queue("MODE $chan -o $nick"); }

  /**
   * VO is alias for MODE command
   * @param string $chan
   * @param string $nick
   */
  public function vo($chan, $nick) { $this->queue("MODE $chan +v $nick"); }

  /**
   * DEVO is alias for MODE command
   * @param string $chan
   * @param string $nick
   */
  public function devo($chan, $nick) { $this->queue("MODE $chan -v $nick"); }

  /**
   * TOPIC command
   * @param string $channel
   * @param string $topic
   */
  public function topic($channel, $topic) 
  {
    $topic = implode(' ',array_slice(func_get_args(), 1));
    $this->queue("TOPIC $channel :$topic");
  }

  /**
   * INVITE command
   * @param string $nick
   * @param string $channel
   */
  public function invite($nick, $channel) { $this->queue("INVITE $nick $channel"); } 

  /**
   * KICK command
   * @param string $channel
   * @param string $nick
   * @param string $comment
   */
  public function kick($channel, $nick, $comment = 'Sorry d0g :p')
  {
    $comment = implode(' ',array_slice(func_get_args(), 3));
    $this->queue("KICK $channel $nick :$comment");
  }

  /**
   * CTCP command
   * @param string $target
   * @param string $msg
   */
  public function ctcp($target, $msg)
  {
    $msg = implode(' ',array_slice(func_get_args(), 1));
    $msg = wordwrap($msg, 430, "\n", true);
    foreach (explode("\n", $msg) as $item)
    {
      $this->msg($target, "\001$item\001");
    }
  }

  /**
   * ACTION command
   * @param string $target
   * @param string $msg
   */
  public function action($target, $msg)
  {
    $msg = implode(' ',array_slice(func_get_args(), 1));
    $this->ctcp($target,'ACTION '.$msg);
  }

  /**
   * YO command, an easter egg
   * @param string $chan
   * @param string $nick
   */
  public function yo($chan,$nick) { $this->action($chan, 'sets mode: +yo '.$nick); }
  
}