<?php
/* (C) Copyright 2004 Armond Carroll. All rights reserved.
Distributed under the BSD License. Please see license.txt for license,
or http://www.opensource.org/licenses/bsd-license.php.
http://mpibot.sourceforge.net
Purpose of this file:
This is the actual bot.
File Version: 1.5
Last Modified: 4/30/2004 12:53AM
*/
class MPIBot {
private $sock, $servers, $modules = array();
public $nickname, $cfg, $db;
/* Special/Core Functions */
// Init:
// This used to be the constructor, but was moved to init because of scope
// problems within modules. (Modules were unable to access $bot->...)
public function init() {
global $cfg;
$this->cfg = $cfg;
// Load Modules
foreach($cfg['irc']['modules'] as $m) {
eval('if(!isset($this->modules["$m"])) $this->modules["$m"] = new $m(); else error("There are two or more modules name \'$m\' loaded. Modules must have unique names.");');
}
// Make sure the modules' ancestors are type 'BaseModule'
foreach($this->modules as $m) {
if(get_parent_class($m) != 'BaseModule') {
error('Module \'' . get_class($m) . '\' is not of ancestry to BaseModule');
}
}
// Since the mySQLi object is somewhat "special", load it here
if(@$cfg['db']['use'] == true) {
$this->db = @new mysqli($cfg['db']['hostname'], $cfg['db']['username'], $cfg['db']['password'], $cfg['db']['database']);
// The manual said to use !$this->db, but that doesn't seem to work well...
if(mysqli_connect_error()) {
error('Could not connect to mySQL: ' . mysqli_connect_error() . '; If you do not want mySQL support, turn db:use to false', OL_WARNING);
$this->db = NULL;
}
} else {
$this->db = NULL;
}
}
// __destruct()
// Call quit(), and close the mySQL connection if needed.
public function __destruct() {
$this->quit();
if($this->db != NULL) $this->db->close();
echo "MPIBot Shutting down... ", EOL, EOL;
}
/* Low level socket functions */
// Send:
// Send formatted data to the socket, and (NYI) optionally log/print it.
public function send($w) {
out($w, OL_PROTOSEND);
return fputs($this->sock, "$w\r\n");
}
// Recv:
// Receive raw messages from the client.
// Note: Written in such an odd way to discard
// NULL messages some servers send an abundance of
public function recv() {
while(empty($d)) {
$d = trim(fgets($this->sock));
}
out($d, OL_PROTORECV);
foreach($this->modules as $m) {
$m->on_recv($d);
}
return $d;
}
/* Connect, Keep, Disconnect Routines */
// Connect:
// Open the socket, and send the NICK+USER login
// sequence, handle in-use/bad/no nicknames, store
// some initial data, and finally once 001 is received,
// pass off to main()
public function connect() {
if(is_resource($this->sock)) { error('Attempting to connect, while already connected to a server', warning); return 0; }
// Keep trying to connect until we get a good socket
do {
// Get Server
$server = current($this->cfg['irc']['servers']);
next($this->cfg['irc']['servers']);
if(current($this->cfg['irc']['servers']) == false)
reset($this->cfg['irc']['servers']);
if(strpos($server, ':') === false) $server = array($server, 6667);
else {
$server = explode(':', $server);
}
// Actually Connect
out("Connecting to $server[0] on $server[1]... ", OL_INFO, true, false);
$this->sock = @fsockopen($server[0], $server[1], $errno, $errstr, $this->cfg['irc']['timeout']);
if($this->sock) out('Connected.', OL_INFO, false, true);
else out('Failed.', OL_INFO, false, true);
} while(!$this->sock);
// Were now connected to the server. Send NICK+USER
$this->nickname = $this->cfg['irc']['nickname'];
$this->send('NICK ' . $this->cfg['irc']['nickname']);
$this->send('USER ' . $this->cfg['irc']['username'] . ' 0 0 :' . $this->cfg['irc']['realname']);
// Loop and stay until we reach 001
while(!feof($this->sock)) {
$info = $this->parse($this->recv());
switch($info['command']) {
case '001':
foreach($this->modules as $m) {
$m->on_connect();
}
break 2;
case '431': // ERR_NONICKNAMEGIVEN
case '432': // ERR_ERRONEUSNICKNAME
case '433': // ERR_NICKNAMEINUSE
case '436': // ERR_NICKCOLLISION
$ltr = mt_rand(65, 90);
$nick = strtolower($ltr . substr($this->nickname, 0, 8));
$this->send('NICK ' . $nick);
$this->nickname = $nick;
break;
case 'PING':
$this->send('PONG ' . $info['parameter']);
break;
case 'ERROR': // Old bug.. Break 2 so we can try to reconnect
out('Disconnected from server.');
break 2;
}
}
}
// Main:
// Keeps bot alive, and provides handling for
// additional module support
public function main() {
if(!is_resource($this->sock)) return 0;
while(!feof($this->sock)) {
$info = $this->parse($this->recv());
switch($info['command']) {
case 'PING':
$this->send('PONG ' . $info['parameter']);
break;
case 'PRIVMSG':
if(is_array($info['privmsg'])) {
foreach($this->modules as $m) {
$m->on_msg($info);
}
if(is_array($info['trigger'])) {
foreach($this->modules as $m) {
$m->on_trigger($info);
}
}
}
break;
}
}
}
// Quit:
// Sends QUIT to server and closes the socket
public function quit($msg = NULL) {
foreach($this->modules as $m) {
$m->on_quit();
}
if(is_resource($this->sock)) {
if($msg == NULL) $msg = $this->cfg['irc']['quitmsg'];
@$this->send("QUIT :$msg");
@fclose($this->sock);
}
}
/* Parsing Routines */
// Parse:
// Input a raw message from the server, returns a pretty formatted
// version. Its rather tedious for a very used function. If anyone
// knows a faster way, please tell me.
public function parse($data) {
// First parse: who, command, parameter
if($data[0] != ':') {
list($command, $parameter) = explode(' ', $data, 2);
} else {
list($who, $command, $parameter) = explode(' ', $data, 3);
$who = substr($who, 1);
}
// Trim : from parameter, if present
if($parameter[0] == ':') $parameter = substr($parameter, 1);
// Now figure out $who
if(!isset($who) || strpos($who, '@') === false) {
$who = from_server;
} else {
list($nick, $rest) = explode('!', $who);
list($user, $host) = explode('@', $rest);
if($user[0] == '~') { $ident = false; $user = substr($user, 1); } else { $ident = true; }
$who = array('nickname' => $nick, 'username' => $user, 'hostname' => $host, 'ident' => $ident);
}
if($command == 'PRIVMSG') {
list($to, $msg) = explode(' ', $parameter, 2);
if($to[0] == '#' || $to[0] == '&') { $reply_to = $to; $type = 'public'; } else { $reply_to = $who['nickname']; $type = 'private'; }
if($msg[0] == ':') $msg = substr($msg, 1);
$privmsg = array('to' => $to, 'reply-to' => $reply_to, 'type' => $type, 'msg' => $msg);
}
// Easy access to stuff for on_trigger
if($who != from_server && $command == 'PRIVMSG' && $privmsg['msg'][0] == $this->cfg['irc']['triggerchar']) {
@list($trigger, $trigger_parameter) = explode(' ', $privmsg['msg'], 2);
$trigger = substr($trigger, 1); // Remove .
$trigger = array('command' => $trigger, 'parameter' => $trigger_parameter);
}
return array('string' => $data, 'who' => @$who, 'command' => $command, 'parameter' => $parameter, 'privmsg' => @$privmsg, 'trigger' => @$trigger);
}
/* Protocol Methods */
// Join:
// Sends JOIN to the server and adds the channel
// to the channel list array. Error handling
// (such as +b) should be done in main()
public function join($chan, $key = NULL) {
out("Attempting to join $chan...", OL_INFO);
$this->send("JOIN $chan $key");
foreach($this->modules as $m) {
$m->on_joinchan($chan, $key);
}
}
// Part:
// Parts a channel. Error handling (if any)
// should be done in main()
public function part($chan, $msg = NULL) {
out("Parting $chan...", OL_INFO);
if($msg == NULL) $msg = $this->cfg['irc']['partmsg'];
$this->send("PART $chan");
foreach($this->modules as $m) {
$m->on_partchan($chan);
}
}
// Say:
// Say something to a channel or person
public function say($to, $what) {
$this->send("PRIVMSG $to :$what");
}
// Nick:
// Change the bot's nickname
public function nick($nick) {
$this->nickname = $nick;
$this->send("NICK $nick");
}
}
// :)
?>