libirc.php
changeset 0 d02690a8552c
child 8 0acb8d9a3194
equal deleted inserted replaced
-1:000000000000 0:d02690a8552c
       
     1 <?php
       
     2 
       
     3 /**
       
     4  * PHP IRC Client library
       
     5  * Copyright (C) 2008 Dan Fuhry. All rights reserved.
       
     6  */
       
     7 
       
     8 /**
       
     9  * Version number
       
    10  * @const string
       
    11  */
       
    12 
       
    13 define('REQUEST_IRC_VERSION', '0.1');
       
    14 
       
    15 /**
       
    16  * The base class for an IRC session.
       
    17  */
       
    18 
       
    19 class Request_IRC
       
    20 {
       
    21   
       
    22   /**
       
    23    * Hostname
       
    24    * @var string
       
    25    */
       
    26   
       
    27   private $host = '';
       
    28   
       
    29   /**
       
    30    * Port number
       
    31    * @var int
       
    32    */
       
    33   
       
    34   private $port = 6667;
       
    35   
       
    36   /**
       
    37    * The socket for the connection.
       
    38    * @var resource
       
    39    */
       
    40   
       
    41   public $sock = false;
       
    42   
       
    43   /**
       
    44    * Channel objects, associative array
       
    45    * @var array
       
    46    */
       
    47   
       
    48   public $channels = array();
       
    49   
       
    50   /**
       
    51    * The function called when a private message is received.
       
    52    * @var string
       
    53    */
       
    54   
       
    55   private $privmsg_handler = false;
       
    56   
       
    57   /**
       
    58    * Switch to track if quitted or not. Helps avoid quitting the connection twice thus causing write errors.
       
    59    * @var bool
       
    60    * @access private
       
    61    */
       
    62   
       
    63   protected $quitted = false;
       
    64   
       
    65   /**
       
    66    * The nickname we're connected as. Not modified once connected.
       
    67    * @var string
       
    68    */
       
    69   
       
    70   public $nick = '';
       
    71   
       
    72   /**
       
    73    * The username we're connected as. Not modified once connected.
       
    74    * @var string
       
    75    */
       
    76   
       
    77   public $user = '';
       
    78   
       
    79   /**
       
    80    * Constructor.
       
    81    * @param string Hostname
       
    82    * @param int Port number, defaults to 6667
       
    83    */
       
    84   
       
    85   public function __construct($host, $port = 6667)
       
    86   {
       
    87     // Check hostname
       
    88     if ( !preg_match('/^(([a-z0-9-]+\.)*?)([a-z0-9-]+)$/', $host) )
       
    89       die(__CLASS__ . ': Invalid hostname');
       
    90     $this->host = $host;
       
    91     
       
    92     // Check port
       
    93     if ( is_int($port) && $port >= 1 && $port <= 65535 )
       
    94       $this->port = $port;
       
    95     else
       
    96       die(__CLASS__ . ': Invalid port');
       
    97   }
       
    98   
       
    99   /**
       
   100    * Sets parameters and opens the connection.
       
   101    * @param string Nick
       
   102    * @param string User
       
   103    * @param string Real name
       
   104    * @param string NickServ password
       
   105    * @param int Flags, defaults to 0.
       
   106    */
       
   107   
       
   108   public function connect($nick, $username, $realname, $pass, $flags = 0)
       
   109   {
       
   110     // Init connection
       
   111     $this->sock = fsockopen($this->host, $this->port);
       
   112     if ( !$this->sock )
       
   113       throw new Exception('Could not make socket connection to host.');
       
   114     
       
   115     stream_set_timeout($this->sock, 5);
       
   116     
       
   117     // Wait for initial ident messages
       
   118     while ( $msg = $this->get() )
       
   119     {
       
   120     }
       
   121     
       
   122     // Send nick and username
       
   123     $this->put("NICK $nick\r\n");
       
   124     $this->put("USER $username 0 * :$realname\r\n");
       
   125     
       
   126     // Wait for response and end of motd
       
   127     $motd = '';
       
   128     while ( $msg = $this->get() )
       
   129     {
       
   130       // Match particles
       
   131       $msg = trim($msg);
       
   132       $mc = preg_match('/^:([A-z0-9\.-]+) ([0-9]+) [A-z0-9_-]+ :(.+)$/', $msg, $match);
       
   133       if ( !$mc )
       
   134       {
       
   135         $mc = preg_match('/^:([A-z0-9_-]+)!([A-z0-9_-]+)@([A-z0-9_\.-]+) NOTICE [A-z0-9_-]+ :(.+)$/', $msg, $match);
       
   136         if ( !$mc )
       
   137           continue;
       
   138         // Look for a response from NickServ
       
   139         if ( $match[1] == 'NickServ' )
       
   140         {
       
   141           // Asking for auth?
       
   142           if ( strpos($match[4], 'IDENTIFY') )
       
   143           {
       
   144             // Yes, send password
       
   145             $this->privmsg('NickServ', "IDENTIFY $pass");
       
   146           }
       
   147         }
       
   148       }
       
   149       list(, $host, $stat, $msg) = $match;
       
   150       $motd .= "$msg";
       
   151     }
       
   152     
       
   153     $this->nick = $nick;
       
   154     $this->user = $username;
       
   155   }
       
   156   
       
   157   /**
       
   158    * Writes some data to the socket, abstracted for debugging purposes.
       
   159    * @param string Message to send, this should include a CRLF.
       
   160    */
       
   161   
       
   162   public function put($message)
       
   163   {
       
   164     if ( !$this->sock )
       
   165     {
       
   166       if ( defined('LIBIRC_DEBUG') )
       
   167         echo ">>> WRITE FAILED: $message";
       
   168       return false;
       
   169     }
       
   170     if ( defined('LIBIRC_DEBUG') )
       
   171       echo ">>> $message";
       
   172     fwrite($this->sock, $message);
       
   173   }
       
   174   
       
   175   /**
       
   176    * Reads from the socket...
       
   177    * @return string
       
   178    */
       
   179   
       
   180   public function get()
       
   181   {
       
   182     if ( !$this->sock )
       
   183     {
       
   184       if ( defined('LIBIRC_DEBUG') )
       
   185         echo "<<< READ FAILED\n";
       
   186       return false;
       
   187     }
       
   188     $out = fgets($this->sock, 4096);
       
   189     if ( defined('LIBIRC_DEBUG') )
       
   190       if ( !empty($out) )
       
   191         echo "<<< $out";
       
   192     return $out;
       
   193   }
       
   194   
       
   195   /**
       
   196    * Sends a message to a nick or channel.
       
   197    * @param string Nick or channel
       
   198    * @param string Message
       
   199    */
       
   200   
       
   201   public function privmsg($nick, $message)
       
   202   {
       
   203     $message = str_replace("\r\n", "\n", $message);
       
   204     $message = explode("\n", $message);
       
   205     foreach ( $message as $line )
       
   206     {
       
   207       $this->put("PRIVMSG $nick :$line\r\n");
       
   208     }
       
   209   }
       
   210   
       
   211   /**
       
   212    * The main event loop.
       
   213    */
       
   214   
       
   215   public function event_loop()
       
   216   {
       
   217     stream_set_timeout($this->sock, 0xFFFFFFFE);
       
   218     while ( $data = $this->get() )
       
   219     {
       
   220       $data_trim = trim($data);
       
   221       $match = self::parse_message($data_trim);
       
   222       if ( preg_match('/^PING :(.+?)$/', $data_trim, $pmatch) )
       
   223       {
       
   224         $this->put("PONG :{$pmatch[1]}\r\n");
       
   225       }
       
   226       else if ( $match )
       
   227       {
       
   228         // Received PRIVMSG or other mainstream action
       
   229         if ( $match['action'] == 'JOIN' )
       
   230           $channel =& $match['message'];
       
   231         else
       
   232           $channel =& $match['target'];
       
   233           
       
   234         if ( !preg_match('/^[#!&\+]/', $channel) )
       
   235         {
       
   236           // Private message from user
       
   237           $result = $this->handle_privmsg($data);
       
   238           stream_set_timeout($this->sock, 0xFFFFFFFE);
       
   239         }
       
   240         else if ( isset($this->channels[strtolower($channel)]) )
       
   241         {
       
   242           // Message into channel
       
   243           $chan =& $this->channels[strtolower($channel)];
       
   244           $func = $chan->get_handler();
       
   245           $result = @call_user_func($func, $data, $chan);
       
   246           stream_set_timeout($this->sock, 0xFFFFFFFE);
       
   247         }
       
   248         if ( $result == 'BREAK' )
       
   249         {
       
   250           break;
       
   251         }
       
   252       }
       
   253     }
       
   254   }
       
   255   
       
   256   /**
       
   257    * Processor for when a private message is received.
       
   258    * @access private
       
   259    */
       
   260   
       
   261   private function handle_privmsg($message)
       
   262   {
       
   263     $message = self::parse_message($message);
       
   264     $ph = $this->privmsg_handler;
       
   265     if ( @function_exists($ph) )
       
   266       return @call_user_func($ph, $message);
       
   267   }
       
   268   
       
   269   /**
       
   270    * Changes the function called upon receipt of a private message.
       
   271    * @param string Function to call, will be passed a parsed message.
       
   272    */
       
   273   
       
   274   function set_privmsg_handler($func)
       
   275   {
       
   276     if ( !function_exists($func) )
       
   277       return false;
       
   278     $this->privmsg_handler = $func;
       
   279     return true;
       
   280   }
       
   281   
       
   282   /**
       
   283    * Parses a message line.
       
   284    * @param string Message text
       
   285    * @return array Associative with keys: nick, user, host, action, target, message
       
   286    */
       
   287    
       
   288   public static function parse_message($message)
       
   289   {
       
   290     // Indices:          12       3       4        5        67                         8
       
   291     $mc = preg_match('/^:(([^ ]+)!([^ ]+)@([^ ]+)) ([A-Z]+) (([#!&\+]*[A-z0-9_-]+) )?:?(.*?)$/', $message, $match);
       
   292     if ( !$mc )
       
   293     {
       
   294       return false;
       
   295     }
       
   296     // Indices: 0 1 2      3      4      5        6 7        8
       
   297     list(       , , $nick, $user, $host, $action, , $target, $message) = $match;
       
   298     return array(
       
   299         'nick' => $nick,
       
   300         'user' => $user,
       
   301         'host' => $host,
       
   302         'action' => $action,
       
   303         'target' => $target,
       
   304         'message' => trim($message)
       
   305       );
       
   306   }
       
   307   
       
   308   /**
       
   309    * Joins a channel, and returns a Request_IRC_Channel object.
       
   310    * @param string Channel name (remember # prefix)
       
   311    * @param string Event handler function, will be called with param 0 = socket output and param 1 = channel object
       
   312    * @return object
       
   313    */
       
   314   
       
   315   function join($channel, $handler)
       
   316   {
       
   317     $chan = new Request_IRC_Channel(strtolower($channel), $handler, $this);
       
   318     $this->channels[strtolower($channel)] = $chan;
       
   319     return $chan;
       
   320   }
       
   321   
       
   322   /**
       
   323    * Closes the connection and quits.
       
   324    * @param string Optional part message
       
   325    */
       
   326   
       
   327   public function close($partmsg = false)
       
   328   {
       
   329     if ( $this->quitted )
       
   330       return true;
       
   331     
       
   332     $this->quitted = true;
       
   333     // Part all channels
       
   334     if ( !$partmsg )
       
   335       $partmsg = 'IRC bot powered by PHP/' . PHP_VERSION . ' libirc/' . REQUEST_IRC_VERSION;
       
   336     
       
   337     foreach ( $this->channels as $channel )
       
   338     {
       
   339       $channel->part($partmsg);
       
   340     }
       
   341     
       
   342     $this->put("QUIT\r\n");
       
   343     
       
   344     while ( $msg = $this->get() )
       
   345     {
       
   346       // Do nothing.
       
   347     }
       
   348     
       
   349     fclose($this->sock);
       
   350   }
       
   351   
       
   352 }
       
   353 
       
   354 /**
       
   355  * Wrapper for channels.
       
   356  */
       
   357 
       
   358 class Request_IRC_Channel extends Request_IRC
       
   359 {
       
   360   
       
   361   /**
       
   362    * The name of the channel
       
   363    * @var string
       
   364    */
       
   365   
       
   366   private $channel_name = '';
       
   367   
       
   368   /**
       
   369    * The event handler function.
       
   370    * @var string
       
   371    */
       
   372   
       
   373   private $handler = '';
       
   374   
       
   375   /**
       
   376    * The parent connection.
       
   377    * @var object
       
   378    */
       
   379   
       
   380   public $parent = false;
       
   381   
       
   382   /**
       
   383    * Whether the channel has been parted or not, used to kill the destructor.
       
   384    * @var bool
       
   385    */
       
   386   
       
   387   protected $parted = false;
       
   388   
       
   389   /**
       
   390    * Constructor.
       
   391    * @param string Channel name
       
   392    * @param string Handler function
       
   393    * @param object IRC connection (Request_IRC object)
       
   394    */
       
   395   
       
   396   function __construct($channel, $handler, $parent)
       
   397   {
       
   398     $this->parent = $parent;
       
   399     $this->parent->put("JOIN $channel\r\n");
       
   400     stream_set_timeout($this->parent->sock, 3);
       
   401     while ( $msg = $this->parent->get() )
       
   402     {
       
   403       // Do nothing
       
   404     }
       
   405     $this->channel_name = $channel;
       
   406     $this->handler = $handler;
       
   407   }
       
   408   
       
   409   /**
       
   410    * Returns the channel name
       
   411    * @return string
       
   412    */
       
   413   
       
   414   function get_channel_name()
       
   415   {
       
   416     return $this->channel_name;
       
   417   }
       
   418   
       
   419   /**
       
   420    * Returns the handler function
       
   421    * @return string
       
   422    */
       
   423   
       
   424   function get_handler()
       
   425   {
       
   426     return $this->handler;
       
   427   }
       
   428   
       
   429   /**
       
   430    * Sends a message.
       
   431    * @param string message
       
   432    * @param bool If true, will fire a message event when the message is sent.
       
   433    */
       
   434   
       
   435   function msg($msg, $fire_event = false)
       
   436   {
       
   437     $this->parent->privmsg($this->channel_name, $msg);
       
   438     if ( $fire_event )
       
   439     {
       
   440       $func = $this->get_handler();
       
   441       // format: :nick!user@host PRIVMSG #channel :msg.
       
   442       $lines = explode("\n", $msg);
       
   443       foreach ( $lines as $line )
       
   444       {
       
   445         $data = ":{$this->parent->nick}!{$this->parent->user}@localhost PRIVMSG {$this->channel_name} :$line";
       
   446         $result = @call_user_func($func, $data, $this);
       
   447         stream_set_timeout($this->parent->sock, 0xFFFFFFFE);
       
   448       }
       
   449     }
       
   450   }
       
   451   
       
   452   /**
       
   453    * Destructor, automatically parts the channel.
       
   454    */
       
   455   
       
   456   function __destruct()
       
   457   {
       
   458     if ( !$this->parted )
       
   459       $this->part('IRC bot powered by PHP/' . PHP_VERSION . ' libirc/' . REQUEST_IRC_VERSION);
       
   460   }
       
   461   
       
   462   /**
       
   463    * Parts the channel.
       
   464    * @param string Optional message
       
   465    */
       
   466   
       
   467   function part($msg = '')
       
   468   {
       
   469     $this->parent->put("PART {$this->channel_name} :$msg\r\n");
       
   470     $this->parted = true;
       
   471     unset($this->parent->channels[$this->channel_name]);
       
   472   }
       
   473   
       
   474 }
       
   475 
       
   476 ?>