Skip to content
Browse files

Initial commit.

  • Loading branch information...
0 parents commit 40bf7ab85e46e54022514c9e272f83aca7a6f524 @epochblue epochblue committed
Showing with 1,027 additions and 0 deletions.
  1. +4 −0 .gitignore
  2. +17 −0 LICENSE
  3. +244 −0 README.md
  4. +21 −0 composer.json
  5. +69 −0 src/Philip/Action.php
  6. +189 −0 src/Philip/IRC/Request.php
  7. +154 −0 src/Philip/IRC/Response.php
  8. +329 −0 src/Philip/Philip.php
4 .gitignore
@@ -0,0 +1,4 @@
+# misc junk files
+.DS_Store
+*.swp
+
17 LICENSE
@@ -0,0 +1,17 @@
+Copyright (c) 2012 Bill Israel <bill.israel@gmail.com>
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software
+and associated documentation files (the "Software"), to deal in the Software without restriction,
+including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
+and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so,
+subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or substantial
+portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
+LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
+NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
244 README.md
@@ -0,0 +1,244 @@
+Philip - a PHP IRC bot framework
+================================
+
+Philip is a [Slim](http://slimframework.com/)-inspired framwork for creating simple IRC bots.
+It was written by [Bill Israel](http://billisrael.info/). The purpose of the project is to
+allow people to create fun, simple IRC bots with a minimal amount of overhead.
+
+
+Requirements
+------------
+
+ * PHP 5.3.0+
+ * [Composer](http://getcomposer.org/)
+
+
+Installation
+------------
+
+The best way to create a bot based on Philip is to use [Composer](http://getcomposer.org/).
+From the command line, create a directory for your bot. In this directory, first download Composer:
+
+```sh
+$> curl -s http://getcomposer.org/installer | php
+```
+
+Then create and open a `composer.json` file. Add Philip to the list of required libraries:
+
+```javascript
+{
+ "require": {
+ "epochblue/philip": "dev-master"
+ }
+}
+```
+
+Run Composer's install command to download and install the Philip library:
+
+```sh
+$> php composer.phar install -v
+```
+
+Once this is complete, your directory should have a few new items (a `composer.lock` file, and
+a `vendors` directory) in it, and you shoudl be ready to go. All that's left is to create the
+the bot. You can name your bot whatever you want, though `bot.php` is nice and easy.
+Here's a basic example:
+
+```php
+// bot.php
+<?php
+
+require __DIR__ . '/vendor/autoload.php';
+
+$config = array(
+ "hostname" => "irc.freenode.net",
+ "servername" => "example.com",
+ "port" => 6667,
+ "username" => "examplebot",
+ "realname" => "example IRC Bot",
+ "nick" => "examplebot",
+ "channels" => array( '#example-channel' ),
+ "admins" => array( 'example' ),
+);
+
+$bot = new Philip($config);
+
+$bot->onChannel('/^!echo (.*)$/', function($request, $matches) {
+ $echo = trim($matches[0]);
+ return Response::msg($request->getSource(), $echo);
+});
+
+$bot->run();
+```
+
+Save your file, and start your bot:
+
+```sh
+$> php examplebot.php
+```
+
+And that's all there is to it! Your bot will connect to the IRC server, join the channels you've
+wanted, and start listening for any commands you've specified. For more information about
+Philip's API, please see the API section below.
+
+
+API
+---
+
+Philip's API is simple and similar to JavaScript's "on*" event system. You add functionality
+to your bot by telling it how to respond to certain events. Events include things like channel
+messages, private messages, users joining a channel, users leaving a channel, etc.
+
+To determine whether your bot should respond to a given event, you will supply a regular expression
+that Philip will test against. If the regex matches, then Philip will execute the callback function
+you provide. Putting it all together, the basic API for Philip follows this pattern:
+
+```php
+$bot->on<Event>(<regex pattern>, <callback function>)
+```
+
+Possible values for &lt;Event&gt; include `Channel`, `PrivateMessage`, `Message`, `Join`, `Part` `Error`,
+and `Notice`.
+
+#### Event Examples:
+
+```php
+$bot->onChannel() // listens only to channel messages
+$bot->onPrivateMessage() // listens only to private messages
+$bot->onMessage() // listens to both channel messages and private messages
+$bot->onJoin() // listens only for people joining channels
+$bot->onPart() // listens only for people leaving channels
+$bot->onError() // listens only for IRC ERROR messages
+$bot->onNotice() // listens only for IRC NOTICE messages
+```
+
+The `<regex pattern>` is a standard PHP regular expression. If `null` is passed instead of a
+regular expression, the callback function will be executed for all messages of that event type.
+If any match groups are specified in the regular expression, they will be passed to the callback function.
+
+If your regular expression is successfully matched, Philip will execute the callback function you provide,
+allowing you to respond to the message. The `<callback function>` is an anonymous function that accepts
+two parameters: `$request` and `$matches`.
+
+`$request` is an instance of `Philip\IRC\Request` (which is a simple wrapper over a raw IRC message).
+`$matches` is an array of any matches found for match groups specified by the <regex pattern>.
+
+There are no strong requirements around what a `<callback function>` must return. However, if you
+wish to send a message back to the IRC server, the function must return a `Philip\IRC\Response`.
+Putting all this together, let's look an example of adding `echo` functionality to Philip:
+
+### Example:
+
+```php
+$bot->onChannel('/^!echo (.*)$/', function($request, $matches) {
+ $echo = trim($matches[0]);
+ return Response::msg($request->getSource(), $echo);
+});
+```
+
+In this example, the bot will listen for channel messages that begin with `!echo` and are followed
+by anything else. The "anything else" is captured in a match group and passed to the callback
+function. The callback function simply returns the match to the channel that originall received
+the message.
+
+#### Methods of note in the `Philip\IRC\Request` object:
+
+```php
+$request->getSendingUser() // get the nick of the user sending a message
+$request->getSource() // get the channel of the sent message,
+ // or nick if it was a private message
+$request->getMessage() // get the text of a message for channel/private messages
+```
+
+
+#### Methods of note in the `Philip\IRC\Response` object:
+
+```php
+Response::msg($where, $what) // Sends a message $what to channel/PM $where
+Response::action($where, $what) // Same as a ::msg(), but sends the message as an IRC ACTION
+```
+
+
+Plugins
+-------
+
+Philip supports a basic plugin system, and adding a plugin to your bot is simple.
+
+### Using a plugin
+
+Using a plugin is simple. In your bot's project directory, create a `plugins` directory, and place the
+plugin files in it. In the file for your bot, load the plugins by calling either `loadPlugin(<name>)`
+(to load them one at a time), or `loadPlugins(array())` (to load multiple plugins at once). A plugin's
+"name" is considered to be the plugin's class name without the word "Plugin".
+
+For example, if you had a HelloPlugin installed, you can do load it with either of the following:
+
+```php
+$bot->loadPlugin('Hello');
+$bot->loadPlugins(array('Hello'));
+```
+
+Additionally, if you'd like to turn some of your bot's functionality into a plugin, that's easy as well.
+
+### Writing a plugin
+
+To create a plugin, you must follow a few simple conventions, but beyond that, there's very little to them.
+A Plugin is little more than a specially-named file containing a single namespace-less plain-old-PHP-object
+that has, at minimum, two methds:
+
+* a constructor that accepts an instance of a Philip bot as a paramter, and
+* an `init()` method for setting up the plugin functionality
+
+Your plugin should be named like `XXXPlugin`, where XXX is the "name" of your plugin.
+Below is an example of a simple plugin:
+
+```php
+// HelloPlugin.php
+<?php
+
+class HelloPlugin
+{
+ /** @var Philip $bot The instance of the bot to add functionality to */
+ private $bot;
+
+ /**
+ * Constructor, with injected $bot dependency
+ */
+ public function __constructor($bot)
+ {
+ $this->bot = $bot;
+ }
+
+ /**
+ * Does the 'heavy lifting' of initializing the plugin's behavior
+ */
+ public funciton init()
+ {
+ $this->bot->onChannel('/^hello$/', function($request, $matches) {
+ return Response::msg($request-getSource(), "Hi, {$request->getSendingUser()}!");
+ });
+ }
+}
+```
+
+
+License
+-------
+
+Copyright (c) 2012 Bill Israel <bill.israel@gmail.com>
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software
+and associated documentation files (the "Software"), to deal in the Software without restriction,
+including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
+and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so,
+subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or substantial
+portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
+LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
+NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
21 composer.json
@@ -0,0 +1,21 @@
+{
+ "name": "epochblue/philip",
+ "description": "A Slim-inspired IRC bot framework",
+ "keywords": ["irc", "bot"],
+ "homepage": "http://github.com/epochblue/philip",
+ "type": "library",
+ "license": "MIT",
+ "authors": [
+ {
+ "name": "Bill Israel",
+ "email": "bill.israel@gmail.com",
+ "homepage": "http://cubicle17.com/"
+ }
+ ],
+ "require": {
+ "php": ">=5.3.0"
+ },
+ "autoload": {
+ "psr-0": {"Philip": "src/"}
+ }
+}
69 src/Philip/Action.php
@@ -0,0 +1,69 @@
+<?php
+
+namespace Philip;
+
+/**
+ * A terribly-named class for storing a callback action and a pattern
+ * that determines whether that callback should be executed.
+ *
+ * @author Bill Israel <bill.israel@gmail.com>
+ */
+class Action
+{
+ /** @var string $pattern A RegEx to compare a string against */
+ private $pattern;
+
+ /** @var callable $callback A function to call */
+ private $callback;
+
+ /** @var array $matches Matches from testing the $pattern */
+ private $matches = array();
+
+ /**
+ * Constructor.
+ *
+ * @param string $pattern A RegEx
+ * @param callable $callback A callable
+ */
+ public function __construct($pattern, $callback)
+ {
+ $this->pattern = $pattern;
+ $this->callback = $callback;
+ }
+
+ /**
+ * Tests the pattern against the given string.
+ *
+ * @param string $str The string to test.
+ * @return boolean True if the pattern matched anything, false otherwise.
+ */
+ public function isExecutable($str)
+ {
+ if ($this->pattern) {
+ return (bool) preg_match($this->pattern, $str, $this->matches);
+ }
+
+ return true;
+ }
+
+ /**
+ * Executes the given callback, returns the callback's return value.
+ *
+ * @param array $params An array of params to pass to the callback
+ */
+ public function executeCallback($params)
+ {
+ return call_user_func_array($this->callback, $params);
+ }
+
+ /**
+ * Get the array of matches from the pattern.
+ *
+ * @return array The matches
+ */
+ public function getMatches()
+ {
+ return array_slice($this->matches, 1);
+ }
+}
+
189 src/Philip/IRC/Request.php
@@ -0,0 +1,189 @@
+<?php
+
+namespace Philip\IRC;
+
+/**
+ * A representation of an IRC request message.
+ *
+ * @author Bill Israel <bill.israel@gmail.com>
+ */
+class Request
+{
+ // IRC Message Constants
+ private static $PREFIX = 1;
+ private static $COMMAND = 2;
+ private static $MIDDLE = 3;
+ private static $TRAILING = 4;
+
+ // IRC User Prefix Constants
+ private static $NICK = 1;
+ private static $USER = 2;
+ private static $HOST = 3;
+
+ // Saves 4 parts: <prefix> <command> <middle params> <trailing param>
+ private static $RE_MSG = '/^(?:[:@]([^\\s]+) )?([^\\s]+)(?: ((?:[^:\\s][^\\s]* ?)*))?(?: ?:(.*))?$/';
+
+ // Saves 3 parts: <nick> <username> <hostname>
+ private static $RE_SENDER = '/^([^!@]+)!(?:[ni]=)?([^@]+)@([^ ]+)$/';
+
+ // Member Vars
+ private $raw;
+ private $prefix;
+ private $cmd;
+ private $middle;
+ private $trailing;
+
+
+ /**
+ * Constructor.
+ *
+ * @param string $raw The raw IRC Request to parse
+ */
+ public function __construct($raw)
+ {
+ $this->raw = $raw;
+
+ $matches = array();
+ preg_match(self::$RE_MSG, $raw, $matches);
+
+ // Remove newlines and carriage returns
+ $count = count($matches);
+ for($i = $count - 1; $i >= 0; $i--) {
+ $matches[$i] = str_replace(array(chr(10), chr(13)), '', $matches[$i]);
+ }
+
+ if ($count) {
+ $this->prefix = $matches[self::$PREFIX];
+ $this->cmd = $matches[self::$COMMAND];
+ $this->middle = $matches[self::$MIDDLE] ? explode(' ', $matches[self::$MIDDLE]) : null;
+ $this->trailing = $matches[self::$TRAILING] ?: null;
+ }
+ }
+
+ /**
+ * Returns the sent command.
+ *
+ * @return string The IRC command in the request
+ */
+ public function getCommand()
+ {
+ return $this->cmd;
+ }
+
+ /**
+ * Returns the parameters from the request.
+ *
+ * @return array The parameters in the request (minus the trailing param)
+ */
+ public function getParams()
+ {
+ if (is_array($this->middle)) {
+ return $this->middle;
+ }
+
+ return array();
+ }
+
+ /**
+ * Returns the message portion of the request.
+ *
+ * @return string The message/trailing part of the request
+ */
+ public function getMessage()
+ {
+ if ($this->trailing) {
+ return $this->trailing;
+ }
+
+ return '';
+ }
+
+ /**
+ * Returns the source of the message. If it was a PM, the source
+ * will be a user's nick. If it was a message in a channel, it'll
+ * be the channel name.
+ *
+ * @return string The sending user's nick, or the channel name
+ */
+ public function getSource()
+ {
+ if ($this->isPrivateMessage()) {
+ return $this->getSendingUser();
+ }
+
+ return $this->middle[0];
+ }
+
+ /**
+ * Returns the sending user's nick, false otherwise.
+ *
+ * @return mixed The sending user's nick, or false if it wasn't sent by a user
+ */
+ public function getSendingUser()
+ {
+ if ($this->isFromUser()) {
+ $matches = array();
+ preg_match(self::$RE_SENDER, $this->prefix, $matches);
+
+ return $matches[self::$NICK];
+ }
+
+ return false;
+ }
+
+ /**
+ * Return the sending server if it was sent by a server, false otherwise.
+ *
+ * @return mixed The sending server, or false if it wasn't sent by a server
+ */
+ public function getServer()
+ {
+ if ($this->isFromServer()) {
+ return $this->prefix;
+ }
+
+ return false;
+ }
+
+ /**
+ * Returns true if the message is a private message.
+ *
+ * @return bool True if the message is a private one
+ */
+ public function isPrivateMessage()
+ {
+ return isset($this->middle[0]) && !$this->isChannel($this->middle[0]);
+ }
+
+ /**
+ * Returns true if the message was sent by a user.
+ *
+ * @return bool True if the request was from a user, false otherwise
+ */
+ public function isFromUser()
+ {
+ return (bool) preg_match(self::$RE_SENDER, $this->prefix);
+ }
+
+ /**
+ * Returns true if the message was sent from a server.
+ *
+ * @return bool True if the request was from a server, false otherwise
+ */
+ public function isFromSever()
+ {
+ return !$this->isFromUser();
+ }
+
+ /**
+ * Determines whether the given string is a channel name.
+ *
+ * @param string $str The string to test
+ * @return bool True if the string is a channel name, false otherwise
+ */
+ private function isChannel($str)
+ {
+ // Channels can start with #, &, !, or + (and have more than 1 of them)
+ return strspn($str, '#&!+', 0, 1) >= 1;
+ }
+}
154 src/Philip/IRC/Response.php
@@ -0,0 +1,154 @@
+<?php
+
+namespace Philip\IRC;
+
+/**
+ * A simplified representation of an IRC response object.
+ *
+ * @author Bill Israel <bill.israel@gmail.com>
+ */
+class Response
+{
+ /** @var array $args The IRC command response arguments */
+ private $args;
+
+ /**
+ * Constructor.
+ *
+ * @param string $cmd The IRC command to return
+ * @param mixed $args The arguments to send with it
+ */
+ private function __construct($cmd, $args = '')
+ {
+ if (!is_array($args)) {
+ $args = array($args);
+ }
+
+ $args = array_map(function($x) { return trim($x); }, $args);
+ $end = count($args) - 1;
+ $args[$end] = ':' . $args[$end];
+
+ $this->args = $args;
+ array_unshift($this->args, strtoupper($cmd));
+ }
+
+ /**
+ * Creates a PONG response.
+ *
+ * @param string $host The host string to send back
+ * @return Response An IRC Response object
+ */
+ public static function pong($host)
+ {
+ return new self('PONG', $host);
+ }
+
+ /**
+ * Creates a QUIT response.
+ *
+ * @param string $msg The quitting message
+ * @return Response An IRC Response object
+ */
+ public static function quit($msg)
+ {
+ return new self('QUIT', $msg);
+ }
+
+ /**
+ * Creates a JOIN response.
+ *
+ * @param string $channels The channels to join
+ * @return Response An IRC Response object
+ */
+ public static function join($channels)
+ {
+ return new self('JOIN', $channels);
+ }
+
+ /**
+ * Creates a PART response.
+ *
+ * @param string $channels The channels to leave
+ * @return Response An IRC Response object
+ */
+ public static function leave($channels)
+ {
+ return new self('PART', $channels);
+ }
+
+ /**
+ * Creates a PRIVMSG response.
+ *
+ * @param string $who The channel/nick to send this msg to
+ * @param string $what The messages to send
+ * @return Response An IRC Response object
+ */
+ public static function msg($who, $what)
+ {
+ return new self('PRIVMSG', array($who, $what));
+ }
+
+ /**
+ * Creates a NOTICE response.
+ *
+ * @param string $channel The channel to send the notice to.
+ * @return Response An IRC Response object
+ */
+ public static function notice($channel, $msg)
+ {
+ return new self('NOTICE', array($channel, $msg));
+ }
+
+ /**
+ * Creates a ACTION response.
+ *
+ * @param string $channel The channel to send the action to.
+ * @return Response An IRC Response object
+ */
+ public static function action($channel, $msg)
+ {
+ // ACTION isn't really part of the IRC spec, it's kind of an agreement between client devs.
+ // An ACTION is just a standard PRIVMSG whose message starts with HEX 01 byte, followed by
+ // "ACTION", then the message, and ends with a HEX 01 byte.
+ //
+ // See also:
+ // http://www.dreamincode.net/forums/topic/85216-irc-action/page__p__535748&#entry535748
+ $msg = "\x01ACTION $msg\x01";
+ return new self('PRIVMSG', array($channel, $msg));
+ }
+
+ /**
+ * Creates a USER response.
+ *
+ * @param string $nick The bot's nickname
+ * @param string $host The IRC host to connect to
+ * @param string $server The server name
+ * @param string $realname The bot's "real name"
+ * @return Response An IRC Response object
+ */
+ public static function user($nick, $host, $server, $realname)
+ {
+ return new self('USER', array($nick, $host, $server, $realname));
+ }
+
+ /**
+ * Creates a NICK response.
+ *
+ * @param string $nick The nickname to set
+ * @return Response An IRC Response object
+ */
+ public static function nick($nick)
+ {
+ return new self('NICK', $nick);
+ }
+
+ /**
+ * Stringify this object.
+ *
+ * @return string The string representation of the response
+ */
+ public function __toString()
+ {
+ return implode(' ', $this->args);
+ }
+}
329 src/Philip/Philip.php
@@ -0,0 +1,329 @@
+<?php
+
+namespace Philip;
+
+use Philip\Action,
+ Philip\IRC\Request,
+ Philip\IRC\Response;
+
+/**
+ * A Slim-inspired IRC bot.
+ *
+ * @author Bill Israel <bill.israel@gmail.com>
+ */
+class Philip
+{
+ /** @var array $config The bot's configuration */
+ private $config;
+
+ /** @var resource $socket The socket for communicating with the IRC server */
+ private $socket;
+
+ /** @var array $events Events and their handlers */
+ private $events;
+
+ /**
+ * Constructor.
+ *
+ * @param array $config The configuration for the bot
+ */
+ public function __construct($config = array())
+ {
+ $this->config = $config;
+ $this->events = array(
+ 'privmsg.channel' => array(),
+ 'privmsg.private' => array(),
+ 'ping' => array(),
+ 'join' => array(),
+ 'part' => array(),
+ 'error' => array(),
+ 'notice' => array(),
+ );
+
+ $this->addDefaultHandlers();
+ }
+
+ /**
+ * Destructor; ensure the socket gets closed.
+ */
+ public function __destruct()
+ {
+ if (isset($this->socket)) {
+ fclose($this->socket);
+ }
+ }
+
+
+ /**
+ * Adds an event handler to the list for when someone talks in a channel.
+ *
+ * @param string $pattern The RegEx to test the message against
+ * @param callable $callback The callback to run if the pattern matches
+ */
+ public function onChannel($pattern, $callback)
+ {
+ $this->onEvent('privmsg.channel', new Action($pattern, $callback));
+ }
+
+ /**
+ * Adds an event handler to the list when private messages come in.
+ *
+ * @param string $pattern The RegEx to test the message against
+ * @param callable $callback The callback to run if the pattern matches
+ */
+ public function onPrivateMessage($pattern, $callback)
+ {
+ $this->onEvent('privmsg.private', new Action($pattern, $callback));
+ }
+
+ /**
+ * Adds event handlers to the list for both channel messages and private messages.
+ *
+ * @param string $pattern The RegEx to test the message against
+ * @param callable $callback The callback to run if the pattern matches
+ */
+ public function onMessages($pattern, $callback)
+ {
+ $this->onEvent('privmsg.channel', new Action($pattern, $callback));
+ $this->onEvent('privmsg.private', new Action($pattern, $callback));
+ }
+
+ /**
+ * Adds event handlers to the list for JOIN messages.
+ *
+ * @param callable $callback The callback to run if the pattern matches
+ */
+ public function onJoin($callback)
+ {
+ $this->onEvent('join', new Action(null, $callback));
+ }
+
+ /**
+ * Adds event handlers to the list for PART messages.
+ *
+ * @param callable $callback The callback to run if the pattern matches
+ */
+ public function onPart($callback)
+ {
+ $this->onEvent('part', new Action(null, $callback));
+ }
+
+ /**
+ * Adds event handlers to the list for ERROR messages.
+ *
+ * @param callable $callback The callback to run if the pattern matches
+ */
+ public function onError($callback)
+ {
+ $this->onEvent('error', new Action(null, $callback));
+ }
+
+ /**
+ * Adds event handlers to the list for NOTICE messages.
+ *
+ * @param callable $callback The callback to run if the pattern matches
+ */
+ public function onNotice($callback)
+ {
+ $this->onEvent('notice', new Action(null, $callback));
+ }
+
+ /**
+ * Return the configuration so plugins and external things can use it.
+ *
+ * @return array The bot's current configuration
+ */
+ public function getConfig()
+ {
+ return $this->config;
+ }
+
+ /**
+ * Loads a plugin. See the README for plugin documentation.
+ *
+ * @param string $name The name of the plugin to load
+ */
+ public function loadPlugin($name)
+ {
+ $plugin_class = $name . 'Plugin';
+ $path = 'plugins/' . $plugin_class . '.php';
+ if (file_exists($path)) {
+ require($path);
+ }
+
+ $n = "\\" . $plugin_class;
+ $plugin = new $n($this);
+ $plugin->init();
+ }
+
+ /**
+ * Loads multiple plugins in a single call.
+ *
+ * @param array $names The names of the plugins to load.
+ */
+ public function loadPlugins($names)
+ {
+ foreach ($names as $name) {
+ $this->loadPlugin($name);
+ }
+ }
+
+ /**
+ * Determins if the given user is an admin.
+ *
+ * @param string $user The username to test
+ * @return boolean True if the user is an admin, false otherwise
+ */
+ public function isAdmin($user) {
+ return in_array($user, $this->config['admins']);
+ }
+
+ /**
+ * Starts the IRC bot.
+ */
+ public function run()
+ {
+ if ($this->connect()) {
+ $this->login();
+ $this->join();
+ $this->listen();
+ }
+ }
+
+ /**
+ * Adds an action to the list of possible actions when an event is fired.
+ *
+ * @param string $event The Event to listen for
+ * @param Action $action The action to run when the event is fired
+ */
+ private function onEvent($event, $action)
+ {
+ $this->events[$event][] = $action;
+ }
+
+ /**
+ * Connects to the IRC server.
+ *
+ * @return boolean True if the socket was created successfully
+ */
+ private function connect()
+ {
+ stream_set_blocking(STDIN, 0);
+ $this->socket = fsockopen($this->config['hostname'], $this->config['port']);
+ return (bool) $this->socket;
+ }
+
+ /**
+ * Logs in to the IRC server with the user info in the config.
+ */
+ private function login()
+ {
+ $this->send(Response::nick($this->config['nick']));
+ $this->send(Response::user(
+ $this->config['nick'],
+ $this->config['hostname'],
+ $this->config['servername'],
+ $this->config['realname']
+ ));
+ }
+
+ /**
+ * Joins the channels specified in the config.
+ */
+ private function join()
+ {
+ if (!is_array($this->config['channels'])) {
+ $this->config['channels'] = array($this->config['channels']);
+ }
+
+ foreach ($this->config['channels'] as $channel) {
+ $this->send(Response::join($channel));
+ }
+ }
+
+ /**
+ * Driver of the bot; listens for messages, responds to them accordingly.
+ */
+ private function listen()
+ {
+ do {
+ $data = fgets($this->socket, 512);
+ if (!empty($data)) {
+ $req = $this->receive($data);
+ $event = strtolower($req->getCommand());
+ $msg = $req->getMessage();
+
+ if ($event === 'privmsg') {
+ $event .= $req->isPrivateMessage() ? '.private' : '.channel';
+ }
+
+ // Skip processing anything if the event is unknown or the user sending
+ // the message is actually the bot
+ if (!isset($this->events[$event]) || ($req->getSendingUser() === $this->config['nick'])) {
+ continue;
+ }
+
+ $responses = array();
+ foreach($this->events[$event] as $action) {
+ if ($action->isExecutable($msg)) {
+ if ($response = $action->executeCallback(array($req, $action->getMatches()))) {
+ $responses[] = $response;
+ }
+ }
+ }
+
+ if (!empty($responses)) {
+ $this->send($responses);
+ }
+ }
+ } while (!feof($this->socket));
+ }
+
+ /**
+ * Convert the raw incoming IRC message into a Request object
+ *
+ * @param string $raw The unparsed incoming IRC message
+ * @return Request The parsed message
+ */
+ private function receive($raw)
+ {
+ fwrite(STDOUT, '--> ' . $raw);
+ return new Request($raw);
+ }
+
+ /**
+ * Actually push data back into the socket (giggity).
+ *
+ * @param array $responses The responses to send back to the server
+ */
+ private function send($responses)
+ {
+ if (!is_array($responses)) {
+ $responses = array($responses);
+ }
+
+ foreach ($responses as $response) {
+ fwrite($this->socket, ($response . "\r\n"));
+ fwrite(STDOUT, '<-- ' . $response . PHP_EOL);
+ }
+ }
+
+ /**
+ * Loads default event handlers for basic IRC commands.
+ */
+ private function addDefaultHandlers()
+ {
+ // When the server PINGs us, just respond with PONG and the server's host
+ $pingAction = new Action(null, function($request, $params) {
+ return Response::pong($request->getMessage());
+ });
+
+ // If an Error message is encountered, just log it for now.
+ $errorAction = new Action(null, function($request, $params) {
+ fwrite(STDOUT, "ERROR: {$request->getMessage()}" . PHP_EOL);
+ });
+
+ $this->onEvent('ping', $pingAction);
+ $this->onEvent('error', $errorAction);
+ }
+}

0 comments on commit 40bf7ab

Please sign in to comment.
Something went wrong with that request. Please try again.