| | ------------------------------------------------------------------ | | Subtility - Core framework for IRC connections and event handling | | ------------------------------------------------------------------ | | This program is free software; you can redistribute it and/or | | modify it under the terms of the GNU General Public License as | | published by the Free Software Foundation; either version 2 of | | the License, or (at your option) any later version. | | | | This program is distributed in the hope that it will be useful, | | but WITHOUT ANY WARRANTY; without even the implied warranty of | | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | | GNU General Public License for more details. | | | | You should have received a copy of the GNU General Public License | | along with this program; if not, write to the Free Software | | Foundation, Inc., 59 Temple Place, Suite 330, Boston, | | MA 02111-1307 USA | ------------------------------------------------------------------ */ /* ------------------------------------------------------------------ | TODO | | ------------------------------------------------------------------ | | | | ------------------------------------------------------------------ | | CHANGELOG | | ------------------------------------------------------------------ | | 19th/Nov/2003 | | - New onNickChange event | | - Now supports multiple channels | | 20th/Sep/2003 | | - Few changes, ircBotPath is now set in bot file, used for paths | | - New onCTCP and onChanCTCP events | | - On any CTCP event the onCTCP/onChanCTCP events will be called | | - Added onInvite event | | 10th/Sep/2003 | | - Now setting stream blocking and write buffer | | - Now uses serverPort configuration variable (sorry) | | 1st/Sep/2003 | | - Fixed stayAlive and ircConnect::_rotateServers(); | | - Added mode handling for: | | onOp/onDeop, onVoice/onDevoice, onBan/onUnBan | | 31st/Aug/2003 | | - Initial public development release | ------------------------------------------------------------------ */ define("ircBotVersion", "subtility-20031119/php-".phpVersion()); /** Callback events */ define("onPing", 1000); define("onChanMsg", 1100); define("onPrivMsg", 1101); define("onNotice", 1102); define("onJoin", 1200); define("onPart", 1201); define("onKick", 1202); define("onKill", 1203); define("onNotInvited", 1204); define("onBanned", 1204); define("onTopic", 1300); define("onQuit", 1301); define("onVersion", 1302); define("onMode", 1400); define("onOp", 1401); define("onDeOp", 1402); define("onVoice", 1403); define("onDeVoice", 1404); define("onBan", 1405); define("onUnBan", 1406); define("onCTCP", 1500); define("onChanCTCP", 1501); define("onInvite", 1502); define("onNickChange", 1504); include(ircBotPath."/lib/ircConf.class.php"); /** Parent bot methods, handles socket and log streams */ class ircConnect { var $log; var $conn; var $servers; var $lastServer; var $currentServer; var $retryConnections; var $retryCount = 0; function _connect() { $this->_rotateServers(); $port = ircConf::get("serverPort"); $this->log("Connecting to $this->currentServer:$port (#".($this->retryCount + 1).")"); if (!isset($this->conn) || !@is_resource($this->conn)) { if ($this->conn = @fSockOpen($this->currentServer, $port, $error, $errMsg, 120)) { /* For fGets() - wait for data to become available to stream */ stream_set_blocking($this->conn, true); stream_set_timeout($this->conn, 120); stream_set_write_buffer($this->conn, 0); $this->log("Connected to $this->currentServer:$port"); return true; } } $this->retryCount++; $this->log("Can't connect to $this->currentServer:$port ($error): $errMsg"); sleep(2); /* Retry every 2 seconds (so we don't throttle) */ return false; } function _rotateServers() { if (isset($this->servers[($this->lastServer + 1)])) { $this->currentServer = $this->servers[++$this->lastServer]; } else { $this->currentServer = $this->servers[0]; $this->lastServer = 0; } } function write($string) { fPuts($this->conn, $string . "\n"); } function read($length = 2048) { if (($data = @fGets($this->conn, $length)) !== false) { if (strlen($data) <= 0) return false; /** ignore empty data */ $data = trim($data); return $data; } return false; } function close($quitMsg = "*wave*") { $this->log("Shutting down..."); $this->write("QUIT :$quitMsg"); fClose($this->conn); fClose($this->log); } function log($str) { if (ircDebug || ircDoLog) fWrite($this->log, sprintf("[%s] %s\n", date("D jS M H:i:s"), $str)); } } /** Extends ircConnect, main bot functionality */ class ircBot extends ircConnect { var $callbacks = array(); function ircBot() { /** Retrieve servers */ $this->servers = ircConf::get("servers"); $this->currentServer = $this->servers[0]; $this->lastServer = 0; /** Open the log file */ $logDir = ircBotPath."/log"; if (!file_exists(ircBotPath."/log")) @mkDir(ircBotPath."/log"); $this->log = fOpen(ircBotPath."/log/subtility.log", "wb"); $this->log(ircBotVersion); } function addCallback($event, $method) { if (!isset($this->callbacks[$event])) $this->callbacks[$event] = array(); $this->callbacks[$event][] = $method; } function callback($code, &$args) { if (isset($this->callbacks[$code]) && is_array($this->callbacks[$code])) { $args[0] =& $this; /* Return the bot object to callback */ foreach ($this->callbacks[$code] as $callback) { call_user_func_array($callback, $args); } } } function init() { set_time_limit(0); @ob_end_flush(); ob_implicit_flush(true); $this->retryConnections = false; do { if ($this->_connect()) { $this->write("NICK ".ircConf::get("botNick")); $this->write("USER ".ircConf::get("botIdent")." 2 3 :".ircConf::get("botName")); $this->privMsg("NickServ", "identify ".ircConf::get("botNickServPass")); foreach ($GLOBALS["ircConf"]["chans"] as $channel => $data) { $this->write("JOIN $channel"); $this->write("MODE $channel ".$data["modes"]); } unset($channel, $data); do { while (($buffer = $this->read()) !== false) { if (ircDebug) $this->log($buffer); /** Handle events in order of their estimated usage desc */ /** Privmsgs */ if (ereg("^:(.+)!(.+)@(.+) PRIVMSG (.+) :(.*)", $buffer, $data)) { /** onChanMsg */ if ($data[4][0] == "#") { /** Channel CTCP's */ if (substr($data[5], 0, 1) == chr(1) && substr($data[5], -1) == chr(1)) { $this->callback(onChanCTCP, $data); break; } else { $this->callback(onChanMsg, $data); break; } } elseif ($data[4] == ircConf::get("botNick")) { /** Handle CTCP Messages */ if (substr($data[5], 0, 1) == chr(1) && substr($data[5], -1) == chr(1)) { /** CTCP versions */ if ($data[5] == chr(1)."VERSION".chr(1)) { $this->ctcpNotice($data[1], "VERSION", ircConf::get("versionReply")); /** CTCP Times */ } elseif ($data[5] == chr(1)."TIME".chr(1)) { $this->ctcpNotice($data[1], "TIME", date("D M j H:i:s Y")); /** CTCP Fingers, reply random data.. (could also add a timer to ircConnect::write() */ } elseif ($data[5] == chr(1)."FINGER".chr(1)) { $this->ctcpNotice($data[1], "FINGER", ircConf::get("botName")." (".ircConf::get("botIdent").") ". "Idle " . mt_rand(1, 5) . " seconds"); /** CTCP Pings */ } elseif (substr($data[5], 0, 5) == chr(1)."PING") { $this->ctcpNotice($data[1], "PING", substr($data[5], 6, -1)); } $this->callback(onCTCP, $data); break; /** onPrivMsg - handle all messages directed at us */ } else { $this->callback(onPrivMsg, $data); break; } } /** PING? PONG! :) */ } elseif (ereg("^PING :(.+)", $buffer, $data)) { $this->write("PONG " . $data[1]); break; /** Modes */ } elseif (ereg("^:(.+)!(.+)@(.+) MODE ([a-zA-Z0-9#_\\\^\{\}\|_-]+) (.*)", $buffer, $data)) { $modes = array(); $modeData = explode(" ", $data[5]); $modePos = 1; $size = strlen($modeData[0]); /** Shift through the raw mode data, collect our neccessary data required for simple and complex modes */ for ($i = 0; $i < $size; $i++) { if ($modeData[0][$i] == "+" || $modeData[0][$i] == "-") { $lastSwitch = $modeData[0][$i]; } else { if (in_array($modeData[0][$i], array("n", "t", "s", "R", "p", "m", "i"))) { $modes[] = array("type" => "simple", "switch" => $lastSwitch, "mode" => $modeData[0][$i], "data" => $lastSwitch . $modeData[0][$i]); /** Obviously some more mode handling needs to be done for the l/k modes */ } elseif (in_array($modeData[0][$i], array("o", "v", "b", "l", "k"))) { $modes[] = array("type" => "complex", "switch" => $lastSwitch, "mode" => $modeData[0][$i], "data" => $modeData[$modePos++]); /** Mode data position needs to be shifted */ } } } $this->_handleModes($modes, $data); unset($modes, $modeData, $size, $lastSwitch, $modePos); break; /** Topics */ } elseif (ereg("^:(.+)!(.+)@(.+) TOPIC (.+) :(.*)", $buffer, $data)) { $this->callback(onTopic, $data); break; /** Notices */ } elseif (ereg("^:(.+)!(.+)@(.+) NOTICE (.+) :(.*)", $buffer, $data)) { $this->callback(onNotice, $data); break; /** Channel parts */ } elseif (ereg("^:(.+)!(.+)@(.+) PART (.+)", $buffer, $data)) { $this->callback(onPart, $data); break; /** Channel joins */ } elseif (ereg("^:(.+)!(.+)@(.+) JOIN :(.+)", $buffer, $data)) { $this->callback(onJoin, $data); break; /** Channel kicks */ } elseif (ereg("^:(.+)!(.+)@(.+) KICK (.+) (.+) :(.*)", $buffer, $data)) { if ($data[5] == ircConf::get("botNick")) { /* we've been kicked, rejoin */ $this->write("JOIN ".$data[4]); } $this->callback(onKick, $data); break; /** Quit messages */ } elseif (ereg("^:(.+)!(.+)@(.+) QUIT :(.*)", $buffer, $data)) { $this->callback(onQuit, $data); break; /** Kill messages */ } elseif (ereg("^:(.+)!(.+)@(.+) KILL (.+) :(.*)", $buffer, $data)) { $this->callback(onKill, $data); /** Nickname taken */ } elseif (ereg("^:(.+) 433", $buffer, $data)) { $botNick = ircConf::get("botNick"); $this->write("NICK ".$botNick."_"); if (($nickPass = ircConf::get("botNickServPass")) !== false) { $this->privMsg("NickServ", "GHOST ".$botNick." ". $nickPass); sleep(1); /** Wait then retry our nick and join channels */ $this->write("NICK ".$botNick); foreach ($GLOBALS["ircConf"]["chans"] as $channel => $data) { $this->write("JOIN $channel"); $this->write("MODE $channel ".$data["modes"]); } unset($botNick, $channel, $data, $nickPass); } else { $this->log("Nickname is taken, can't GHOST - botNickServPass not set"); } unset($botNick); break; /** Nick changes */ } elseif (ereg("^:(.+)!(.+)@(.+) NICK :(.+)", $buffer, $data)) { $this->callback(onNickChange, $data); break; /** Invites */ } elseif (ereg("^:(.+)!(.+)@(.+) INVITE (.+) :(.+)", $buffer, $data)) { $this->callback(onInvite, $data); break; /** Error messages */ } elseif (ereg("^ERROR :(.+)", $buffer, $data)) { $this->log("ERROR: ".implode("] [", $data)); $this->log("Recieved ERROR, reconnecting"); break ; } elseif ($data[1] == 473) { $this->callback(onNotInvited, $data); break; } /** END Hanlders */ unset($buffer, $data); } /** Check socket */ } while ((!feOf($this->conn) && is_resource($this->conn))); $this->log("Connection failed, cleaning up socket"); fClose($this->conn); $this->retryConnections = true; } else { $this->retryConnections = true; } unset($this->conn); if ($this->retryCount >= 5) { if (!ircConf::get("stayAlive")) { $this->retryConnections = false; } else { $this->retryCount = 0; $this->log("Stay-Alive: trying again in 30 seconds\n"); sleep(30); } } } while ($this->retryConnections); $this->log("Finished, shutting down"); fClose($this->log); return false; } function _handleModes($modes, $data) { foreach ($modes as $mode) { $data[5] = $mode["data"]; switch ($mode["mode"]) { case 'o': ($mode["switch"] == "+") ? $this->callback(onOp, $data) : $this->callback(onDeOp, $data); break; case 'v': ($mode["switch"] == "+") ? $this->callback(onVoice, $data) : $this->callback(onDeVoice, $data); break; case 'b': ($mode["switch"] == "+") ? $this->callback(onBan, $data) : $this->callback(onUnBan, $data); break; default: $this->callback(onMode, $data); break; } } } function notice($to, $message) { $this->write("NOTICE ".$to." :".$message); } function privMsg($to, $message) { $this->write("PRIVMSG ".$to." :".$message); } function ctcpNotice($to, $type, $string) { $this->write("NOTICE $to :".chr(1).strToUpper($type)." ".$string.chr(1)); } function mode($target, $mode) { $this->write("MODE $target $mode"); } function umode ($chan = NULL, $nick, $mode = NULL){ if (!$mode) continue; else{ if(!$chan) $this->write('MODE '.$nick.' '. $mode .''); else $this->write('MODE ' . $chan . ' ' . $mode . ' ' . $nick . ''); } } function cmode ($target, $newmode = NULL){ if (!$newmode) continue; else $this->write('MODE ' . $target . ' ' . $newmode . ''); } function kick ($chan, $nick, $reason = NULL){ $this->write('KICK ' . $chan . ' ' . $nick. ' :'. $reason); } } ?>