]> git.mxchange.org Git - quix0rs-gnu-social.git/commitdiff
Merge remote branch 'statusnet/1.0.x' into idle-irc-plugin
authorLuke Fitzgerald <lw.fitzgerald@googlemail.com>
Wed, 11 Aug 2010 02:48:49 +0000 (19:48 -0700)
committerLuke Fitzgerald <lw.fitzgerald@googlemail.com>
Wed, 11 Aug 2010 02:48:49 +0000 (19:48 -0700)
124 files changed:
plugins/Irc/ChannelResponseChannel.php [new file with mode: 0644]
plugins/Irc/Fake_Irc.php [new file with mode: 0644]
plugins/Irc/IrcPlugin.php [new file with mode: 0644]
plugins/Irc/Irc_waiting_message.php [new file with mode: 0644]
plugins/Irc/README [new file with mode: 0644]
plugins/Irc/extlib/.gitignore [new file with mode: 0644]
plugins/Irc/extlib/phergie/.gitignore [new file with mode: 0644]
plugins/Irc/extlib/phergie/LICENSE [new file with mode: 0644]
plugins/Irc/extlib/phergie/Phergie/Autoload.php [new file with mode: 0755]
plugins/Irc/extlib/phergie/Phergie/Bot.php [new file with mode: 0755]
plugins/Irc/extlib/phergie/Phergie/Config.php [new file with mode: 0755]
plugins/Irc/extlib/phergie/Phergie/Config/Exception.php [new file with mode: 0644]
plugins/Irc/extlib/phergie/Phergie/Connection.php [new file with mode: 0755]
plugins/Irc/extlib/phergie/Phergie/Connection/Exception.php [new file with mode: 0644]
plugins/Irc/extlib/phergie/Phergie/Connection/Handler.php [new file with mode: 0644]
plugins/Irc/extlib/phergie/Phergie/Driver/Abstract.php [new file with mode: 0755]
plugins/Irc/extlib/phergie/Phergie/Driver/Exception.php [new file with mode: 0755]
plugins/Irc/extlib/phergie/Phergie/Driver/Statusnet.php [new file with mode: 0644]
plugins/Irc/extlib/phergie/Phergie/Driver/Streams.php [new file with mode: 0755]
plugins/Irc/extlib/phergie/Phergie/Event/Abstract.php [new file with mode: 0644]
plugins/Irc/extlib/phergie/Phergie/Event/Command.php [new file with mode: 0644]
plugins/Irc/extlib/phergie/Phergie/Event/Exception.php [new file with mode: 0644]
plugins/Irc/extlib/phergie/Phergie/Event/Handler.php [new file with mode: 0644]
plugins/Irc/extlib/phergie/Phergie/Event/Request.php [new file with mode: 0755]
plugins/Irc/extlib/phergie/Phergie/Event/Response.php [new file with mode: 0755]
plugins/Irc/extlib/phergie/Phergie/Exception.php [new file with mode: 0755]
plugins/Irc/extlib/phergie/Phergie/Hostmask.php [new file with mode: 0755]
plugins/Irc/extlib/phergie/Phergie/Hostmask/Exception.php [new file with mode: 0644]
plugins/Irc/extlib/phergie/Phergie/Plugin/Abstract.php [new file with mode: 0755]
plugins/Irc/extlib/phergie/Phergie/Plugin/Acl.php [new file with mode: 0755]
plugins/Irc/extlib/phergie/Phergie/Plugin/AltNick.php [new file with mode: 0755]
plugins/Irc/extlib/phergie/Phergie/Plugin/AudioScrobbler.php [new file with mode: 0755]
plugins/Irc/extlib/phergie/Phergie/Plugin/AutoJoin.php [new file with mode: 0755]
plugins/Irc/extlib/phergie/Phergie/Plugin/Beer.php [new file with mode: 0644]
plugins/Irc/extlib/phergie/Phergie/Plugin/Beer/db.php [new file with mode: 0644]
plugins/Irc/extlib/phergie/Phergie/Plugin/BeerScore.php [new file with mode: 0644]
plugins/Irc/extlib/phergie/Phergie/Plugin/Cache.php [new file with mode: 0644]
plugins/Irc/extlib/phergie/Phergie/Plugin/Caffeine.php [new file with mode: 0644]
plugins/Irc/extlib/phergie/Phergie/Plugin/Caffeine/db.php [new file with mode: 0644]
plugins/Irc/extlib/phergie/Phergie/Plugin/Censor.php [new file with mode: 0755]
plugins/Irc/extlib/phergie/Phergie/Plugin/Cocktail.php [new file with mode: 0644]
plugins/Irc/extlib/phergie/Phergie/Plugin/Cocktail/db.php [new file with mode: 0644]
plugins/Irc/extlib/phergie/Phergie/Plugin/Command.php [new file with mode: 0644]
plugins/Irc/extlib/phergie/Phergie/Plugin/Cookie.php [new file with mode: 0644]
plugins/Irc/extlib/phergie/Phergie/Plugin/Cookie/db.php [new file with mode: 0644]
plugins/Irc/extlib/phergie/Phergie/Plugin/Cron.php [new file with mode: 0644]
plugins/Irc/extlib/phergie/Phergie/Plugin/Ctcp.php [new file with mode: 0755]
plugins/Irc/extlib/phergie/Phergie/Plugin/Daddy.php [new file with mode: 0644]
plugins/Irc/extlib/phergie/Phergie/Plugin/Encoding.php [new file with mode: 0644]
plugins/Irc/extlib/phergie/Phergie/Plugin/Exception.php [new file with mode: 0755]
plugins/Irc/extlib/phergie/Phergie/Plugin/Google.php [new file with mode: 0644]
plugins/Irc/extlib/phergie/Phergie/Plugin/Handler.php [new file with mode: 0755]
plugins/Irc/extlib/phergie/Phergie/Plugin/Help.php [new file with mode: 0644]
plugins/Irc/extlib/phergie/Phergie/Plugin/Http.php [new file with mode: 0644]
plugins/Irc/extlib/phergie/Phergie/Plugin/Http/Response.php [new file with mode: 0644]
plugins/Irc/extlib/phergie/Phergie/Plugin/Ideone.php [new file with mode: 0644]
plugins/Irc/extlib/phergie/Phergie/Plugin/Invisible.php [new file with mode: 0755]
plugins/Irc/extlib/phergie/Phergie/Plugin/Iterator.php [new file with mode: 0644]
plugins/Irc/extlib/phergie/Phergie/Plugin/Join.php [new file with mode: 0755]
plugins/Irc/extlib/phergie/Phergie/Plugin/Karma.php [new file with mode: 0644]
plugins/Irc/extlib/phergie/Phergie/Plugin/Lart.php [new file with mode: 0644]
plugins/Irc/extlib/phergie/Phergie/Plugin/Message.php [new file with mode: 0644]
plugins/Irc/extlib/phergie/Phergie/Plugin/NickServ.php [new file with mode: 0644]
plugins/Irc/extlib/phergie/Phergie/Plugin/Part.php [new file with mode: 0755]
plugins/Irc/extlib/phergie/Phergie/Plugin/Php.php [new file with mode: 0644]
plugins/Irc/extlib/phergie/Phergie/Plugin/Php/Source.php [new file with mode: 0644]
plugins/Irc/extlib/phergie/Phergie/Plugin/Php/Source/Local.php [new file with mode: 0644]
plugins/Irc/extlib/phergie/Phergie/Plugin/Ping.php [new file with mode: 0755]
plugins/Irc/extlib/phergie/Phergie/Plugin/Pong.php [new file with mode: 0755]
plugins/Irc/extlib/phergie/Phergie/Plugin/Prioritize.php [new file with mode: 0755]
plugins/Irc/extlib/phergie/Phergie/Plugin/Puppet.php [new file with mode: 0644]
plugins/Irc/extlib/phergie/Phergie/Plugin/Quit.php [new file with mode: 0755]
plugins/Irc/extlib/phergie/Phergie/Plugin/Reload.php [new file with mode: 0755]
plugins/Irc/extlib/phergie/Phergie/Plugin/Remind.php [new file with mode: 0644]
plugins/Irc/extlib/phergie/Phergie/Plugin/Serve.php [new file with mode: 0755]
plugins/Irc/extlib/phergie/Phergie/Plugin/SpellCheck.php [new file with mode: 0644]
plugins/Irc/extlib/phergie/Phergie/Plugin/Statusnet.php [new file with mode: 0644]
plugins/Irc/extlib/phergie/Phergie/Plugin/Tea.php [new file with mode: 0644]
plugins/Irc/extlib/phergie/Phergie/Plugin/Tea/db.php [new file with mode: 0644]
plugins/Irc/extlib/phergie/Phergie/Plugin/Temperature.php [new file with mode: 0644]
plugins/Irc/extlib/phergie/Phergie/Plugin/TerryChay.php [new file with mode: 0644]
plugins/Irc/extlib/phergie/Phergie/Plugin/TheFuckingWeather.php [new file with mode: 0644]
plugins/Irc/extlib/phergie/Phergie/Plugin/Time.php [new file with mode: 0644]
plugins/Irc/extlib/phergie/Phergie/Plugin/Tld.php [new file with mode: 0644]
plugins/Irc/extlib/phergie/Phergie/Plugin/Tld/db.php [new file with mode: 0644]
plugins/Irc/extlib/phergie/Phergie/Plugin/Twitter.php [new file with mode: 0644]
plugins/Irc/extlib/phergie/Phergie/Plugin/Twitter/laconica.class.php [new file with mode: 0644]
plugins/Irc/extlib/phergie/Phergie/Plugin/Twitter/twitter.class.php [new file with mode: 0644]
plugins/Irc/extlib/phergie/Phergie/Plugin/Url.php [new file with mode: 0644]
plugins/Irc/extlib/phergie/Phergie/Plugin/Url/Shorten/Abstract.php [new file with mode: 0644]
plugins/Irc/extlib/phergie/Phergie/Plugin/Url/Shorten/Trim.php [new file with mode: 0644]
plugins/Irc/extlib/phergie/Phergie/Plugin/UserInfo.php [new file with mode: 0644]
plugins/Irc/extlib/phergie/Phergie/Plugin/Weather.php [new file with mode: 0644]
plugins/Irc/extlib/phergie/Phergie/Plugin/Wine.php [new file with mode: 0644]
plugins/Irc/extlib/phergie/Phergie/Plugin/Wine/db.php [new file with mode: 0644]
plugins/Irc/extlib/phergie/Phergie/Plugin/Youtube.php [new file with mode: 0644]
plugins/Irc/extlib/phergie/Phergie/Process/Abstract.php [new file with mode: 0755]
plugins/Irc/extlib/phergie/Phergie/Process/Async.php [new file with mode: 0644]
plugins/Irc/extlib/phergie/Phergie/Process/Exception.php [new file with mode: 0755]
plugins/Irc/extlib/phergie/Phergie/Process/Standard.php [new file with mode: 0644]
plugins/Irc/extlib/phergie/Phergie/StatusnetBot.php [new file with mode: 0644]
plugins/Irc/extlib/phergie/Phergie/Tools/LogViewer/INSTALL [new file with mode: 0755]
plugins/Irc/extlib/phergie/Phergie/Tools/LogViewer/index.php [new file with mode: 0755]
plugins/Irc/extlib/phergie/Phergie/Tools/README [new file with mode: 0755]
plugins/Irc/extlib/phergie/Phergie/Ui/Abstract.php [new file with mode: 0644]
plugins/Irc/extlib/phergie/Phergie/Ui/Console.php [new file with mode: 0644]
plugins/Irc/extlib/phergie/PhergiePackageTask.php [new file with mode: 0644]
plugins/Irc/extlib/phergie/README [new file with mode: 0644]
plugins/Irc/extlib/phergie/Settings.php.dist [new file with mode: 0755]
plugins/Irc/extlib/phergie/Tests/Phergie/Plugin/HandlerTest.php [new file with mode: 0644]
plugins/Irc/extlib/phergie/Tests/Phergie/Plugin/IteratorTest.php [new file with mode: 0644]
plugins/Irc/extlib/phergie/Tests/Phergie/Plugin/Mock.php [new file with mode: 0755]
plugins/Irc/extlib/phergie/Tests/Phergie/Plugin/PingTest.php [new file with mode: 0644]
plugins/Irc/extlib/phergie/Tests/Phergie/Plugin/PongTest.php [new file with mode: 0644]
plugins/Irc/extlib/phergie/Tests/Phergie/Plugin/SpellCheckTest.php [new file with mode: 0644]
plugins/Irc/extlib/phergie/Tests/Phergie/Plugin/TerryChayTest.php [new file with mode: 0644]
plugins/Irc/extlib/phergie/Tests/Phergie/Plugin/TestCase.php [new file with mode: 0644]
plugins/Irc/extlib/phergie/Tests/Phergie/Plugin/TestNonInstantiablePluginFromFile.php [new file with mode: 0755]
plugins/Irc/extlib/phergie/Tests/TestHelper.php [new file with mode: 0644]
plugins/Irc/extlib/phergie/Tests/phpunit.xml [new file with mode: 0644]
plugins/Irc/extlib/phergie/build.xml [new file with mode: 0644]
plugins/Irc/extlib/phergie/phergie.bat [new file with mode: 0644]
plugins/Irc/extlib/phergie/phergie.php [new file with mode: 0755]
plugins/Irc/ircmanager.php [new file with mode: 0644]

diff --git a/plugins/Irc/ChannelResponseChannel.php b/plugins/Irc/ChannelResponseChannel.php
new file mode 100644 (file)
index 0000000..d29a1da
--- /dev/null
@@ -0,0 +1,61 @@
+<?php\r
+/**\r
+ * StatusNet, the distributed open-source microblogging tool\r
+ *\r
+ * Extend the IMChannel class to allow commands to send messages\r
+ * to a channel instead of PMing a user\r
+ *\r
+ * PHP version 5\r
+ *\r
+ * LICENCE: This program is free software: you can redistribute it and/or modify\r
+ * it under the terms of the GNU Affero General Public License as published by\r
+ * the Free Software Foundation, either version 3 of the License, or\r
+ * (at your option) any later version.\r
+ *\r
+ * This program is distributed in the hope that it will be useful,\r
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of\r
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\r
+ * GNU Affero General Public License for more details.\r
+ *\r
+ * You should have received a copy of the GNU Affero General Public License\r
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.\r
+ *\r
+ * @category  Network\r
+ * @package   StatusNet\r
+ * @author    Luke Fitzgerald <lw.fitzgerald@googlemail.com>\r
+ * @copyright 2010 StatusNet, Inc.\r
+ * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0\r
+ * @link      http://status.net/\r
+ */\r
+\r
+if (!defined('STATUSNET') && !defined('LACONICA')) {\r
+    exit(1);\r
+}\r
+\r
+class ChannelResponseChannel extends IMChannel {\r
+    protected $ircChannel;\r
+\r
+    /**\r
+    * Construct a ChannelResponseChannel\r
+    *\r
+    * @param IMplugin $imPlugin IMPlugin\r
+    * @param string $ircChannel IRC Channel to reply to\r
+    * @return ChannelResponseChannel\r
+    */\r
+    public function __construct($imPlugin, $ircChannel) {\r
+        $this->ircChannel = $ircChannel;\r
+        parent::__construct($imPlugin);\r
+    }\r
+\r
+    /**\r
+    * Send a message using the plugin\r
+    *\r
+    * @param User $user User\r
+    * @param string $text Message text\r
+    * @return void\r
+    */\r
+    public function output($user, $text) {\r
+        $text = $user->nickname.': ['.common_config('site', 'name') . '] ' . $text;\r
+        $this->imPlugin->send_message($this->ircChannel, $text);\r
+    }\r
+}
\ No newline at end of file
diff --git a/plugins/Irc/Fake_Irc.php b/plugins/Irc/Fake_Irc.php
new file mode 100644 (file)
index 0000000..d95ab04
--- /dev/null
@@ -0,0 +1,47 @@
+<?php
+/**
+ * StatusNet, the distributed open-source microblogging tool
+ *
+ * Instead of sending IRC messages, retrieve the raw data that would be sent
+ *
+ * PHP version 5
+ *
+ * LICENCE: This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 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 Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @category  Network
+ * @package   StatusNet
+ * @author    Luke Fitzgerald <lw.fitzgerald@googlemail.com>
+ * @copyright 2010 StatusNet, Inc.
+ * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link      http://status.net/
+ */
+
+if (!defined('STATUSNET') && !defined('LACONICA')) {
+    exit(1);
+}
+
+class Fake_Irc extends Phergie_Driver_Streams {
+    public $would_be_sent = null;
+
+    /**
+    * Store the components for sending a command
+    *
+    * @param string $command Command
+    * @param array $args Arguments
+    * @return void
+    */
+    protected function send($command, $args = '') {
+        $this->would_be_sent = array('command' => $command, 'args' => $args);
+    }
+}
\ No newline at end of file
diff --git a/plugins/Irc/IrcPlugin.php b/plugins/Irc/IrcPlugin.php
new file mode 100644 (file)
index 0000000..e073d6f
--- /dev/null
@@ -0,0 +1,392 @@
+<?php
+/**
+ * StatusNet - the distributed open-source microblogging tool
+ * Copyright (C) 2010, StatusNet, Inc.
+ *
+ * Send and receive notices using an IRC network
+ *
+ * PHP version 5
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 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 Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @category  IM
+ * @package   StatusNet
+ * @author    Luke Fitzgerald <lw.fitzgerald@googlemail.com>
+ * @copyright 2010 StatusNet, Inc.
+ * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
+ * @link      http://status.net/
+ */
+
+if (!defined('STATUSNET')) {
+    // This check helps protect against security problems;
+    // your code file can't be executed directly from the web.
+    exit(1);
+}
+
+// We bundle the Phergie library...
+set_include_path(get_include_path() . PATH_SEPARATOR . dirname(__FILE__) . '/extlib/phergie');
+
+/**
+ * Plugin for IRC
+ *
+ * @category  Plugin
+ * @package   StatusNet
+ * @author    Luke Fitzgerald <lw.fitzgerald@googlemail.com>
+ * @copyright 2010 StatusNet, Inc.
+ * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
+ * @link      http://status.net/
+ */
+
+class IrcPlugin extends ImPlugin {
+    public $host =  null;
+    public $port = null;
+    public $username = null;
+    public $realname = null;
+    public $nick = null;
+    public $password = null;
+    public $nickservidentifyregexp = null;
+    public $nickservpassword = null;
+    public $channels = null;
+    public $transporttype = null;
+    public $encoding = null;
+
+    public $regcheck = null;
+    public $unregregexp = null;
+    public $regregexp = null;
+
+    public $transport = 'irc';
+    protected $whiteList;
+    protected $fake_irc;
+
+    /**
+     * Get the internationalized/translated display name of this IM service
+     *
+     * @return string Name of service
+     */
+    public function getDisplayName() {
+        return _m('IRC');
+    }
+
+    /**
+     * Normalize a screenname for comparison
+     *
+     * @param string $screenname Screenname to normalize
+     * @return string An equivalent screenname in normalized form
+     */
+    public function normalize($screenname) {
+        $screenname = str_replace(" ","", $screenname);
+        return strtolower($screenname);
+    }
+
+    /**
+     * Get the screenname of the daemon that sends and receives messages
+     *
+     * @return string Screenname
+     */
+    public function daemon_screenname() {
+        return $this->nick;
+    }
+
+    /**
+     * Validate (ensure the validity of) a screenname
+     *
+     * @param string $screenname Screenname to validate
+     * @return boolean true if screenname is valid
+     */
+    public function validate($screenname) {
+        if (preg_match('/\A[a-z0-9\-_]{1,1000}\z/i', $screenname)) {
+            return true;
+        } else {
+            return false;
+        }
+    }
+
+    /**
+     * Load related modules when needed
+     *
+     * @param string $cls Name of the class to be loaded
+     * @return boolean hook value; true means continue processing, false means stop.
+     */
+    public function onAutoload($cls) {
+        $dir = dirname(__FILE__);
+
+        switch ($cls) {
+            case 'IrcManager':
+                include_once $dir . '/'.strtolower($cls).'.php';
+                return false;
+            case 'Fake_Irc':
+            case 'Irc_waiting_message':
+            case 'ChannelResponseChannel':
+                include_once $dir . '/'. $cls .'.php';
+                return false;
+            default:
+                if (substr($cls, 0, 7) == 'Phergie') {
+                    include_once str_replace('_', DIRECTORY_SEPARATOR, $cls) . '.php';
+                    return false;
+                }
+                return true;
+        }
+    }
+
+    /*
+     * Start manager on daemon start
+     *
+     * @param array &$versions Array to insert manager into
+     * @return boolean
+     */
+    public function onStartImDaemonIoManagers(&$classes) {
+        parent::onStartImDaemonIoManagers(&$classes);
+        $classes[] = new IrcManager($this); // handles sending/receiving
+        return true;
+    }
+
+    /**
+    * Ensure the database table is present
+    *
+    */
+    public function onCheckSchema() {
+        $schema = Schema::get();
+
+        // For storing messages while sessions become ready
+        $schema->ensureTable('irc_waiting_message',
+                             array(new ColumnDef('id', 'integer', null,
+                                                 false, 'PRI', null, null, true),
+                                   new ColumnDef('data', 'blob', null, false),
+                                   new ColumnDef('prioritise', 'tinyint', 1, false),
+                                   new ColumnDef('attempts', 'integer', null, false),
+                                   new ColumnDef('created', 'datetime', null, false),
+                                   new ColumnDef('claimed', 'datetime')));
+
+        return true;
+    }
+
+    /**
+    * Get a microid URI for the given screenname
+    *
+    * @param string $screenname Screenname
+    * @return string microid URI
+    */
+    public function microiduri($screenname) {
+        return 'irc:' . $screenname;
+    }
+
+    /**
+     * Send a message to a given screenname
+     *
+     * @param string $screenname Screenname to send to
+     * @param string $body Text to send
+     * @return boolean true on success
+     */
+    public function send_message($screenname, $body) {
+        $lines = explode("\n", $body);
+        foreach ($lines as $line) {
+            $this->fake_irc->doPrivmsg($screenname, $line);
+            $this->enqueue_outgoing_raw(array('type' => 'message', 'prioritise' => 0, 'data' => $this->fake_irc->would_be_sent));
+        }
+        return true;
+    }
+
+    /**
+     * Accept a queued input message.
+     *
+     * @return boolean true if processing completed, false if message should be reprocessed
+     */
+    public function receive_raw_message($data) {
+        if (strpos($data['source'], '#') === 0) {
+            $message = $data['message'];
+            $parts = explode(' ', $message, 2);
+            $command = $parts[0];
+            if (in_array($command, $this->whiteList)) {
+                $this->handle_channel_incoming($data['sender'], $data['source'], $message);
+            } else {
+                $this->handle_incoming($data['sender'], $message);
+            }
+        } else {
+            $this->handle_incoming($data['sender'], $data['message']);
+        }
+        return true;
+    }
+
+    /**
+     * Helper for handling incoming messages from a channel requiring response
+     * to the channel instead of via PM
+     *
+     * @param string $nick Screenname the message was sent from
+     * @param string $channel Channel the message originated from
+     * @param string $message Message text
+     * @param boolean true on success
+     */
+    protected function handle_channel_incoming($nick, $channel, $notice_text) {
+        $user = $this->get_user($nick);
+        // For common_current_user to work
+        global $_cur;
+        $_cur = $user;
+
+        if (!$user) {
+            $this->send_from_site($nick, 'Unknown user; go to ' .
+                             common_local_url('imsettings') .
+                             ' to add your address to your account');
+            common_log(LOG_WARNING, 'Message from unknown user ' . $nick);
+            return;
+        }
+        if ($this->handle_channel_command($user, $channel, $notice_text)) {
+            common_log(LOG_INFO, "Command message by $nick handled.");
+            return;
+        } else if ($this->is_autoreply($notice_text)) {
+            common_log(LOG_INFO, 'Ignoring auto reply from ' . $nick);
+            return;
+        } else if ($this->is_otr($notice_text)) {
+            common_log(LOG_INFO, 'Ignoring OTR from ' . $nick);
+            return;
+        } else {
+            common_log(LOG_INFO, 'Posting a notice from ' . $user->nickname);
+            $this->add_notice($nick, $user, $notice_text);
+        }
+
+        $user->free();
+        unset($user);
+        unset($_cur);
+        unset($message);
+    }
+
+    /**
+     * Attempt to handle a message from a channel as a command
+     *
+     * @param User $user User the message is from
+     * @param string $channel Channel the message originated from
+     * @param string $body Message text
+     * @return boolean true if the message was a command and was executed, false if it was not a command
+     */
+    protected function handle_channel_command($user, $channel, $body) {
+        $inter = new CommandInterpreter();
+        $cmd = $inter->handle_command($user, $body);
+        if ($cmd) {
+            $chan = new ChannelResponseChannel($this, $channel);
+            $cmd->execute($chan);
+            return true;
+        } else {
+            return false;
+        }
+    }
+
+    /**
+     * Send a confirmation code to a user
+     *
+     * @param string $screenname screenname sending to
+     * @param string $code the confirmation code
+     * @param User $user user sending to
+     * @return boolean success value
+     */
+    public function send_confirmation_code($screenname, $code, $user, $checked = false) {
+        $body = sprintf(_('User "%s" on %s has said that your %s screenname belongs to them. ' .
+          'If that\'s true, you can confirm by clicking on this URL: ' .
+          '%s' .
+          ' . (If you cannot click it, copy-and-paste it into the ' .
+          'address bar of your browser). If that user isn\'t you, ' .
+          'or if you didn\'t request this confirmation, just ignore this message.'),
+          $user->nickname, common_config('site', 'name'), $this->getDisplayName(), common_local_url('confirmaddress', array('code' => $code)));
+
+        if ($this->regcheck && !$checked) {
+            return $this->checked_send_confirmation_code($screenname, $code, $user);
+        } else {
+            return $this->send_message($screenname, $body);
+        }
+    }
+
+    /**
+    * Only sends the confirmation message if the nick is
+    * registered
+    *
+    * @param string $screenname Screenname sending to
+    * @param string $code The confirmation code
+    * @param User $user User sending to
+    * @return boolean true on succes
+    */
+    public function checked_send_confirmation_code($screenname, $code, $user) {
+        $this->fake_irc->doPrivmsg('NickServ', 'INFO '.$screenname);
+        $this->enqueue_outgoing_raw(
+            array(
+                'type' => 'nickcheck',
+                'prioritise' => 1,
+                'data' => $this->fake_irc->would_be_sent,
+                'nickdata' =>
+                    array(
+                        'screenname' => $screenname,
+                        'code' => $code,
+                        'user' => $user
+                    )
+            )
+        );
+        return true;
+    }
+
+    /**
+    * Initialize plugin
+    *
+    * @return boolean
+    */
+    public function initialize() {
+        if (!isset($this->host)) {
+            throw new Exception('must specify a host');
+        }
+        if (!isset($this->username)) {
+            throw new Exception('must specify a username');
+        }
+        if (!isset($this->realname)) {
+            throw new Exception('must specify a "real name"');
+        }
+        if (!isset($this->nick)) {
+            throw new Exception('must specify a nickname');
+        }
+
+        if (!isset($this->port)) {
+            $this->port = 6667;
+        }
+        if (!isset($this->transporttype)) {
+            $this->transporttype = 'tcp';
+        }
+        if (!isset($this->encoding)) {
+            $this->encoding = 'UTF-8';
+        }
+
+        if (!isset($this->regcheck)) {
+            $this->regcheck = true;
+        }
+
+        $this->fake_irc = new Fake_Irc;
+
+        /*
+         * Commands allowed to return output to a channel
+         */
+        $this->whiteList = array('stats', 'last', 'get');
+
+        return true;
+    }
+
+    /**
+     * Get plugin information
+     *
+     * @param array $versions Array to insert information into
+     * @return void
+     */
+    public function onPluginVersion(&$versions) {
+        $versions[] = array('name' => 'IRC',
+                            'version' => STATUSNET_VERSION,
+                            'author' => 'Luke Fitzgerald',
+                            'homepage' => 'http://status.net/wiki/Plugin:IRC',
+                            'rawdescription' =>
+                            _m('The IRC plugin allows users to send and receive notices over an IRC network.'));
+        return true;
+    }
+}
diff --git a/plugins/Irc/Irc_waiting_message.php b/plugins/Irc/Irc_waiting_message.php
new file mode 100644 (file)
index 0000000..59eec63
--- /dev/null
@@ -0,0 +1,142 @@
+<?php\r
+/**\r
+ * Table Definition for irc_waiting_message\r
+ */\r
+require_once INSTALLDIR.'/classes/Memcached_DataObject.php';\r
+\r
+class Irc_waiting_message extends Memcached_DataObject {\r
+\r
+    public $__table = 'irc_waiting_message'; // table name\r
+    public $id;                              // int primary_key not_null auto_increment\r
+    public $data;                            // blob not_null\r
+    public $prioritise;                      // tinyint(1) not_null\r
+    public $attempts;                        // int not_null\r
+    public $created;                         // datetime() not_null\r
+    public $claimed;                         // datetime()\r
+\r
+    /* Static get */\r
+    public function staticGet($k, $v = null) {\r
+        return Memcached_DataObject::staticGet('Irc_waiting_message', $k, $v);\r
+    }\r
+\r
+    /**\r
+    * return table definition for DB_DataObject\r
+    *\r
+    * DB_DataObject needs to know something about the table to manipulate\r
+    * instances. This method provides all the DB_DataObject needs to know.\r
+    *\r
+    * @return array array of column definitions\r
+    */\r
+    public function table() {\r
+        return array('id' => DB_DATAOBJECT_INT + DB_DATAOBJECT_NOTNULL,\r
+                     'data' => DB_DATAOBJECT_BLOB + DB_DATAOBJECT_STR + DB_DATAOBJECT_NOTNULL,\r
+                     'prioritise' => DB_DATAOBJECT_INT + DB_DATAOBJECT_NOTNULL,\r
+                     'created' => DB_DATAOBJECT_TIME + DB_DATAOBJECT_STR + DB_DATAOBJECT_NOTNULL,\r
+                     'claimed' => DB_DATAOBJECT_TIME + DB_DATAOBJECT_STR);\r
+    }\r
+\r
+    /**\r
+    * return key definitions for DB_DataObject\r
+    *\r
+    * DB_DataObject needs to know about keys that the table has, since it\r
+    * won't appear in StatusNet's own keys list. In most cases, this will\r
+    * simply reference your keyTypes() function.\r
+    *\r
+    * @return array list of key field names\r
+    */\r
+    public function keys() {\r
+        return array_keys($this->keyTypes());\r
+    }\r
+\r
+    /**\r
+    * return key definitions for Memcached_DataObject\r
+    *\r
+    * Our caching system uses the same key definitions, but uses a different\r
+    * method to get them. This key information is used to store and clear\r
+    * cached data, so be sure to list any key that will be used for static\r
+    * lookups.\r
+    *\r
+    * @return array associative array of key definitions, field name to type:\r
+    *         'K' for primary key: for compound keys, add an entry for each component;\r
+    *         'U' for unique keys: compound keys are not well supported here.\r
+    */\r
+    public function keyTypes() {\r
+        return array('id' => 'K');\r
+    }\r
+\r
+    /**\r
+    * Magic formula for non-autoincrementing integer primary keys\r
+    *\r
+    * If a table has a single integer column as its primary key, DB_DataObject\r
+    * assumes that the column is auto-incrementing and makes a sequence table\r
+    * to do this incrementation. Since we don't need this for our class, we\r
+    * overload this method and return the magic formula that DB_DataObject needs.\r
+    *\r
+    * @return array magic three-false array that stops auto-incrementing.\r
+    */\r
+    public function sequenceKey() {\r
+        return array(false, false, false);\r
+    }\r
+\r
+    /**\r
+     * Get the next item in the queue\r
+     *\r
+     * @return Irc_waiting_message Next message if there is one\r
+     */\r
+    public static function top() {\r
+        $wm = new Irc_waiting_message();\r
+\r
+        $wm->orderBy('prioritise DESC, created');\r
+        $wm->whereAdd('claimed is null');\r
+\r
+        $wm->limit(1);\r
+\r
+        $cnt = $wm->find(true);\r
+\r
+        if ($cnt) {\r
+            # XXX: potential race condition\r
+            # can we force it to only update if claimed is still null\r
+            # (or old)?\r
+            common_log(LOG_INFO, 'claiming IRC waiting message id = ' . $wm->id);\r
+            $orig = clone($wm);\r
+            $wm->claimed = common_sql_now();\r
+            $result = $wm->update($orig);\r
+            if ($result) {\r
+                common_log(LOG_INFO, 'claim succeeded.');\r
+                return $wm;\r
+            } else {\r
+                common_log(LOG_INFO, 'claim failed.');\r
+            }\r
+        }\r
+        $wm = null;\r
+        return null;\r
+    }\r
+\r
+    /**\r
+    * Increment the attempts count\r
+    *\r
+    * @return void\r
+    * @throws Exception\r
+    */\r
+    public function incAttempts() {\r
+        $orig = clone($this);\r
+        $this->attempts++;\r
+        $result = $this->update($orig);\r
+\r
+        if (!$result) {\r
+            throw Exception(sprintf(_m("Could not increment attempts count for %d"), $this->id));\r
+        }\r
+    }\r
+\r
+    /**\r
+     * Release a claimed item.\r
+     */\r
+    public function releaseClaim() {\r
+        // DB_DataObject doesn't let us save nulls right now\r
+        $sql = sprintf("UPDATE irc_waiting_message SET claimed=NULL WHERE id=%d", $this->id);\r
+        $this->query($sql);\r
+\r
+        $this->claimed = null;\r
+        $this->encache();\r
+    }\r
+}\r
diff --git a/plugins/Irc/README b/plugins/Irc/README
new file mode 100644 (file)
index 0000000..bc45688
--- /dev/null
@@ -0,0 +1,43 @@
+The IRC plugin allows users to send and receive notices over an IRC network.
+
+Installation
+============
+add "addPlugin('irc',
+    array('setting'=>'value', 'setting2'=>'value2', ...);"
+to the bottom of your config.php
+
+scripts/imdaemon.php included with StatusNet must be running. It will be started by
+the plugin along with their other daemons when you run scripts/startdaemons.sh.
+See the StatusNet README for more about queuing and daemons.
+
+Settings
+========
+host*: Hostname of IRC server
+port: Port of IRC server (defaults to 6667)
+username*: Username of bot
+realname*: Real name of bot
+nick*: Nickname of bot
+password: Password
+nickservpassword: NickServ password for identification
+nickservidentifyregexp: Override existing regexp matching request for identification from NickServ
+channels: Channels for bot to idle in
+transporttype: Set to 'ssl' to enable SSL
+encoding: Set to change encoding
+regcheck: Check user's nicknames are registered, enabled by default, set to false to disable
+regregexp: Override existing regexp matching response from NickServ if nick checked is registered.
+           Must contain a capturing group catching the nick
+unregregexp: Override existing regexp matching response from NickServ if nick checked is unregistered
+             Must contain a capturing group catching the nick
+
+* required
+
+Example
+=======
+addPlugin('irc', array(
+    'host' => '...',
+    'username' => '...',
+    'realname' => '...',
+    'nick' => '...',
+    'channels' => array('#channel1', '#channel2')
+));
+
diff --git a/plugins/Irc/extlib/.gitignore b/plugins/Irc/extlib/.gitignore
new file mode 100644 (file)
index 0000000..553fe8e
--- /dev/null
@@ -0,0 +1,2 @@
+Settings.php
+*.db
diff --git a/plugins/Irc/extlib/phergie/.gitignore b/plugins/Irc/extlib/phergie/.gitignore
new file mode 100644 (file)
index 0000000..553fe8e
--- /dev/null
@@ -0,0 +1,2 @@
+Settings.php
+*.db
diff --git a/plugins/Irc/extlib/phergie/LICENSE b/plugins/Irc/extlib/phergie/LICENSE
new file mode 100644 (file)
index 0000000..d7d2342
--- /dev/null
@@ -0,0 +1,27 @@
+Copyright (c) 2010, Phergie Development Team 
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without 
+modification, are permitted provided that the following conditions are met:
+
+Redistributions of source code must retain the above copyright notice, this 
+list of conditions and the following disclaimer.
+
+Redistributions in binary form must reproduce the above copyright notice, 
+this list of conditions and the following disclaimer in the documentation 
+and/or other materials provided with the distribution.
+
+Neither the name of the Phergie Development Team nor the names of its 
+contributors may be used to endorse or promote products derived from this 
+software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
+USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/plugins/Irc/extlib/phergie/Phergie/Autoload.php b/plugins/Irc/extlib/phergie/Phergie/Autoload.php
new file mode 100755 (executable)
index 0000000..0004f44
--- /dev/null
@@ -0,0 +1,81 @@
+<?php
+/**
+ * Phergie
+ *
+ * PHP version 5
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.
+ * It is also available through the world-wide-web at this URL:
+ * http://phergie.org/license
+ *
+ * @category  Phergie
+ * @package   Phergie
+ * @author    Phergie Development Team <team@phergie.org>
+ * @copyright 2008-2010 Phergie Development Team (http://phergie.org)
+ * @license   http://phergie.org/license New BSD License
+ * @link      http://pear.phergie.org/package/Phergie
+ */
+
+/**
+ * Autoloader for Phergie classes.
+ *
+ * @category Phergie
+ * @package  Phergie
+ * @author   Phergie Development Team <team@phergie.org>
+ * @license  http://phergie.org/license New BSD License
+ * @link     http://pear.phergie.org/package/Phergie
+ */
+class Phergie_Autoload
+{
+    /**
+     * Constructor to add the base Phergie path to the include_path.
+     *
+     * @return void
+     */
+    public function __construct()
+    {
+        $path = realpath(dirname(__FILE__) . '/..');
+        $includePath = get_include_path();
+        $includePathList = explode(PATH_SEPARATOR, $includePath);
+        if (!in_array($path, $includePathList)) {
+            self::addPath($path);
+        }
+    }
+
+    /**
+     * Autoload callback for loading class files.
+     *
+     * @param string $class Class to load
+     *
+     * @return void
+     */
+    public function load($class)
+    {
+        include str_replace('_', DIRECTORY_SEPARATOR, $class) . '.php';
+    }
+
+    /**
+     * Registers an instance of this class as an autoloader.
+     *
+     * @return void
+     */
+    public static function registerAutoloader()
+    {
+        spl_autoload_register(array(new self, 'load'));
+    }
+
+    /**
+     * Add a path to the include path.
+     *
+     * @param string $path Path to add
+     *
+     * @return void
+     */
+    public static function addPath($path)
+    {
+        set_include_path($path . PATH_SEPARATOR . get_include_path());
+    }
+}
diff --git a/plugins/Irc/extlib/phergie/Phergie/Bot.php b/plugins/Irc/extlib/phergie/Phergie/Bot.php
new file mode 100755 (executable)
index 0000000..85e8a00
--- /dev/null
@@ -0,0 +1,390 @@
+<?php
+/**
+ * Phergie
+ *
+ * PHP version 5
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.
+ * It is also available through the world-wide-web at this URL:
+ * http://phergie.org/license
+ *
+ * @category  Phergie
+ * @package   Phergie
+ * @author    Phergie Development Team <team@phergie.org>
+ * @copyright 2008-2010 Phergie Development Team (http://phergie.org)
+ * @license   http://phergie.org/license New BSD License
+ * @link      http://pear.phergie.org/package/Phergie
+ */
+
+/**
+ * Composite class for other components to represent the bot.
+ *
+ * @category Phergie
+ * @package  Phergie
+ * @author   Phergie Development Team <team@phergie.org>
+ * @license  http://phergie.org/license New BSD License
+ * @link     http://pear.phergie.org/package/Phergie
+ */
+class Phergie_Bot
+{
+    /**
+     * Current version of Phergie
+     */
+    const VERSION = '2.0.1';
+
+    /**
+     * Current driver instance
+     *
+     * @var Phergie_Driver_Abstract
+     */
+    protected $driver;
+
+    /**
+     * Current configuration instance
+     *
+     * @var Phergie_Config
+     */
+    protected $config;
+
+    /**
+     * Current connection handler instance
+     *
+     * @var Phergie_Connection_Handler
+     */
+    protected $connections;
+
+    /**
+     * Current plugin handler instance
+     *
+     * @var Phergie_Plugin_Handler
+     */
+    protected $plugins;
+
+    /**
+     * Current event handler instance
+     *
+     * @var Phergie_Event_Handler
+     */
+    protected $events;
+
+    /**
+     * Current end-user interface instance
+     *
+     * @var Phergie_Ui_Abstract
+     */
+    protected $ui;
+
+    /**
+     * Current processor instance
+     *
+     * @var Phergie_Process_Abstract
+     */
+    protected $processor;
+
+    /**
+     * Returns a driver instance, creating one of the default class if
+     * none has been set.
+     *
+     * @return Phergie_Driver_Abstract
+     */
+    public function getDriver()
+    {
+        if (empty($this->driver)) {
+            // Check if a driver has been defined in the configuration to use
+            // as the default
+            $config = $this->getConfig();
+            if (isset($config['driver'])) {
+                $class = 'Phergie_Driver_' . ucfirst($config['driver']);
+            } else {
+                // Otherwise default to the Streams driver.
+                $class = 'Phergie_Driver_Streams';
+            }
+
+            $this->driver = new $class;
+        }
+        return $this->driver;
+    }
+
+    /**
+     * Sets the driver instance to use.
+     *
+     * @param Phergie_Driver_Abstract $driver Driver instance
+     *
+     * @return Phergie_Bot Provides a fluent interface
+     */
+    public function setDriver(Phergie_Driver_Abstract $driver)
+    {
+        $this->driver = $driver;
+        return $this;
+    }
+
+    /**
+     * Sets the configuration to use.
+     *
+     * @param Phergie_Config $config Configuration instance
+     *
+     * @return Phergie_Runner_Abstract Provides a fluent interface
+     */
+    public function setConfig(Phergie_Config $config)
+    {
+        $this->config = $config;
+        return $this;
+    }
+
+    /**
+     * Returns the entire configuration in use or the value of a specific
+     * configuration setting.
+     *
+     * @param string $index   Optional index of a specific configuration
+     *        setting for which the corresponding value should be returned
+     * @param mixed  $default Value to return if no match is found for $index
+     *
+     * @return mixed Value corresponding to $index or the entire
+     *         configuration if $index is not specified
+     */
+    public function getConfig($index = null, $default = null)
+    {
+        if (empty($this->config)) {
+            $this->config = new Phergie_Config;
+            $this->config->read('Settings.php');
+        }
+        if ($index !== null) {
+            if (isset($this->config[$index])) {
+                return $this->config[$index];
+            } else {
+                return $default;
+            }
+        }
+        return $this->config;
+    }
+
+    /**
+     * Returns a plugin handler instance, creating it if it does not already
+     * exist and using a default class if none has been set.
+     *
+     * @return Phergie_Plugin_Handler
+     */
+    public function getPluginHandler()
+    {
+        if (empty($this->plugins)) {
+            $this->plugins = new Phergie_Plugin_Handler(
+                $this->getConfig(),
+                $this->getEventHandler()
+            );
+        }
+        return $this->plugins;
+    }
+
+    /**
+     * Sets the plugin handler instance to use.
+     *
+     * @param Phergie_Plugin_Handler $handler Plugin handler instance
+     *
+     * @return Phergie_Bot Provides a fluent interface
+     */
+    public function setPluginHandler(Phergie_Plugin_Handler $handler)
+    {
+        $this->plugins = $handler;
+        return $this;
+    }
+
+    /**
+     * Returns an event handler instance, creating it if it does not already
+     * exist and using a default class if none has been set.
+     *
+     * @return Phergie_Event_Handler
+     */
+    public function getEventHandler()
+    {
+        if (empty($this->events)) {
+            $this->events = new Phergie_Event_Handler;
+        }
+        return $this->events;
+    }
+
+    /**
+     * Sets the event handler instance to use.
+     *
+     * @param Phergie_Event_Handler $handler Event handler instance
+     *
+     * @return Phergie_Bot Provides a fluent interface
+     */
+    public function setEventHandler(Phergie_Event_Handler $handler)
+    {
+        $this->events = $handler;
+        return $this;
+    }
+
+    /**
+     * Returns a connection handler instance, creating it if it does not
+     * already exist and using a default class if none has been set.
+     *
+     * @return Phergie_Connection_Handler
+     */
+    public function getConnectionHandler()
+    {
+        if (empty($this->connections)) {
+            $this->connections = new Phergie_Connection_Handler;
+        }
+        return $this->connections;
+    }
+
+    /**
+     * Sets the connection handler instance to use.
+     *
+     * @param Phergie_Connection_Handler $handler Connection handler instance
+     *
+     * @return Phergie_Bot Provides a fluent interface
+     */
+    public function setConnectionHandler(Phergie_Connection_Handler $handler)
+    {
+        $this->connections = $handler;
+        return $this;
+    }
+
+    /**
+     * Returns an end-user interface instance, creating it if it does not
+     * already exist and using a default class if none has been set.
+     *
+     * @return Phergie_Ui_Abstract
+     */
+    public function getUi()
+    {
+        if (empty($this->ui)) {
+            $this->ui = new Phergie_Ui_Console;
+        }
+        return $this->ui;
+    }
+
+    /**
+     * Sets the end-user interface instance to use.
+     *
+     * @param Phergie_Ui_Abstract $ui End-user interface instance
+     *
+     * @return Phergie_Bot Provides a fluent interface
+     */
+    public function setUi(Phergie_Ui_Abstract $ui)
+    {
+        $this->ui = $ui;
+        return $this;
+    }
+
+    /**
+     * Returns a processer instance, creating one if none exists.
+     *
+     * @return Phergie_Process_Abstract
+     */
+    public function getProcessor()
+    {
+        if (empty($this->processor)) {
+            $class = 'Phergie_Process_Standard';
+
+            $type = $this->getConfig('processor');
+            if (!empty($type)) {
+                $class = 'Phergie_Process_' . ucfirst($type);
+            }
+
+            $this->processor = new $class(
+                $this,
+                $this->getConfig('processor.options', array())
+            );
+        }
+        return $this->processor;
+    }
+
+    /**
+     * Sets the processer instance to use.
+     *
+     * @param Phergie_Process_Abstract $processor Processer instance
+     *
+     * @return Phergie_Bot Provides a fluent interface
+     */
+    public function setProcessor(Phergie_Process_Abstract $processor)
+    {
+        $this->processor = $processor;
+        return $this;
+    }
+
+    /**
+     * Loads plugins into the plugin handler.
+     *
+     * @return void
+     */
+    protected function loadPlugins()
+    {
+        $config = $this->getConfig();
+        $plugins = $this->getPluginHandler();
+        $ui = $this->getUi();
+
+        $plugins->setAutoload($config['plugins.autoload']);
+        foreach ($config['plugins'] as $name) {
+            try {
+                $plugin = $plugins->addPlugin($name);
+                $ui->onPluginLoad($name);
+            } catch (Phergie_Plugin_Exception $e) {
+                $ui->onPluginFailure($name, $e->getMessage());
+                if (!empty($plugin)) {
+                    $plugins->removePlugin($plugin);
+                }
+            }
+        }
+    }
+
+    /**
+     * Configures and establishes connections to IRC servers.
+     *
+     * @return void
+     */
+    protected function loadConnections()
+    {
+        $config = $this->getConfig();
+        $driver = $this->getDriver();
+        $connections = $this->getConnectionHandler();
+        $plugins = $this->getPluginHandler();
+        $ui = $this->getUi();
+
+        foreach ($config['connections'] as $data) {
+            $connection = new Phergie_Connection($data);
+            $connections->addConnection($connection);
+
+            $ui->onConnect($data['host']);
+            $driver->setConnection($connection)->doConnect();
+            $plugins->setConnection($connection);
+            $plugins->onConnect();
+        }
+    }
+
+    /**
+     * Establishes server connections and initiates an execution loop to
+     * continuously receive and process events.
+     *
+     * @return Phergie_Bot Provides a fluent interface
+     */
+    public function run()
+    {
+        set_time_limit(0);
+
+        $timezone = $this->getConfig('timezone', 'UTC');
+        date_default_timezone_set($timezone);
+
+        $ui = $this->getUi();
+        $ui->setEnabled($this->getConfig('ui.enabled'));
+
+        $this->loadPlugins();
+        $this->loadConnections();
+
+        $processor = $this->getProcessor();
+
+        $connections = $this->getConnectionHandler();
+        while (count($connections)) {
+            $processor->handleEvents();
+        }
+
+        $ui->onShutdown();
+
+        return $this;
+    }
+}
diff --git a/plugins/Irc/extlib/phergie/Phergie/Config.php b/plugins/Irc/extlib/phergie/Phergie/Config.php
new file mode 100755 (executable)
index 0000000..c182f2a
--- /dev/null
@@ -0,0 +1,186 @@
+<?php
+/**
+ * Phergie
+ *
+ * PHP version 5
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.
+ * It is also available through the world-wide-web at this URL:
+ * http://phergie.org/license
+ *
+ * @category  Phergie
+ * @package   Phergie
+ * @author    Phergie Development Team <team@phergie.org>
+ * @copyright 2008-2010 Phergie Development Team (http://phergie.org)
+ * @license   http://phergie.org/license New BSD License
+ * @link      http://pear.phergie.org/package/Phergie
+ */
+
+/**
+ * Reads from and writes to PHP configuration files and provides access to
+ * the settings they contain.
+ *
+ * @category Phergie
+ * @package  Phergie
+ * @author   Phergie Development Team <team@phergie.org>
+ * @license  http://phergie.org/license New BSD License
+ * @link     http://pear.phergie.org/package/Phergie
+ */
+class Phergie_Config implements ArrayAccess
+{
+    /**
+     * Mapping of configuration file paths to an array of names of settings
+     * they contain
+     *
+     * @var array
+     */
+    protected $files = array();
+
+    /**
+     * Mapping of setting names to their current corresponding values
+     *
+     * @var array
+     */
+    protected $settings = array();
+
+    /**
+     * Includes a specified PHP configuration file and incorporates its
+     * return value (which should be an associative array) into the current
+     * configuration settings.
+     *
+     * @param string $file Path to the file to read
+     *
+     * @return Phergie_Config Provides a fluent interface
+     * @throws Phergie_Config_Exception
+     */
+    public function read($file)
+    {
+        if (!(strtoupper(substr(PHP_OS, 0, 3)) === 'WIN'
+            && file_exists($file))
+            && !is_executable($file)
+        ) {
+            throw new Phergie_Config_Exception(
+                'Path "' . $file . '" does not reference an executable file',
+                Phergie_Config_Exception::ERR_FILE_NOT_EXECUTABLE
+            );
+        }
+
+        $settings = include $file;
+        if (!is_array($settings)) {
+            throw new Phergie_Config_Exception(
+                'File "' . $file . '" does not return an array',
+                Phergie_Config_Exception::ERR_ARRAY_NOT_RETURNED
+            );
+        }
+
+        $this->files[$file] = array_keys($settings);
+        $this->settings += $settings;
+
+        return $this;
+    }
+
+    /**
+     * Merges an associative array of configuration setting values into the
+     * current configuration settings.
+     *
+     * @param array $settings Associative array of configuration setting
+     *        values keyed by setting name
+     *
+     * @return Phergie_Config Provides a fluent interface
+     */
+    public function readArray(array $settings)
+    {
+        $this->settings += $settings;
+
+        return $this;
+    }
+
+    /**
+     * Writes the values of the current configuration settings back to their
+     * originating files.
+     *
+     * @return Phergie_Config Provides a fluent interface
+     */
+    public function write()
+    {
+        foreach ($this->files as $file => &$settings) {
+            $values = array();
+            foreach ($settings as $setting) {
+                $values[$setting] = $this->settings[$setting];
+            }
+            $source = '<?php' . PHP_EOL . PHP_EOL .
+                'return ' . var_export($value, true) . ';';
+            file_put_contents($file, $source);
+        }
+    }
+
+    /**
+     * Checks to see if a configuration setting is assigned a value.
+     *
+     * @param string $offset Configuration setting name
+     *
+     * @return bool TRUE if the setting has a value, FALSE otherwise
+     * @see ArrayAccess::offsetExists()
+     */
+    public function offsetExists($offset)
+    {
+        return isset($this->settings[$offset]);
+    }
+
+    /**
+     * Returns the value of a configuration setting.
+     *
+     * @param string $offset Configuration setting name
+     *
+     * @return mixed Configuration setting value or NULL if it is not
+     *         assigned a value
+     * @see ArrayAccess::offsetGet()
+     */
+    public function offsetGet($offset)
+    {
+        if (isset($this->settings[$offset])) {
+            $value = &$this->settings[$offset];
+        } else {
+            $value = null;
+        }
+
+        return $value;
+    }
+
+    /**
+     * Sets the value of a configuration setting.
+     *
+     * @param string $offset Configuration setting name
+     * @param mixed  $value  New setting value
+     *
+     * @return void
+     * @see ArrayAccess::offsetSet()
+     */
+    public function offsetSet($offset, $value)
+    {
+        $this->settings[$offset] = $value;
+    }
+
+    /**
+     * Removes the value set for a configuration setting.
+     *
+     * @param string $offset Configuration setting name
+     *
+     * @return void
+     * @see ArrayAccess::offsetUnset()
+     */
+    public function offsetUnset($offset)
+    {
+        unset($this->settings[$offset]);
+
+        foreach ($this->files as $file => $settings) {
+            $key = array_search($offset, $settings);
+            if ($key !== false) {
+                unset($this->files[$file][$key]);
+            }
+        }
+    }
+}
diff --git a/plugins/Irc/extlib/phergie/Phergie/Config/Exception.php b/plugins/Irc/extlib/phergie/Phergie/Config/Exception.php
new file mode 100644 (file)
index 0000000..fb646c1
--- /dev/null
@@ -0,0 +1,44 @@
+<?php
+/**
+ * Phergie 
+ *
+ * PHP version 5
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.
+ * It is also available through the world-wide-web at this URL:
+ * http://phergie.org/license
+ *
+ * @category  Phergie 
+ * @package   Phergie
+ * @author    Phergie Development Team <team@phergie.org>
+ * @copyright 2008-2010 Phergie Development Team (http://phergie.org)
+ * @license   http://phergie.org/license New BSD License
+ * @link      http://pear.phergie.org/package/Phergie
+ */
+
+/**
+ * Exception related to configuration.
+ *
+ * @category Phergie 
+ * @package  Phergie
+ * @author   Phergie Development Team <team@phergie.org>
+ * @license  http://phergie.org/license New BSD License
+ * @link     http://pear.phergie.org/package/Phergie
+ */
+class Phergie_Config_Exception extends Phergie_Exception
+{
+    /**
+     * Error indicating that an attempt was made to read a configuration 
+     * file that could not be executed
+     */
+    const ERR_FILE_NOT_EXECUTABLE = 1;
+
+    /**
+     * Error indicating that a read configuration file does not return an 
+     * array
+     */
+    const ERR_ARRAY_NOT_RETURNED = 2; 
+}
diff --git a/plugins/Irc/extlib/phergie/Phergie/Connection.php b/plugins/Irc/extlib/phergie/Phergie/Connection.php
new file mode 100755 (executable)
index 0000000..b3f0acf
--- /dev/null
@@ -0,0 +1,401 @@
+<?php
+/**
+ * Phergie
+ *
+ * PHP version 5
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.
+ * It is also available through the world-wide-web at this URL:
+ * http://phergie.org/license
+ *
+ * @category  Phergie
+ * @package   Phergie
+ * @author    Phergie Development Team <team@phergie.org>
+ * @copyright 2008-2010 Phergie Development Team (http://phergie.org)
+ * @license   http://phergie.org/license New BSD License
+ * @link      http://pear.phergie.org/package/Phergie
+ */
+
+/**
+ * Data structure for connection metadata.
+ *
+ * @category Phergie
+ * @package  Phergie
+ * @author   Phergie Development Team <team@phergie.org>
+ * @license  http://phergie.org/license New BSD License
+ * @link     http://pear.phergie.org/package/Phergie
+ */
+class Phergie_Connection
+{
+    /**
+     * Host to which the client will connect
+     *
+     * @var string
+     */
+    protected $host;
+
+    /**
+     * Port on which the client will connect, defaults to the standard IRC
+     * port
+     *
+     * @var int
+     */
+    protected $port;
+
+    /**
+     * Transport for the connection, defaults to tcp but can be set to ssl
+     * or variations thereof to connect over SSL
+     *
+     * @var string
+     */
+    protected $transport;
+
+    /**
+     * Encoding method for the connection, defaults to ISO-8859-1 but can
+     * be set to UTF8 if necessary
+     *
+     * @var strng
+     */
+    protected $encoding;
+
+    /**
+     * Nick that the client will use
+     *
+     * @var string
+     */
+    protected $nick;
+
+    /**
+     * Username that the client will use
+     *
+     * @var string
+     */
+    protected $username;
+
+    /**
+     * Realname that the client will use
+     *
+     * @var string
+     */
+    protected $realname;
+
+    /**
+     * Password that the client will use
+     *
+     * @var string
+     */
+    protected $password;
+
+    /**
+     * Hostmask for the connection
+     *
+     * @var Phergie_Hostmask
+     */
+    protected $hostmask;
+
+    /**
+     * Constructor to initialize instance properties.
+     *
+     * @param array $options Optional associative array of property values
+     *        to initialize
+     *
+     * @return void
+     */
+    public function __construct(array $options = array())
+    {
+        $this->transport = 'tcp';
+        $this->encoding = 'ISO-8859-1';
+        // @note this may need changed to something different, for broader support.
+        // @note also may need to make use of http://us.php.net/manual/en/function.stream-encoding.php
+
+        $this->setOptions($options);
+    }
+
+    /**
+     * Emits an error related to a required connection setting does not have
+     * value set for it.
+     *
+     * @param string $setting Name of the setting
+     *
+     * @return void
+     */
+    protected function checkSetting($setting)
+    {
+        if (empty($this->$setting)) {
+            throw new Phergie_Connection_Exception(
+                'Required connection setting "' . $setting . '" missing',
+                Phergie_Connection_Exception::ERR_REQUIRED_SETTING_MISSING
+            );
+        }
+    }
+
+    /**
+     * Returns a hostmask that uniquely identifies the connection.
+     *
+     * @return string
+     */
+    public function getHostmask()
+    {
+        if (empty($this->hostmask)) {
+            $this->hostmask = new Phergie_Hostmask(
+                $this->nick,
+                $this->username,
+                $this->host
+            );
+        }
+
+        return $this->hostmask;
+    }
+
+    /**
+     * Sets the host to which the client will connect.
+     *
+     * @param string $host Hostname
+     *
+     * @return Phergie_Connection Provides a fluent interface
+     */
+    public function setHost($host)
+    {
+        if (empty($this->host)) {
+            $this->host = (string) $host;
+        }
+
+        return $this;
+    }
+
+    /**
+     * Returns the host to which the client will connect if it is set or
+     * emits an error if it is not set.
+     *
+     * @return string
+     */
+    public function getHost()
+    {
+        $this->checkSetting('host');
+
+        return $this->host;
+    }
+
+    /**
+     * Sets the port on which the client will connect.
+     *
+     * @param int $port Port
+     *
+     * @return Phergie_Connection Provides a fluent interface
+     */
+    public function setPort($port)
+    {
+        if (empty($this->port)) {
+            $this->port = (int) $port;
+        }
+
+        return $this;
+    }
+
+    /**
+     * Returns the port on which the client will connect.
+     *
+     * @return int
+     */
+    public function getPort()
+    {
+        if (empty($this->port)) {
+            $this->port = 6667;
+        }
+
+        return $this->port;
+    }
+
+    /**
+     * Sets the transport for the connection to use.
+     *
+     * @param string $transport Transport (ex: tcp, ssl, etc.)
+     *
+     * @return Phergie_Connection Provides a fluent interface
+     */
+    public function setTransport($transport)
+    {
+        $this->transport = (string) $transport;
+
+        if (!in_array($this->transport, stream_get_transports())) {
+            throw new Phergie_Connection_Exception(
+                'Transport ' . $this->transport . ' is not supported',
+                Phergie_Connection_Exception::TRANSPORT_NOT_SUPPORTED
+            );
+        }
+
+        return $this;
+    }
+
+    /**
+     * Returns the transport in use by the connection.
+     *
+     * @return string Transport (ex: tcp, ssl, etc.)
+     */
+    public function getTransport()
+    {
+        return $this->transport;
+    }
+
+    /**
+     * Sets the encoding for the connection to use.
+     *
+     * @param string $encoding Encoding to use (ex: ASCII, ISO-8859-1, UTF8, etc.)
+     *
+     * @return Phergie_Connection Provides a fluent interface
+     */
+    public function setEncoding($encoding)
+    {
+        $this->encoding = (string) $encoding;
+
+        if (!in_array($this->encoding, mb_list_encodings())) {
+            throw new Phergie_Connection_Exception(
+                'Encoding ' . $this->encoding . ' is not supported',
+                Phergie_Connection_Exception::ERR_ENCODING_NOT_SUPPORTED
+            );
+        }
+
+        return $this;
+    }
+
+    /**
+     * Returns the encoding in use by the connection.
+     *
+     * @return string Encoding (ex: ASCII, ISO-8859-1, UTF8, etc.)
+     */
+    public function getEncoding()
+    {
+        return $this->encoding;
+    }
+
+    /**
+     * Sets the nick that the client will use.
+     *
+     * @param string $nick Nickname
+     *
+     * @return Phergie_Connection Provides a fluent interface
+     */
+    public function setNick($nick)
+    {
+        if (empty($this->nick)) {
+            $this->nick = (string) $nick;
+        }
+
+        return $this;
+    }
+
+    /**
+     * Returns the nick that the client will use.
+     *
+     * @return string
+     */
+    public function getNick()
+    {
+        $this->checkSetting('nick');
+
+        return $this->nick;
+    }
+
+    /**
+     * Sets the username that the client will use.
+     *
+     * @param string $username Username
+     *
+     * @return Phergie_Connection Provides a fluent interface
+     */
+    public function setUsername($username)
+    {
+        if (empty($this->username)) {
+            $this->username = (string) $username;
+        }
+
+        return $this;
+    }
+
+    /**
+     * Returns the username that the client will use.
+     *
+     * @return string
+     */
+    public function getUsername()
+    {
+        $this->checkSetting('username');
+
+        return $this->username;
+    }
+
+    /**
+     * Sets the realname that the client will use.
+     *
+     * @param string $realname Real name
+     *
+     * @return Phergie_Connection Provides a fluent interface
+     */
+    public function setRealname($realname)
+    {
+        if (empty($this->realname)) {
+            $this->realname = (string) $realname;
+        }
+
+        return $this;
+    }
+
+    /**
+     * Returns the realname that the client will use.
+     *
+     * @return string
+     */
+    public function getRealname()
+    {
+        $this->checkSetting('realname');
+
+        return $this->realname;
+    }
+
+    /**
+     * Sets the password that the client will use.
+     *
+     * @param string $password Password
+     *
+     * @return Phergie_Connection Provides a fluent interface
+     */
+    public function setPassword($password)
+    {
+        if (empty($this->password)) {
+            $this->password = (string) $password;
+        }
+
+        return $this;
+    }
+
+    /**
+     * Returns the password that the client will use.
+     *
+     * @return string
+     */
+    public function getPassword()
+    {
+        return $this->password;
+    }
+
+    /**
+     * Sets multiple connection settings using an array.
+     *
+     * @param array $options Associative array of setting names mapped to
+     *        corresponding values
+     *
+     * @return Phergie_Connection Provides a fluent interface
+     */
+    public function setOptions(array $options)
+    {
+        foreach ($options as $option => $value) {
+            $method = 'set' . ucfirst($option);
+            if (method_exists($this, $method)) {
+                $this->$method($value);
+            }
+        }
+    }
+}
diff --git a/plugins/Irc/extlib/phergie/Phergie/Connection/Exception.php b/plugins/Irc/extlib/phergie/Phergie/Connection/Exception.php
new file mode 100644 (file)
index 0000000..aec1cd8
--- /dev/null
@@ -0,0 +1,50 @@
+<?php
+/**
+ * Phergie 
+ *
+ * PHP version 5
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.
+ * It is also available through the world-wide-web at this URL:
+ * http://phergie.org/license
+ *
+ * @category  Phergie 
+ * @package   Phergie
+ * @author    Phergie Development Team <team@phergie.org>
+ * @copyright 2008-2010 Phergie Development Team (http://phergie.org)
+ * @license   http://phergie.org/license New BSD License
+ * @link      http://pear.phergie.org/package/Phergie
+ */
+
+/**
+ * Exception related to a connection to an IRC server.
+ *
+ * @category Phergie 
+ * @package  Phergie
+ * @author   Phergie Development Team <team@phergie.org>
+ * @license  http://phergie.org/license New BSD License
+ * @link     http://pear.phergie.org/package/Phergie
+ */
+class Phergie_Connection_Exception extends Phergie_Exception
+{
+    /**
+     * Error indicating that an operation was attempted requiring a value 
+     * for a specific configuration setting, but none was set
+     */
+    const ERR_REQUIRED_SETTING_MISSING = 1;
+
+    /**
+     * Error indicating that a connection is configured to use a transport, 
+     * but that transport is not supported by the current PHP installation
+     */
+    const ERR_TRANSPORT_NOT_SUPPORTED = 2;
+
+    /**
+     * Error indicating that a connection is configured to use an encoding,
+     * but that encoding is not supported by the current PHP installation
+     */
+    const ERR_ENCODING_NOT_SUPPORTED = 3;
+}
diff --git a/plugins/Irc/extlib/phergie/Phergie/Connection/Handler.php b/plugins/Irc/extlib/phergie/Phergie/Connection/Handler.php
new file mode 100644 (file)
index 0000000..e9aeddc
--- /dev/null
@@ -0,0 +1,130 @@
+<?php
+/**
+ * Phergie 
+ *
+ * PHP version 5
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.
+ * It is also available through the world-wide-web at this URL:
+ * http://phergie.org/license
+ *
+ * @category  Phergie 
+ * @package   Phergie
+ * @author    Phergie Development Team <team@phergie.org>
+ * @copyright 2008-2010 Phergie Development Team (http://phergie.org)
+ * @license   http://phergie.org/license New BSD License
+ * @link      http://pear.phergie.org/package/Phergie
+ */
+
+/**
+ * Handles connections initiated by the bot.
+ *
+ * @category Phergie 
+ * @package  Phergie
+ * @author   Phergie Development Team <team@phergie.org>
+ * @license  http://phergie.org/license New BSD License
+ * @link     http://pear.phergie.org/package/Phergie
+ */
+class Phergie_Connection_Handler implements Countable, IteratorAggregate
+{
+    /**
+     * Map of connections indexed by hostmask
+     *
+     * @var array
+     */
+    protected $connections;
+
+    /**
+     * Constructor to initialize storage for connections. 
+     *
+     * @return void
+     */
+    public function __construct()
+    {
+        $this->connections = array();
+    }
+
+    /**
+     * Adds a connection to the connection list.
+     *
+     * @param Phergie_Connection $connection Connection to add
+     *
+     * @return Phergie_Connection_Handler Provides a fluent interface
+     */
+    public function addConnection(Phergie_Connection $connection)
+    {
+        $this->connections[(string) $connection->getHostmask()] = $connection;
+        return $this;
+    }
+
+    /**
+     * Removes a connection from the connection list.
+     *
+     * @param Phergie_Connection|string $connection Instance or hostmask for
+     *        the connection to remove
+     *
+     * @return Phergie_Connection_Handler Provides a fluent interface
+     */
+    public function removeConnection($connection)
+    {
+        if ($connection instanceof Phergie_Connection) {
+            $hostmask = (string) $connection->getHostmask(); 
+        } elseif (is_string($connection) 
+            && isset($this->connections[$connection])) {
+            $hostmask = $connection;
+        } else {
+            return $this;
+        }
+        unset($this->connections[$hostmask]);
+        return $this;
+    }
+
+    /**
+     * Returns the number of connections in the list. 
+     *
+     * @return int Number of connections 
+     */
+    public function count()
+    {
+        return count($this->connections);
+    }
+
+    /**
+     * Returns an iterator for the connection list. 
+     *
+     * @return ArrayIterator
+     */
+    public function getIterator()
+    {
+        return new ArrayIterator($this->connections);
+    }
+
+    /**
+     * Returns a list of specified connection objects.
+     *
+     * @param array|string $keys One or more hostmasks identifying the 
+     *        connections to return
+     *
+     * @return array List of Phergie_Connection objects corresponding to the 
+     *         specified hostmask(s)
+     */
+    public function getConnections($keys)
+    {
+        $connections = array();
+
+        if (!is_array($keys)) {
+            $keys = array($keys);
+        }
+        
+        foreach ($keys as $key) {
+            if (isset($this->connections[$key])) {
+                $connections[] = $this->connections[$key];
+            }
+        }
+
+        return $connections;
+    }
+}
diff --git a/plugins/Irc/extlib/phergie/Phergie/Driver/Abstract.php b/plugins/Irc/extlib/phergie/Phergie/Driver/Abstract.php
new file mode 100755 (executable)
index 0000000..6273662
--- /dev/null
@@ -0,0 +1,301 @@
+<?php
+/**
+ * Phergie 
+ *
+ * PHP version 5
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.
+ * It is also available through the world-wide-web at this URL:
+ * http://phergie.org/license
+ *
+ * @category  Phergie 
+ * @package   Phergie
+ * @author    Phergie Development Team <team@phergie.org>
+ * @copyright 2008-2010 Phergie Development Team (http://phergie.org)
+ * @license   http://phergie.org/license New BSD License
+ * @link      http://pear.phergie.org/package/Phergie
+ */
+
+/**
+ * Base class for drivers which handle issuing client commands to the IRC
+ * server and converting responses into usable data objects.
+ *
+ * @category Phergie 
+ * @package  Phergie
+ * @author   Phergie Development Team <team@phergie.org>
+ * @license  http://phergie.org/license New BSD License
+ * @link     http://pear.phergie.org/package/Phergie
+ */
+abstract class Phergie_Driver_Abstract
+{
+    /**
+     * Currently active connection
+     *
+     * @var Phergie_Connection
+     */
+    protected $connection;
+
+    /**
+     * Sets the currently active connection.
+     *
+     * @param Phergie_Connection $connection Active connection
+     *
+     * @return Phergie_Driver_Abstract Provides a fluent interface
+     */
+    public function setConnection(Phergie_Connection $connection)
+    {
+        $this->connection = $connection;
+
+        return $this;
+    }
+
+    /**
+     * Returns the currently active connection.
+     *
+     * @return Phergie_Connection
+     * @throws Phergie_Driver_Exception
+     */
+    public function getConnection()
+    {
+        if (empty($this->connection)) {
+            throw new Phergie_Driver_Exception(
+                'Operation requires an active connection, but none is set',
+                Phergie_Driver_Exception::ERR_NO_ACTIVE_CONNECTION
+            );
+        }
+
+        return $this->connection;
+    }
+
+    /**
+     * Returns an event if one has been received from the server.
+     *
+     * @return Phergie_Event_Interface|null Event instance if an event has
+     *         been received, NULL otherwise
+     */
+    public abstract function getEvent();
+
+    /**
+     * Initiates a connection with the server.
+     *
+     * @return void
+     */
+    public abstract function doConnect();
+
+    /**
+     * Terminates the connection with the server.
+     *
+     * @param string $reason Reason for connection termination (optional)
+     *
+     * @return void
+     * @link http://www.irchelp.org/irchelp/rfc/chapter4.html#c4_1_6
+     */
+    public abstract function doQuit($reason = null);
+
+    /**
+     * Joins a channel.
+     *
+     * @param string $channels Comma-delimited list of channels to join 
+     * @param string $keys     Optional comma-delimited list of channel keys
+     *
+     * @return void
+     * @link http://www.irchelp.org/irchelp/rfc/chapter4.html#c4_2_1
+     */
+    public abstract function doJoin($channels, $keys = null);
+
+    /**
+     * Leaves a channel.
+     *
+     * @param string $channels Comma-delimited list of channels to leave 
+     *
+     * @return void
+     * @link http://www.irchelp.org/irchelp/rfc/chapter4.html#c4_2_2
+     */
+    public abstract function doPart($channels);
+
+    /**
+     * Invites a user to an invite-only channel.
+     *
+     * @param string $nick    Nick of the user to invite
+     * @param string $channel Name of the channel
+     *
+     * @return void
+     * @link http://www.irchelp.org/irchelp/rfc/chapter4.html#c4_2_7
+     */
+    public abstract function doInvite($nick, $channel);
+
+    /**
+     * Obtains a list of nicks of users in specified channels.
+     *
+     * @param string $channels Comma-delimited list of one or more channels
+     *
+     * @return void
+     * @link http://www.irchelp.org/irchelp/rfc/chapter4.html#c4_2_5
+     */
+    public abstract function doNames($channels);
+
+    /**
+     * Obtains a list of channel names and topics.
+     *
+     * @param string $channels Comma-delimited list of one or more channels
+     *                         to which the response should be restricted
+     *                         (optional)
+     *
+     * @return void
+     * @link http://www.irchelp.org/irchelp/rfc/chapter4.html#c4_2_6
+     */
+    public abstract function doList($channels = null);
+
+    /**
+     * Retrieves or changes a channel topic.
+     *
+     * @param string $channel Name of the channel
+     * @param string $topic   New topic to assign (optional)
+     *
+     * @return void
+     * @link http://www.irchelp.org/irchelp/rfc/chapter4.html#c4_2_4
+     */
+    public abstract function doTopic($channel, $topic = null);
+
+    /**
+     * Retrieves or changes a channel or user mode.
+     *
+     * @param string $target Channel name or user nick
+     * @param string $mode   New mode to assign (optional)
+     *
+     * @return void
+     * @link http://www.irchelp.org/irchelp/rfc/chapter4.html#c4_2_3
+     */
+    public abstract function doMode($target, $mode = null);
+
+    /**
+     * Changes the client nick.
+     *
+     * @param string $nick New nick to assign
+     *
+     * @return void
+     * @link http://www.irchelp.org/irchelp/rfc/chapter4.html#c4_1_2
+     */
+    public abstract function doNick($nick);
+
+    /**
+     * Retrieves information about a nick.
+     *
+     * @param string $nick Nick
+     *
+     * @return void
+     * @link http://www.irchelp.org/irchelp/rfc/chapter4.html#c4_5_2
+     */
+    public abstract function doWhois($nick);
+
+    /**
+     * Sends a message to a nick or channel.
+     *
+     * @param string $target Channel name or user nick
+     * @param string $text   Text of the message to send
+     *
+     * @return void
+     * @link http://www.irchelp.org/irchelp/rfc/chapter4.html#c4_4_1
+     */
+    public abstract function doPrivmsg($target, $text);
+
+    /**
+     * Sends a notice to a nick or channel.
+     *
+     * @param string $target Channel name or user nick
+     * @param string $text   Text of the notice to send
+     *
+     * @return void
+     * @link http://www.irchelp.org/irchelp/rfc/chapter4.html#c4_4_2
+     */
+    public abstract function doNotice($target, $text);
+
+    /**
+     * Kicks a user from a channel.
+     *
+     * @param string $nick    Nick of the user
+     * @param string $channel Channel name
+     * @param string $reason  Reason for the kick (optional)
+     *
+     * @return void
+     * @link http://www.irchelp.org/irchelp/rfc/chapter4.html#c4_2_8
+     */
+    public abstract function doKick($nick, $channel, $reason = null);
+
+    /**
+     * Responds to a server test of client responsiveness.
+     *
+     * @param string $daemon Daemon from which the original request originates
+     *
+     * @return void
+     * @link http://www.irchelp.org/irchelp/rfc/chapter4.html#c4_6_3
+     */
+    public abstract function doPong($daemon);
+
+    /**
+     * Sends a CTCP ACTION (/me) command to a nick or channel.
+     *
+     * @param string $target Channel name or user nick
+     * @param string $text   Text of the action to perform
+     *
+     * @return void
+     * @link http://www.invlogic.com/irc/ctcp.html#4.4
+     */
+    public abstract function doAction($target, $text);
+
+    /**
+     * Sends a CTCP PING request to a user.
+     *
+     * @param string $nick User nick
+     * @param string $hash Hash to use in the handshake
+     *
+     * @return void
+     * @link http://www.invlogic.com/irc/ctcp.html#4.2
+     */
+    public abstract function doPing($nick, $hash);
+
+    /**
+     * Sends a CTCP VERSION request or response to a user.
+     *
+     * @param string $nick    User nick
+     * @param string $version Version string to send for a response
+     *
+     * @return void
+     * @link http://www.invlogic.com/irc/ctcp.html#4.1
+     */
+    public abstract function doVersion($nick, $version = null);
+
+    /**
+     * Sends a CTCP TIME request to a user.
+     *
+     * @param string $nick User nick
+     * @param string $time Time string to send for a response
+     *
+     * @return void
+     * @link http://www.invlogic.com/irc/ctcp.html#4.6
+     */
+    public abstract function doTime($nick, $time = null);
+
+    /**
+     * Sends a CTCP FINGER request to a user.
+     *
+     * @param string $nick   User nick
+     * @param string $finger Finger string to send for a response
+     *
+     * @return void
+     * @link http://www.irchelp.org/irchelp/rfc/ctcpspec.html 
+     */
+    public abstract function doFinger($nick, $finger = null);
+
+    /**
+     * Sends a raw command to the server.
+     *
+     * @param string $command Command string to send
+     *
+     * @return void
+     */
+    public abstract function doRaw($command);
+}
diff --git a/plugins/Irc/extlib/phergie/Phergie/Driver/Exception.php b/plugins/Irc/extlib/phergie/Phergie/Driver/Exception.php
new file mode 100755 (executable)
index 0000000..5873b2c
--- /dev/null
@@ -0,0 +1,59 @@
+<?php
+/**
+ * Phergie
+ *
+ * PHP version 5
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.
+ * It is also available through the world-wide-web at this URL:
+ * http://phergie.org/license
+ *
+ * @category  Phergie
+ * @package   Phergie
+ * @author    Phergie Development Team <team@phergie.org>
+ * @copyright 2008-2010 Phergie Development Team (http://phergie.org)
+ * @license   http://phergie.org/license New BSD License
+ * @link      http://pear.phergie.org/package/Phergie
+ */
+
+/**
+ * Exception related to driver operations.
+ *
+ * @category Phergie
+ * @package  Phergie
+ * @author   Phergie Development Team <team@phergie.org>
+ * @license  http://phergie.org/license New BSD License
+ * @link     http://pear.phergie.org/package/Phergie
+ */
+class Phergie_Driver_Exception extends Phergie_Exception
+{
+    /**
+     * Error indicating that an operation was requested requiring an active
+     * connection before one had been set
+     */
+    const ERR_NO_ACTIVE_CONNECTION = 1;
+
+    /**
+     * Error indicating that an operation was requested requiring an active
+     * connection where one had been set but not initiated
+     */
+    const ERR_NO_INITIATED_CONNECTION = 2;
+
+    /**
+     * Error indicating that an attempt to initiate a connection failed
+     */
+    const ERR_CONNECTION_ATTEMPT_FAILED = 3;
+
+    /**
+     * Error indicating that an attempt to send data via a connection failed
+     */
+    const ERR_CONNECTION_WRITE_FAILED = 4;
+
+    /**
+     * Error indicating that an attempt to read data via a connection failed
+     */
+    const ERR_CONNECTION_READ_FAILED = 5;
+}
diff --git a/plugins/Irc/extlib/phergie/Phergie/Driver/Statusnet.php b/plugins/Irc/extlib/phergie/Phergie/Driver/Statusnet.php
new file mode 100644 (file)
index 0000000..84c85a0
--- /dev/null
@@ -0,0 +1,66 @@
+<?php\r
+/**\r
+ * StatusNet - the distributed open-source microblogging tool\r
+ *\r
+ * This program is free software: you can redistribute it and/or modify\r
+ * it under the terms of the GNU Affero General Public License as published by\r
+ * the Free Software Foundation, either version 3 of the License, or\r
+ * (at your option) any later version.\r
+ *\r
+ * This program is distributed in the hope that it will be useful,\r
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of\r
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\r
+ * GNU Affero General Public License for more details.\r
+ *\r
+ * You should have received a copy of the GNU Affero General Public License\r
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.\r
+ *\r
+ * Extends the Streams driver (Phergie_Driver_Streams) to give external access\r
+ * to the socket resources and send method\r
+ *\r
+ * @category  Phergie\r
+ * @package   Phergie_Driver_Statusnet\r
+ * @author    Luke Fitzgerald <lw.fitzgerald@googlemail.com>\r
+ * @copyright 2010 StatusNet, Inc.\r
+ * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0\r
+ * @link      http://status.net/\r
+ */\r
+\r
+class Phergie_Driver_Statusnet extends Phergie_Driver_Streams {\r
+    /**\r
+     * Handles construction of command strings and their transmission to the\r
+     * server.\r
+     *\r
+     * @param string       $command Command to send\r
+     * @param string|array $args    Optional string or array of sequential\r
+     *        arguments\r
+     *\r
+     * @return string Command string that was sent\r
+     * @throws Phergie_Driver_Exception\r
+     */\r
+    public function send($command, $args = '') {\r
+        return parent::send($command, $args);\r
+    }\r
+\r
+    public function forceQuit() {\r
+        try {\r
+            // Send a QUIT command to the server\r
+            $this->send('QUIT', 'Reconnecting');\r
+        } catch (Phergie_Driver_Exception $e){}\r
+\r
+        // Terminate the socket connection\r
+        fclose($this->socket);\r
+\r
+        // Remove the socket from the internal socket list\r
+        unset($this->sockets[(string) $this->getConnection()->getHostmask()]);\r
+    }\r
+\r
+    /**\r
+    * Returns the array of sockets\r
+    *\r
+    * @return array Array of socket resources\r
+    */\r
+    public function getSockets() {\r
+        return $this->sockets;\r
+    }\r
+}
\ No newline at end of file
diff --git a/plugins/Irc/extlib/phergie/Phergie/Driver/Streams.php b/plugins/Irc/extlib/phergie/Phergie/Driver/Streams.php
new file mode 100755 (executable)
index 0000000..73c0230
--- /dev/null
@@ -0,0 +1,729 @@
+<?php
+/**
+ * Phergie
+ *
+ * PHP version 5
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.
+ * It is also available through the world-wide-web at this URL:
+ * http://phergie.org/license
+ *
+ * @category  Phergie
+ * @package   Phergie
+ * @author    Phergie Development Team <team@phergie.org>
+ * @copyright 2008-2010 Phergie Development Team (http://phergie.org)
+ * @license   http://phergie.org/license New BSD License
+ * @link      http://pear.phergie.org/package/Phergie
+ */
+
+/**
+ * Driver that uses the sockets wrapper of the streams extension for
+ * communicating with the server and handles formatting and parsing of
+ * events using PHP.
+ *
+ * @category Phergie
+ * @package  Phergie
+ * @author   Phergie Development Team <team@phergie.org>
+ * @license  http://phergie.org/license New BSD License
+ * @link     http://pear.phergie.org/package/Phergie
+ */
+class Phergie_Driver_Streams extends Phergie_Driver_Abstract
+{
+    /**
+     * Socket handlers
+     *
+     * @var array
+     */
+    protected $sockets = array();
+
+    /**
+     * Reference to the currently active socket handler
+     *
+     * @var resource
+     */
+    protected $socket;
+
+    /**
+     * Amount of time in seconds to wait to receive an event each time the
+     * socket is polled
+     *
+     * @var float
+     */
+    protected $timeout = 0.1;
+
+    /**
+     * Handles construction of command strings and their transmission to the
+     * server.
+     *
+     * @param string       $command Command to send
+     * @param string|array $args    Optional string or array of sequential
+     *        arguments
+     *
+     * @return string Command string that was sent
+     * @throws Phergie_Driver_Exception
+     */
+    protected function send($command, $args = '')
+    {
+        $connection = $this->getConnection();
+        $encoding = $connection->getEncoding();
+
+        // Require an open socket connection to continue
+        if (empty($this->socket)) {
+            throw new Phergie_Driver_Exception(
+                'doConnect() must be called first',
+                Phergie_Driver_Exception::ERR_NO_INITIATED_CONNECTION
+            );
+        }
+
+        // Add the command
+        $buffer = strtoupper($command);
+
+        // Add arguments
+        if (!empty($args)) {
+
+            // Apply formatting if arguments are passed in as an array
+            if (is_array($args)) {
+                $end = count($args) - 1;
+                $args[$end] = ':' . $args[$end];
+                $args = implode(' ', $args);
+            } else {
+                $args = ':' . $args;
+            }
+
+            $buffer .= ' ' . $args;
+        }
+
+        // Transmit the command over the socket connection
+        $attempts = $written = 0;
+        $temp = $buffer . "\r\n";
+        $is_multibyte = !substr($encoding, 0, 8) === 'ISO-8859' && $encoding !== 'ASCII' && $encoding !== 'CP1252';
+        $length = ($is_multibyte) ? mb_strlen($buffer, '8bit') : strlen($buffer);
+        while (true) {
+            $written += (int) fwrite($this->socket, $temp);
+            if ($written < $length) {
+                $temp = substr($temp, $written);
+                $attempts++;
+                if ($attempts == 3) {
+                    throw new Phergie_Driver_Exception(
+                        'Unable to write to socket',
+                        Phergie_Driver_Exception::ERR_CONNECTION_WRITE_FAILED
+                    );
+                }
+            } else {
+                break;
+            }
+        }
+
+        // Return the command string that was transmitted
+        return $buffer;
+    }
+
+    /**
+     * Overrides the parent class to set the currently active socket handler
+     * when the active connection is changed.
+     *
+     * @param Phergie_Connection $connection Active connection
+     *
+     * @return Phergie_Driver_Streams Provides a fluent interface
+     */
+    public function setConnection(Phergie_Connection $connection)
+    {
+        // Set the active socket handler
+        $hostmask = (string) $connection->getHostmask();
+        if (!empty($this->sockets[$hostmask])) {
+            $this->socket = $this->sockets[$hostmask];
+        }
+
+        // Set the active connection
+        return parent::setConnection($connection);
+    }
+
+    /**
+     * Returns a list of hostmasks corresponding to sockets with data to read.
+     *
+     * @param int $sec  Length of time to wait for new data (seconds)
+     * @param int $usec Length of time to wait for new data (microseconds)
+     *
+     * @return array List of hostmasks or an empty array if none were found
+     *         to have data to read
+     */
+    public function getActiveReadSockets($sec = 0, $usec = 200000)
+    {
+        $read = $this->sockets;
+        $write = null;
+        $error = null;
+        $active = array();
+
+        if (count($this->sockets) > 0) {
+            $number = stream_select($read, $write, $error, $sec, $usec);
+            if ($number > 0) {
+                foreach ($read as $item) {
+                    $active[] = array_search($item, $this->sockets);
+                }
+            }
+        }
+
+        return $active;
+    }
+
+    /**
+     * Sets the amount of time to wait for a new event each time the socket
+     * is polled.
+     *
+     * @param float $timeout Amount of time in seconds
+     *
+     * @return Phergie_Driver_Streams Provides a fluent interface
+     */
+    public function setTimeout($timeout)
+    {
+        $timeout = (float) $timeout;
+        if ($timeout) {
+            $this->timeout = $timeout;
+        }
+        return $this;
+    }
+
+    /**
+     * Returns the amount of time to wait for a new event each time the
+     * socket is polled.
+     *
+     * @return float Amount of time in seconds
+     */
+    public function getTimeout()
+    {
+        return $this->timeout;
+    }
+
+    /**
+     * Supporting method to parse event argument strings where the last
+     * argument may contain a colon.
+     *
+     * @param string $args  Argument string to parse
+     * @param int    $count Optional maximum number of arguments
+     *
+     * @return array Array of argument values
+     */
+    protected function parseArguments($args, $count = -1)
+    {
+        return preg_split('/ :?/S', $args, $count);
+    }
+
+    /**
+     * Listens for an event on the current connection.
+     *
+     * @return Phergie_Event_Interface|null Event instance if an event was
+     *         received, NULL otherwise
+     */
+    public function getEvent()
+    {
+        // Check the socket is still active
+        if (feof($this->socket)) {
+            throw new Phergie_Driver_Exception(
+                'EOF detected on socket',
+                Phergie_Driver_Exception::ERR_CONNECTION_READ_FAILED
+            );
+        }
+
+        // Check for a new event on the current connection
+        $buffer = fgets($this->socket, 512);
+
+        // If no new event was found, return NULL
+        if (empty($buffer)) {
+            return null;
+        }
+
+        // Strip the trailing newline from the buffer
+        $buffer = rtrim($buffer);
+
+        // If the event is from the server...
+        if (substr($buffer, 0, 1) != ':') {
+
+            // Parse the command and arguments
+            list($cmd, $args) = array_pad(explode(' ', $buffer, 2), 2, null);
+            $hostmask = new Phergie_Hostmask(null, null, $this->connection->getHost());
+
+        } else {
+            // If the event could be from the server or a user...
+
+            // Parse the server hostname or user hostmask, command, and arguments
+            list($prefix, $cmd, $args)
+                = array_pad(explode(' ', ltrim($buffer, ':'), 3), 3, null);
+            if (strpos($prefix, '@') !== false) {
+                $hostmask = Phergie_Hostmask::fromString($prefix);
+            } else {
+                $hostmask = new Phergie_Hostmask(null, null, $prefix);
+            }
+        }
+
+        // Parse the event arguments depending on the event type
+        $cmd = strtolower($cmd);
+        switch ($cmd) {
+        case 'names':
+        case 'nick':
+        case 'quit':
+        case 'ping':
+        case 'join':
+        case 'error':
+            $args = array(ltrim($args, ':'));
+            break;
+
+        case 'privmsg':
+        case 'notice':
+            $args = $this->parseArguments($args, 2);
+            list($source, $ctcp) = $args;
+            if (substr($ctcp, 0, 1) === "\001" && substr($ctcp, -1) === "\001") {
+                $ctcp = substr($ctcp, 1, -1);
+                $reply = ($cmd == 'notice');
+                list($cmd, $args) = array_pad(explode(' ', $ctcp, 2), 2, null);
+                $cmd = strtolower($cmd);
+                switch ($cmd) {
+                case 'version':
+                case 'time':
+                case 'finger':
+                    if ($reply) {
+                        $args = $ctcp;
+                    }
+                    break;
+                case 'ping':
+                    if ($reply) {
+                        $cmd .= 'Response';
+                    } else {
+                        $cmd = 'ctcpPing';
+                    }
+                    break;
+                case 'action':
+                    $args = array($source, $args);
+                    break;
+
+                default:
+                    $cmd = 'ctcp';
+                    if ($reply) {
+                        $cmd .= 'Response';
+                    }
+                    $args = array($source, $args);
+                    break;
+                }
+            }
+            break;
+
+        case 'oper':
+        case 'topic':
+        case 'mode':
+            $args = $this->parseArguments($args);
+            break;
+
+        case 'part':
+        case 'kill':
+        case 'invite':
+            $args = $this->parseArguments($args, 2);
+            break;
+
+        case 'kick':
+            $args = $this->parseArguments($args, 3);
+            break;
+
+        // Remove the target from responses
+        default:
+            $args = substr($args, strpos($args, ' ') + 1);
+            break;
+        }
+
+        // Create, populate, and return an event object
+        if (ctype_digit($cmd)) {
+            $event = new Phergie_Event_Response;
+            $event
+                ->setCode($cmd)
+                ->setDescription($args);
+        } else {
+            $event = new Phergie_Event_Request;
+            $event
+                ->setType($cmd)
+                ->setArguments($args);
+            if (isset($hostmask)) {
+                $event->setHostmask($hostmask);
+            }
+        }
+        $event->setRawData($buffer);
+        return $event;
+    }
+
+    /**
+     * Initiates a connection with the server.
+     *
+     * @return void
+     */
+    public function doConnect()
+    {
+        // Listen for input indefinitely
+        set_time_limit(0);
+
+        // Get connection information
+        $connection = $this->getConnection();
+        $hostname = $connection->getHost();
+        $port = $connection->getPort();
+        $password = $connection->getPassword();
+        $username = $connection->getUsername();
+        $nick = $connection->getNick();
+        $realname = $connection->getRealname();
+        $transport = $connection->getTransport();
+
+        // Establish and configure the socket connection
+        $remote = $transport . '://' . $hostname . ':' . $port;
+        $this->socket = @stream_socket_client($remote, $errno, $errstr);
+        if (!$this->socket) {
+            throw new Phergie_Driver_Exception(
+                'Unable to connect: socket error ' . $errno . ' ' . $errstr,
+                Phergie_Driver_Exception::ERR_CONNECTION_ATTEMPT_FAILED
+            );
+        }
+
+        $seconds = (int) $this->timeout;
+        $microseconds = ($this->timeout - $seconds) * 1000000;
+        stream_set_timeout($this->socket, $seconds, $microseconds);
+
+        // Send the password if one is specified
+        if (!empty($password)) {
+            $this->send('PASS', $password);
+        }
+
+        // Send user information
+        $this->send(
+            'USER',
+            array(
+                $username,
+                $hostname,
+                $hostname,
+                $realname
+            )
+        );
+
+        $this->send('NICK', $nick);
+
+        // Add the socket handler to the internal array for socket handlers
+        $this->sockets[(string) $connection->getHostmask()] = $this->socket;
+    }
+
+    /**
+     * Terminates the connection with the server.
+     *
+     * @param string $reason Reason for connection termination (optional)
+     *
+     * @return void
+     */
+    public function doQuit($reason = null)
+    {
+        // Send a QUIT command to the server
+        $this->send('QUIT', $reason);
+
+        // Terminate the socket connection
+        fclose($this->socket);
+
+        // Remove the socket from the internal socket list
+        unset($this->sockets[(string) $this->getConnection()->getHostmask()]);
+    }
+
+    /**
+     * Joins a channel.
+     *
+     * @param string $channels Comma-delimited list of channels to join
+     * @param string $keys     Optional comma-delimited list of channel keys
+     *
+     * @return void
+     */
+    public function doJoin($channels, $keys = null)
+    {
+        $args = array($channels);
+
+        if (!empty($keys)) {
+            $args[] = $keys;
+        }
+
+        $this->send('JOIN', $args);
+    }
+
+    /**
+     * Leaves a channel.
+     *
+     * @param string $channels Comma-delimited list of channels to leave
+     *
+     * @return void
+     */
+    public function doPart($channels)
+    {
+        $this->send('PART', $channels);
+    }
+
+    /**
+     * Invites a user to an invite-only channel.
+     *
+     * @param string $nick    Nick of the user to invite
+     * @param string $channel Name of the channel
+     *
+     * @return void
+     */
+    public function doInvite($nick, $channel)
+    {
+        $this->send('INVITE', array($nick, $channel));
+    }
+
+    /**
+     * Obtains a list of nicks of usrs in currently joined channels.
+     *
+     * @param string $channels Comma-delimited list of one or more channels
+     *
+     * @return void
+     */
+    public function doNames($channels)
+    {
+        $this->send('NAMES', $channels);
+    }
+
+    /**
+     * Obtains a list of channel names and topics.
+     *
+     * @param string $channels Comma-delimited list of one or more channels
+     *                         to which the response should be restricted
+     *                         (optional)
+     *
+     * @return void
+     */
+    public function doList($channels = null)
+    {
+        $this->send('LIST', $channels);
+    }
+
+    /**
+     * Retrieves or changes a channel topic.
+     *
+     * @param string $channel Name of the channel
+     * @param string $topic   New topic to assign (optional)
+     *
+     * @return void
+     */
+    public function doTopic($channel, $topic = null)
+    {
+        $args = array($channel);
+
+        if (!empty($topic)) {
+            $args[] = $topic;
+        }
+
+        $this->send('TOPIC', $args);
+    }
+
+    /**
+     * Retrieves or changes a channel or user mode.
+     *
+     * @param string $target Channel name or user nick
+     * @param string $mode   New mode to assign (optional)
+     *
+     * @return void
+     */
+    public function doMode($target, $mode = null)
+    {
+        $args = array($target);
+
+        if (!empty($mode)) {
+            $args[] = $mode;
+        }
+
+        $this->send('MODE', $args);
+    }
+
+    /**
+     * Changes the client nick.
+     *
+     * @param string $nick New nick to assign
+     *
+     * @return void
+     */
+    public function doNick($nick)
+    {
+        $this->send('NICK', $nick);
+    }
+
+    /**
+     * Retrieves information about a nick.
+     *
+     * @param string $nick Nick
+     *
+     * @return void
+     */
+    public function doWhois($nick)
+    {
+        $this->send('WHOIS', $nick);
+    }
+
+    /**
+     * Sends a message to a nick or channel.
+     *
+     * @param string $target Channel name or user nick
+     * @param string $text   Text of the message to send
+     *
+     * @return void
+     */
+    public function doPrivmsg($target, $text)
+    {
+        $this->send('PRIVMSG', array($target, $text));
+    }
+
+    /**
+     * Sends a notice to a nick or channel.
+     *
+     * @param string $target Channel name or user nick
+     * @param string $text   Text of the notice to send
+     *
+     * @return void
+     */
+    public function doNotice($target, $text)
+    {
+        $this->send('NOTICE', array($target, $text));
+    }
+
+    /**
+     * Kicks a user from a channel.
+     *
+     * @param string $nick    Nick of the user
+     * @param string $channel Channel name
+     * @param string $reason  Reason for the kick (optional)
+     *
+     * @return void
+     */
+    public function doKick($nick, $channel, $reason = null)
+    {
+        $args = array($nick, $channel);
+
+        if (!empty($reason)) {
+            $args[] = $response;
+        }
+
+        $this->send('KICK', $args);
+    }
+
+    /**
+     * Responds to a server test of client responsiveness.
+     *
+     * @param string $daemon Daemon from which the original request originates
+     *
+     * @return void
+     */
+    public function doPong($daemon)
+    {
+        $this->send('PONG', $daemon);
+    }
+
+    /**
+     * Sends a CTCP ACTION (/me) command to a nick or channel.
+     *
+     * @param string $target Channel name or user nick
+     * @param string $text   Text of the action to perform
+     *
+     * @return void
+     */
+    public function doAction($target, $text)
+    {
+        $buffer = rtrim('ACTION ' . $text);
+
+        $this->doPrivmsg($target, chr(1) . $buffer . chr(1));
+    }
+
+    /**
+     * Sends a CTCP response to a user.
+     *
+     * @param string       $nick    User nick
+     * @param string       $command Command to send
+     * @param string|array $args    String or array of sequential arguments
+     *        (optional)
+     *
+     * @return void
+     */
+    protected function doCtcp($nick, $command, $args = null)
+    {
+        if (is_array($args)) {
+            $args = implode(' ', $args);
+        }
+
+        $buffer = rtrim(strtoupper($command) . ' ' . $args);
+
+        $this->doNotice($nick, chr(1) . $buffer . chr(1));
+    }
+
+    /**
+     * Sends a CTCP PING request or response (they are identical) to a user.
+     *
+     * @param string $nick User nick
+     * @param string $hash Hash to use in the handshake
+     *
+     * @return void
+     */
+    public function doPing($nick, $hash)
+    {
+        $this->doCtcp($nick, 'PING', $hash);
+    }
+
+    /**
+     * Sends a CTCP VERSION request or response to a user.
+     *
+     * @param string $nick    User nick
+     * @param string $version Version string to send for a response
+     *
+     * @return void
+     */
+    public function doVersion($nick, $version = null)
+    {
+        if ($version) {
+            $this->doCtcp($nick, 'VERSION', $version);
+        } else {
+            $this->doCtcp($nick, 'VERSION');
+        }
+    }
+
+    /**
+     * Sends a CTCP TIME request to a user.
+     *
+     * @param string $nick User nick
+     * @param string $time Time string to send for a response
+     *
+     * @return void
+     */
+    public function doTime($nick, $time = null)
+    {
+        if ($time) {
+            $this->doCtcp($nick, 'TIME', $time);
+        } else {
+            $this->doCtcp($nick, 'TIME');
+        }
+    }
+
+    /**
+     * Sends a CTCP FINGER request to a user.
+     *
+     * @param string $nick   User nick
+     * @param string $finger Finger string to send for a response
+     *
+     * @return void
+     */
+    public function doFinger($nick, $finger = null)
+    {
+        if ($finger) {
+            $this->doCtcp($nick, 'FINGER', $finger);
+        } else {
+            $this->doCtcp($nick, 'FINGER');
+        }
+    }
+
+    /**
+     * Sends a raw command to the server.
+     *
+     * @param string $command Command string to send
+     *
+     * @return void
+     */
+    public function doRaw($command)
+    {
+        $this->send('RAW', $command);
+    }
+}
diff --git a/plugins/Irc/extlib/phergie/Phergie/Event/Abstract.php b/plugins/Irc/extlib/phergie/Phergie/Event/Abstract.php
new file mode 100644 (file)
index 0000000..54b035d
--- /dev/null
@@ -0,0 +1,62 @@
+<?php
+/**
+ * Phergie 
+ *
+ * PHP version 5
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.
+ * It is also available through the world-wide-web at this URL:
+ * http://phergie.org/license
+ *
+ * @category  Phergie 
+ * @package   Phergie
+ * @author    Phergie Development Team <team@phergie.org>
+ * @copyright 2008-2010 Phergie Development Team (http://phergie.org)
+ * @license   http://phergie.org/license New BSD License
+ * @link      http://pear.phergie.org/package/Phergie
+ */
+
+/**
+ * Base class for events.
+ *
+ * @category Phergie 
+ * @package  Phergie
+ * @author   Phergie Development Team <team@phergie.org>
+ * @license  http://phergie.org/license New BSD License
+ * @link     http://pear.phergie.org/package/Phergie
+ */
+abstract class Phergie_Event_Abstract
+{
+    /**
+     * Event type, used for determining the callback to execute in response
+     *
+     * @var string
+     */
+    protected $type;
+
+    /**
+     * Returns the event type.
+     *
+     * @return string
+     */
+    public function getType()
+    {
+        return $this->type; 
+    }
+
+    /**
+     * Sets the event type.
+     *
+     * @param string $type Event type
+     *
+     * @return Phergie_Event_Abstract Implements a fluent interface
+     */
+    public function setType($type)
+    {
+        $this->type = (string) $type;
+        return $this;
+    }
+}
diff --git a/plugins/Irc/extlib/phergie/Phergie/Event/Command.php b/plugins/Irc/extlib/phergie/Phergie/Event/Command.php
new file mode 100644 (file)
index 0000000..5940636
--- /dev/null
@@ -0,0 +1,62 @@
+<?php
+/**
+ * Phergie 
+ *
+ * PHP version 5
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.
+ * It is also available through the world-wide-web at this URL:
+ * http://phergie.org/license
+ *
+ * @category  Phergie 
+ * @package   Phergie
+ * @author    Phergie Development Team <team@phergie.org>
+ * @copyright 2008-2010 Phergie Development Team (http://phergie.org)
+ * @license   http://phergie.org/license New BSD License
+ * @link      http://pear.phergie.org/package/Phergie
+ */
+
+/**
+ * Event originating from a plugin for the bot.
+ *
+ * @category Phergie 
+ * @package  Phergie
+ * @author   Phergie Development Team <team@phergie.org>
+ * @license  http://phergie.org/license New BSD License
+ * @link     http://pear.phergie.org/package/Phergie
+ */
+class Phergie_Event_Command extends Phergie_Event_Request
+{
+    /**
+     * Reference to the plugin instance that created the event
+     *
+     * @var Phergie_Plugin_Abstract
+     */
+    protected $plugin;
+
+    /**
+     * Stores a reference to the plugin instance that created the event.
+     *
+     * @param Phergie_Plugin_Abstract $plugin Plugin instance
+     *
+     * @return Phergie_Event_Command Provides a fluent interface
+     */
+    public function setPlugin(Phergie_Plugin_Abstract $plugin)
+    {
+        $this->plugin = $plugin;
+        return $this;
+    }
+
+    /**
+     * Returns a reference to the plugin instance that created the event.
+     *
+     * @return Phergie_Plugin_Abstract
+     */
+    public function getPlugin()
+    {
+        return $this->plugin;
+    }
+}
diff --git a/plugins/Irc/extlib/phergie/Phergie/Event/Exception.php b/plugins/Irc/extlib/phergie/Phergie/Event/Exception.php
new file mode 100644 (file)
index 0000000..6b094a8
--- /dev/null
@@ -0,0 +1,38 @@
+<?php
+/**
+ * Phergie 
+ *
+ * PHP version 5
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.
+ * It is also available through the world-wide-web at this URL:
+ * http://phergie.org/license
+ *
+ * @category  Phergie 
+ * @package   Phergie
+ * @author    Phergie Development Team <team@phergie.org>
+ * @copyright 2008-2010 Phergie Development Team (http://phergie.org)
+ * @license   http://phergie.org/license New BSD License
+ * @link      http://pear.phergie.org/package/Phergie
+ */
+
+/**
+ * Exception related to outgoing events. 
+ *
+ * @category Phergie 
+ * @package  Phergie
+ * @author   Phergie Development Team <team@phergie.org>
+ * @license  http://phergie.org/license New BSD License
+ * @link     http://pear.phergie.org/package/Phergie
+ */
+class Phergie_Event_Exception extends Phergie_Exception
+{
+    /**
+     * Error indicating that an attempt was made to create an event of an 
+     * unknown type
+     */
+    const ERR_UNKNOWN_EVENT_TYPE = 1;
+}
diff --git a/plugins/Irc/extlib/phergie/Phergie/Event/Handler.php b/plugins/Irc/extlib/phergie/Phergie/Event/Handler.php
new file mode 100644 (file)
index 0000000..e308df8
--- /dev/null
@@ -0,0 +1,190 @@
+<?php
+/**
+ * Phergie
+ *
+ * PHP version 5
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.
+ * It is also available through the world-wide-web at this URL:
+ * http://phergie.org/license
+ *
+ * @category  Phergie
+ * @package   Phergie
+ * @author    Phergie Development Team <team@phergie.org>
+ * @copyright 2008-2010 Phergie Development Team (http://phergie.org)
+ * @license   http://phergie.org/license New BSD License
+ * @link      http://pear.phergie.org/package/Phergie
+ */
+
+/**
+ * Handles events initiated by plugins.
+ *
+ * @category Phergie
+ * @package  Phergie
+ * @author   Phergie Development Team <team@phergie.org>
+ * @license  http://phergie.org/license New BSD License
+ * @link     http://pear.phergie.org/package/Phergie
+ */
+class Phergie_Event_Handler implements IteratorAggregate, Countable
+{
+    /**
+     * Current queue of events
+     *
+     * @var array
+     */
+    protected $events;
+
+    /**
+     * Constructor to initialize the event queue.
+     *
+     * @return void
+     */
+    public function __construct()
+    {
+        $this->events = array();
+    }
+
+    /**
+     * Adds an event to the queue.
+     *
+     * @param Phergie_Plugin_Abstract $plugin Plugin originating the event
+     * @param string                  $type   Event type, corresponding to a
+     *        Phergie_Event_Command::TYPE_* constant
+     * @param array                   $args   Optional event arguments
+     *
+     * @return Phergie_Event_Handler Provides a fluent interface
+     */
+    public function addEvent(Phergie_Plugin_Abstract $plugin, $type,
+        array $args = array()
+    ) {
+        if (!defined('Phergie_Event_Command::TYPE_' . strtoupper($type))) {
+            throw new Phergie_Event_Exception(
+                'Unknown event type "' . $type . '"',
+                Phergie_Event_Exception::ERR_UNKNOWN_EVENT_TYPE
+            );
+        }
+
+        $event = new Phergie_Event_Command;
+        $event
+            ->setPlugin($plugin)
+            ->setType($type)
+            ->setArguments($args);
+
+        $this->events[] = $event;
+
+        return $this;
+    }
+
+    /**
+     * Returns the current event queue.
+     *
+     * @return array Enumerated array of Phergie_Event_Command objects
+     */
+    public function getEvents()
+    {
+        return $this->events;
+    }
+
+    /**
+     * Clears the event queue.
+     *
+     * @return Phergie_Event_Handler Provides a fluent interface
+     */
+    public function clearEvents()
+    {
+        $this->events = array();
+        return $this;
+    }
+
+    /**
+     * Replaces the current event queue with a given queue of events.
+     *
+     * @param array $events Ordered list of objects of the class
+     *        Phergie_Event_Command
+     *
+     * @return Phergie_Event_Handler Provides a fluent interface
+     */
+    public function replaceEvents(array $events)
+    {
+        $this->events = $events;
+        return $this;
+    }
+
+    /**
+     * Returns whether an event of the given type exists in the queue.
+     *
+     * @param string $type Event type from Phergie_Event_Request::TYPE_*
+     *        constants
+     *
+     * @return bool TRUE if an event of the specified type exists in the
+     *         queue, FALSE otherwise
+     */
+    public function hasEventOfType($type)
+    {
+        foreach ($this->events as $event) {
+            if ($event->getType() == $type) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Returns a list of events of a specified type.
+     *
+     * @param string $type Event type from Phergie_Event_Request::TYPE_*
+     *        constants
+     *
+     * @return array Array containing event instances of the specified type
+     *         or an empty array if no such events were found
+     */
+    public function getEventsOfType($type)
+    {
+        $events = array();
+        foreach ($this->events as $event) {
+            if ($event->getType() == $type) {
+                $events[] = $event;
+            }
+        }
+        return $events;
+    }
+
+    /**
+     * Removes a single event from the event queue.
+     *
+     * @param Phergie_Event_Command $event Event to remove
+     *
+     * @return Phergie_Event_Handler Provides a fluent interface
+     */
+    public function removeEvent(Phergie_Event_Command $event)
+    {
+        $key = array_search($event, $this->events);
+        if ($key !== false) {
+            unset($this->events[$key]);
+        }
+        return $this;
+    }
+
+    /**
+     * Returns an iterator for the current event queue.
+     *
+     * @return ArrayIterator
+     */
+    public function getIterator()
+    {
+        return new ArrayIterator($this->events);
+    }
+
+    /**
+     * Returns the number of events in the event queue
+     *
+     * @return int number of queued events
+     */
+    public function count()
+    {
+        return count($this->events);
+    }
+}
diff --git a/plugins/Irc/extlib/phergie/Phergie/Event/Request.php b/plugins/Irc/extlib/phergie/Phergie/Event/Request.php
new file mode 100755 (executable)
index 0000000..647b5ac
--- /dev/null
@@ -0,0 +1,468 @@
+<?php
+/**
+ * Phergie
+ *
+ * PHP version 5
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.
+ * It is also available through the world-wide-web at this URL:
+ * http://phergie.org/license
+ *
+ * @category  Phergie
+ * @package   Phergie
+ * @author    Phergie Development Team <team@phergie.org>
+ * @copyright 2008-2010 Phergie Development Team (http://phergie.org)
+ * @license   http://phergie.org/license New BSD License
+ * @link      http://pear.phergie.org/package/Phergie
+ */
+
+/**
+ * Autonomous event originating from a user or the server.
+ *
+ * @category Phergie
+ * @package  Phergie
+ * @author   Phergie Development Team <team@phergie.org>
+ * @license  http://phergie.org/license New BSD License
+ * @link     http://pear.phergie.org/package/Phergie
+ * @link     http://www.irchelp.org/irchelp/rfc/chapter4.html
+ */
+class Phergie_Event_Request
+    extends Phergie_Event_Abstract
+    implements ArrayAccess
+{
+    /**
+     * Nick message event type
+     */
+    const TYPE_NICK = 'nick';
+
+    /**
+     * Whois message event type
+     */
+    const TYPE_WHOIS = 'whois';
+
+    /**
+     * Quit command event type
+     */
+    const TYPE_QUIT = 'quit';
+
+    /**
+     * Join message event type
+     */
+    const TYPE_JOIN = 'join';
+
+    /**
+     * Kick message event type
+     */
+    const TYPE_KICK = 'kick';
+
+    /**
+     * Part message event type
+     */
+    const TYPE_PART = 'part';
+
+    /**
+     * Invite message event type
+     */
+    const TYPE_INVITE = 'invite';
+
+    /**
+     * Mode message event type
+     */
+    const TYPE_MODE = 'mode';
+
+    /**
+     * Topic message event type
+     */
+    const TYPE_TOPIC = 'topic';
+
+    /**
+     * Private message command event type
+     */
+    const TYPE_PRIVMSG = 'privmsg';
+
+    /**
+     * Notice message event type
+     */
+    const TYPE_NOTICE = 'notice';
+
+    /**
+     * Pong message event type
+     */
+    const TYPE_PONG = 'pong';
+
+    /**
+     * CTCP ACTION command event type
+     */
+    const TYPE_ACTION = 'action';
+
+    /**
+     * CTCP PING command event type
+     */
+    const TYPE_PING = 'ping';
+
+    /**
+     * CTCP TIME command event type
+     */
+    const TYPE_TIME = 'time';
+
+    /**
+     * CTCP VERSION command event type
+     */
+    const TYPE_VERSION = 'version';
+
+    /**
+     * RAW message event type
+     */
+    const TYPE_RAW = 'raw';
+
+    /**
+     * Mapping of event types to their named parameters
+     *
+     * @var array
+     */
+    protected static $map = array(
+
+        self::TYPE_QUIT => array(
+            'message' => 0
+        ),
+
+        self::TYPE_JOIN => array(
+            'channel' => 0
+        ),
+
+        self::TYPE_KICK => array(
+            'channel' => 0,
+            'user'    => 1,
+            'comment' => 2
+        ),
+
+        self::TYPE_PART => array(
+            'channel' => 0,
+            'message' => 1
+        ),
+
+        self::TYPE_INVITE => array(
+            'nickname' => 0,
+            'channel'  => 1
+        ),
+
+        self::TYPE_MODE => array(
+            'target'  => 0,
+            'mode'    => 1,
+            'limit'   => 2,
+            'user'    => 3,
+            'banmask' => 4
+        ),
+
+        self::TYPE_TOPIC => array(
+            'channel' => 0,
+            'topic'   => 1
+        ),
+
+        self::TYPE_PRIVMSG => array(
+            'receiver' => 0,
+            'text'     => 1
+        ),
+
+        self::TYPE_NOTICE => array(
+            'nickname' => 0,
+            'text'     => 1
+        ),
+
+        self::TYPE_ACTION => array(
+            'target' => 0,
+            'action' => 1
+        ),
+
+        self::TYPE_RAW => array(
+            'message' => 0
+        )
+
+    );
+
+    /**
+     * Hostmask representing the originating user, if applicable
+     *
+     * @var Phergie_Hostmask
+     */
+    protected $hostmask;
+
+    /**
+     * Arguments included with the message
+     *
+     * @var array
+     */
+    protected $arguments;
+
+    /**
+     * Raw data sent by the server
+     *
+     * @var string
+     */
+    protected $rawData;
+
+    /**
+     * Sets the hostmask representing the originating user.
+     *
+     * @param Phergie_Hostmask $hostmask User hostmask
+     *
+     * @return Phergie_Event_Request Provides a fluent interface
+     */
+    public function setHostmask(Phergie_Hostmask $hostmask)
+    {
+        $this->hostmask = $hostmask;
+        return $this;
+    }
+
+    /**
+     * Returns the hostmask representing the originating user.
+     *
+     * @return Phergie_Event_Request|null Hostmask or NULL if none was set
+     */
+    public function getHostmask()
+    {
+        return $this->hostmask;
+    }
+
+    /**
+     * Sets the arguments for the request.
+     *
+     * @param array $arguments Request arguments
+     *
+     * @return Phergie_Event_Request Provides a fluent interface
+     */
+    public function setArguments($arguments)
+    {
+        $this->arguments = $arguments;
+        return $this;
+    }
+
+    /**
+     * Sets the value of a single argument for the request.
+     *
+     * @param mixed  $argument Integer position (starting from 0) or the
+     *        equivalent string name of the argument from self::$map
+     * @param string $value    Value to assign to the argument
+     *
+     * @return Phergie_Event_Request Provides a fluent interface
+     */
+    public function setArgument($argument, $value)
+    {
+        $argument = $this->resolveArgument($argument);
+        if ($argument !== null) {
+            $this->arguments[$argument] = (string) $value;
+        }
+        return $this;
+    }
+
+    /**
+     * Returns the arguments for the request.
+     *
+     * @return array
+     */
+    public function getArguments()
+    {
+        return $this->arguments;
+    }
+
+    /**
+     * Resolves an argument specification to an integer position.
+     *
+     * @param mixed $argument Integer position (starting from 0) or the
+     *        equivalent string name of the argument from self::$map
+     *
+     * @return int|null Integer position of the argument or NULL if no
+     *         corresponding argument was found
+     */
+    protected function resolveArgument($argument)
+    {
+        if (isset($this->arguments[$argument])) {
+            return $argument;
+        } else {
+            $argument = strtolower($argument);
+            if (isset(self::$map[$this->type][$argument])
+                && isset($this->arguments[self::$map[$this->type][$argument]])
+            ) {
+                return self::$map[$this->type][$argument];
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Returns a single specified argument for the request.
+     *
+     * @param mixed $argument Integer position (starting from 0) or the
+     *        equivalent string name of the argument from self::$map
+     *
+     * @return string|null Argument value or NULL if none is set
+     */
+    public function getArgument($argument)
+    {
+        $argument = $this->resolveArgument($argument);
+        if ($argument !== null) {
+            return $this->arguments[$argument];
+        }
+        return null;
+    }
+
+    /**
+     * Sets the raw buffer for the event.
+     *
+     * @param string $buffer Raw event buffer
+     *
+     * @return Phergie_Event_Request Provides a fluent interface
+     */
+    public function setRawData($buffer)
+    {
+        $this->rawData = $buffer;
+        return $this;
+    }
+
+    /**
+     * Returns the raw buffer sent from the server for the event.
+     *
+     * @return string
+     */
+    public function getRawData()
+    {
+        return $this->rawData;
+    }
+
+    /**
+     * Returns the nick of the user who originated the event.
+     *
+     * @return string
+     */
+    public function getNick()
+    {
+        return $this->hostmask->getNick();
+    }
+
+    /**
+     * Returns the channel name if the event occurred in a channel or the
+     * user nick if the event was a private message directed at the bot by a
+     * user.
+     *
+     * @return string
+     */
+    public function getSource()
+    {
+        if (substr($this->arguments[0], 0, 1) == '#') {
+            return $this->arguments[0];
+        }
+        return $this->hostmask->getNick();
+    }
+
+    /**
+     * Returns whether or not the event occurred within a channel.
+     *
+     * @return TRUE if the event is in a channel, FALSE otherwise
+     */
+    public function isInChannel()
+    {
+        return (substr($this->getSource(), 0, 1) == '#');
+    }
+
+    /**
+     * Returns whether or not the event originated from a user.
+     *
+     * @return TRUE if the event is from a user, FALSE otherwise
+     */
+    public function isFromUser()
+    {
+        if (empty($this->hostmask)) {
+            return false;
+        }
+        $username = $this->hostmask->getUsername();
+        return !empty($username);
+    }
+
+    /**
+     * Returns whether or not the event originated from the server.
+     *
+     * @return TRUE if the event is from the server, FALSE otherwise
+     */
+    public function isFromServer()
+    {
+        $username = $this->hostmask->getUsername();
+        return empty($username);
+    }
+
+    /**
+     * Provides access to named parameters via virtual "getter" methods.
+     *
+     * @param string $name      Name of the method called
+     * @param array  $arguments Arguments passed to the method (should always
+     *        be empty)
+     *
+     * @return mixed Method return value
+     */
+    public function __call($name, array $arguments)
+    {
+        if (!count($arguments) && substr($name, 0, 3) == 'get') {
+            return $this->getArgument(substr($name, 3));
+        }
+    }
+
+    /**
+     * Checks to see if an event argument is assigned a value.
+     *
+     * @param string|int $offset Argument name or position beginning from 0
+     *
+     * @return bool TRUE if the argument has a value, FALSE otherwise
+     * @see ArrayAccess::offsetExists()
+     */
+    public function offsetExists($offset)
+    {
+        return ($this->resolveArgument($offset) !== null);
+    }
+
+    /**
+     * Returns the value of an event argument.
+     *
+     * @param string|int $offset Argument name or position beginning from 0
+     *
+     * @return string|null Argument value or NULL if none is set
+     * @see ArrayAccess::offsetGet()
+     */
+    public function offsetGet($offset)
+    {
+        return $this->getArgument($offset);
+    }
+
+    /**
+     * Sets the value of an event argument.
+     *
+     * @param string|int $offset Argument name or position beginning from 0
+     * @param string     $value  New argument value
+     *
+     * @return void
+     * @see ArrayAccess::offsetSet()
+     */
+    public function offsetSet($offset, $value)
+    {
+        $offset = $this->resolveArgument($offset);
+        if ($offset !== null) {
+            $this->arguments[$offset] = $value;
+        }
+    }
+
+    /**
+     * Removes the value set for an event argument.
+     *
+     * @param string|int $offset Argument name or position beginning from 0
+     *
+     * @return void
+     * @see ArrayAccess::offsetUnset()
+     */
+    public function offsetUnset($offset)
+    {
+        if ($offset = $this->resolveArgument($offset)) {
+            unset($this->arguments[$offset]);
+        }
+    }
+}
diff --git a/plugins/Irc/extlib/phergie/Phergie/Event/Response.php b/plugins/Irc/extlib/phergie/Phergie/Event/Response.php
new file mode 100755 (executable)
index 0000000..097e253
--- /dev/null
@@ -0,0 +1,953 @@
+<?php
+/**
+ * Phergie 
+ *
+ * PHP version 5
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.
+ * It is also available through the world-wide-web at this URL:
+ * http://phergie.org/license
+ *
+ * @category  Phergie 
+ * @package   Phergie
+ * @author    Phergie Development Team <team@phergie.org>
+ * @copyright 2008-2010 Phergie Development Team (http://phergie.org)
+ * @license   http://phergie.org/license New BSD License
+ * @link      http://pear.phergie.org/package/Phergie
+ */
+
+/**
+ * Event originating from the server in response to an event sent by the
+ * current client.
+ *
+ * @category Phergie 
+ * @package  Phergie
+ * @author   Phergie Development Team <team@phergie.org>
+ * @license  http://phergie.org/license New BSD License
+ * @link     http://pear.phergie.org/package/Phergie
+ * @link     http://www.irchelp.org/irchelp/rfc/chapter6.html
+ */
+class Phergie_Event_Response extends Phergie_Event_Abstract
+{
+    /**
+     * <nickname> No such nick/channel
+     *
+     * Used to indicate the nickname parameter supplied to a command is currently
+     * unused.
+     */
+    const ERR_NOSUCHNICK = '401';
+
+    /**
+     * <server name> No such server
+     *
+     * Used to indicate the server name given currently doesn't exist.
+     */
+    const ERR_NOSUCHSERVER = '402';
+
+    /**
+     * <channel name> No such channel
+     *
+     * Used to indicate the given channel name is invalid.
+     */
+    const ERR_NOSUCHCHANNEL = '403';
+
+    /**
+     * <channel name> Cannot send to channel
+     *
+     * Sent to a user who is either (a) not on a channel which is mode +n or (b) not
+     * a chanop (or mode +v) on a channel which has mode +m set and is trying to send
+     * a PRIVMSG message to that channel.
+     */
+    const ERR_CANNOTSENDTOCHAN = '404';
+
+    /**
+     * <channel name> You have joined too many channels
+     *
+     * Sent to a user when they have joined the maximum number of allowed channels
+     * and they try to join another channel.
+     */
+    const ERR_TOOMANYCHANNELS = '405';
+
+    /**
+     * <nickname> There was no such nickname
+     *
+     * Returned by WHOWAS to indicate there is no history information for that
+     * nickname.
+     */
+    const ERR_WASNOSUCHNICK = '406';
+
+    /**
+     * <target> Duplicate recipients. No message delivered
+     *
+     * Returned to a client which is attempting to send PRIVMSG/NOTICE using the
+     * user@host destination format and for a user@host which has several
+     * occurrences.
+     */
+    const ERR_TOOMANYTARGETS = '407';
+
+    /**
+     * No origin specified
+     *
+     * PING or PONG message missing the originator parameter which is required since
+     * these commands must work without valid prefixes.
+     */
+    const ERR_NOORIGIN = '409';
+
+    /**
+     * No recipient given (<command>)
+     */
+    const ERR_NORECIPIENT = '411';
+
+    /**
+     * No text to send
+     */
+    const ERR_NOTEXTTOSEND = '412';
+
+    /**
+     * <mask> No toplevel domain specified
+     */
+    const ERR_NOTOPLEVEL = '413';
+
+    /**
+     * <mask> Wildcard in toplevel domain
+     *
+     * 412 - 414 are returned by PRIVMSG to indicate that the message wasn't
+     * delivered for some reason. ERR_NOTOPLEVEL and ERR_WILDTOPLEVEL are errors that
+     * are returned when an invalid use of "PRIVMSG $<server>" or "PRIVMSG #<host>"
+     * is attempted.
+     */
+    const ERR_WILDTOPLEVEL = '414';
+
+    /**
+     * <command> Unknown command
+     *
+     * Returned to a registered client to indicate that the command sent is unknown
+     * by the server.
+     */
+    const ERR_UNKNOWNCOMMAND = '421';
+
+    /**
+     * MOTD File is missing
+     *
+     * Server's MOTD file could not be opened by the server.
+     */
+    const ERR_NOMOTD = '422';
+
+    /**
+     * <server> No administrative info available
+     *
+     * Returned by a server in response to an ADMIN message when there is an error in
+     * finding the appropriate information.
+     */
+    const ERR_NOADMININFO = '423';
+
+    /**
+     * File error doing <file op> on <file>
+     *
+     * Generic error message used to report a failed file operation during the
+     * processing of a message.
+     */
+    const ERR_FILEERROR = '424';
+
+    /**
+     * No nickname given
+     *
+     * Returned when a nickname parameter expected for a command and isn't found.
+     */
+    const ERR_NONICKNAMEGIVEN = '431';
+
+    /**
+     * <nick> Erroneus nickname
+     *
+     * Returned after receiving a NICK message which contains characters which do not
+     * fall in the defined set. See section x.x.x for details on valid nicknames.
+     */
+    const ERR_ERRONEUSNICKNAME = '432';
+
+    /**
+     * <nick> Nickname is already in use
+     *
+     * Returned when a NICK message is processed that results in an attempt to change
+     * to a currently existing nickname.
+     */
+    const ERR_NICKNAMEINUSE = '433';
+
+    /**
+     * <nick> Nickname collision KILL
+     *
+     * Returned by a server to a client when it detects a nickname collision
+     * (registered of a NICK that already exists by another server).
+     */
+    const ERR_NICKCOLLISION = '436';
+
+    /**
+     * <nick> <channel> They aren't on that channel
+     *
+     * Returned by the server to indicate that the target user of the command is not
+     * on the given channel.
+     */
+    const ERR_USERNOTINCHANNEL = '441';
+
+    /**
+     * <channel> You're not on that channel
+     *
+     * Returned by the server whenever a client tries to perform a channel effecting
+     * command for which the client isn't a member.
+     */
+    const ERR_NOTONCHANNEL = '442';
+
+    /**
+     * <user> <channel> is already on channel
+     *
+     * Returned when a client tries to invite a user to a channel they are already
+     * on.
+     */
+    const ERR_USERONCHANNEL = '443';
+
+    /**
+     * <user> User not logged in
+     *
+     * Returned by the summon after a SUMMON command for a user was unable to be
+     * performed since they were not logged in.
+     */
+    const ERR_NOLOGIN = '444';
+
+    /**
+     * SUMMON has been disabled
+     *
+     * Returned as a response to the SUMMON command. Must be returned by any server
+     * which does not implement it.
+     */
+    const ERR_SUMMONDISABLED = '445';
+
+    /**
+     * USERS has been disabled
+     *
+     * Returned as a response to the USERS command. Must be returned by any server
+     * which does not implement it.
+     */
+    const ERR_USERSDISABLED = '446';
+
+    /**
+     * You have not registered
+     *
+     * Returned by the server to indicate that the client must be registered before
+     * the server will allow it to be parsed in detail.
+     */
+    const ERR_NOTREGISTERED = '451';
+
+    /**
+     * <command> Not enough parameters
+     *
+     * Returned by the server by numerous commands to indicate to the client that it
+     * didn't supply enough parameters.
+     */
+    const ERR_NEEDMOREPARAMS = '461';
+
+    /**
+     * You may not reregister
+     *
+     * Returned by the server to any link which tries to change part of the
+     * registered details (such as password or user details from second USER
+     * message).
+     */
+    const ERR_ALREADYREGISTRED = '462';
+
+    /**
+     * Your host isn't among the privileged
+     *
+     * Returned to a client which attempts to register with a server which does not
+     * been setup to allow connections from the host the attempted connection is
+     * tried.
+     */
+    const ERR_NOPERMFORHOST = '463';
+
+    /**
+     * Password incorrect
+     *
+     * Returned to indicate a failed attempt at registering a connection for which a
+     * password was required and was either not given or incorrect.
+     */
+    const ERR_PASSWDMISMATCH = '464';
+
+    /**
+     * You are banned from this server
+     *
+     * Returned after an attempt to connect and register yourself with a server which
+     * has been setup to explicitly deny connections to you.
+     */
+    const ERR_YOUREBANNEDCREEP = '465';
+
+    /**
+     * <channel> Channel key already set
+     */
+    const ERR_KEYSET = '467';
+
+    /**
+     * <channel> Cannot join channel (+l)
+     */
+    const ERR_CHANNELISFULL = '471';
+
+    /**
+     * <char> is unknown mode char to me
+     */
+    const ERR_UNKNOWNMODE = '472';
+
+    /**
+     * <channel> Cannot join channel (+i)
+     */
+    const ERR_INVITEONLYCHAN = '473';
+
+    /**
+     * <channel> Cannot join channel (+b)
+     */
+    const ERR_BANNEDFROMCHAN = '474';
+
+    /**
+     * <channel> Cannot join channel (+k)
+     */
+    const ERR_BADCHANNELKEY = '475';
+
+    /**
+     * Permission Denied- You're not an IRC operator
+     *
+     * Any command requiring operator privileges to operate must return this error to
+     * indicate the attempt was unsuccessful.
+     */
+    const ERR_NOPRIVILEGES = '481';
+
+    /**
+     * <channel> You're not channel operator
+     *
+     * Any command requiring 'chanop' privileges (such as MODE messages) must return
+     * this error if the client making the attempt is not a chanop on the specified
+     * channel.
+     */
+    const ERR_CHANOPRIVSNEEDED = '482';
+
+    /**
+     * You cant kill a server!
+     *
+     * Any attempts to use the KILL command on a server are to be refused and this
+     * error returned directly to the client.
+     */
+    const ERR_CANTKILLSERVER = '483';
+
+    /**
+     * No O-lines for your host
+     *
+     * If a client sends an OPER message and the server has not been configured to
+     * allow connections from the client's host as an operator, this error must be
+     * returned.
+     */
+    const ERR_NOOPERHOST = '491';
+
+    /**
+     * Unknown MODE flag
+     *
+     * Returned by the server to indicate that a MODE message was sent with a
+     * nickname parameter and that the a mode flag sent was not recognized.
+     */
+    const ERR_UMODEUNKNOWNFLAG = '501';
+
+    /**
+     * Cant change mode for other users
+     *
+     * Error sent to any user trying to view or change the user mode for a user other
+     * than themselves.
+     */
+    const ERR_USERSDONTMATCH = '502';
+
+    /**
+     * Dummy reply number. Not used.
+     */
+    const RPL_NONE = '300';
+
+    /**
+     * [<reply>{<space><reply>}]
+     *
+     * Reply format used by USERHOST to list replies to the query list. The reply
+     * string is composed as follows <reply> = <nick>['*'] '=' <'+'|'-'><hostname>
+     * The '*' indicates whether the client has registered as an Operator. The '-' or
+     * '+' characters represent whether the client has set an AWAY message or not
+     * respectively.
+     */
+    const RPL_USERHOST = '302';
+
+    /**
+     * [<nick> {<space><nick>}]
+     *
+     * Reply format used by ISON to list replies to the query list.
+     */
+    const RPL_ISON = '303';
+
+    /**
+     * <nick> <away message>
+     */
+    const RPL_AWAY = '301';
+
+    /**
+     * You are no longer marked as being away
+     */
+    const RPL_UNAWAY = '305';
+
+    /**
+     * You have been marked as being away
+     *
+     * These replies are used with the AWAY command (if allowed). RPL_AWAY is sent to
+     * any client sending a PRIVMSG to a client which is away. RPL_AWAY is only sent
+     * by the server to which the client is connected. Replies RPL_UNAWAY and
+     * RPL_NOWAWAY are sent when the client removes and sets an AWAY message.
+     */
+    const RPL_NOWAWAY = '306';
+
+    /**
+     * <nick> <user> <host> * <real name>
+     */
+    const RPL_WHOISUSER = '311';
+
+    /**
+     * <nick> <server> <server info>
+     */
+    const RPL_WHOISSERVER = '312';
+
+    /**
+     * <nick> is an IRC operator
+     */
+    const RPL_WHOISOPERATOR = '313';
+
+    /**
+     * <nick> <integer> seconds idle
+     */
+    const RPL_WHOISIDLE = '317';
+
+    /**
+     * <nick> End of /WHOIS list
+     */
+    const RPL_ENDOFWHOIS = '318';
+
+    /**
+     * <nick> {[@|+]<channel><space>}
+     *
+     * Replies 311 - 313, 317 - 319 are all replies generated in response to a WHOIS
+     * message. Given that there are enough parameters present, the answering server
+     * must either formulate a reply out of the above numerics (if the query nick is
+     * found) or return an error reply. The '*' in RPL_WHOISUSER is there as the
+     * literal character and not as a wild card. For each reply set, only
+     * RPL_WHOISCHANNELS may appear more than once (for long lists of channel names).
+     * The '@' and '+' characters next to the channel name indicate whether a client
+     * is a channel operator or has been granted permission to speak on a moderated
+     * channel. The RPL_ENDOFWHOIS reply is used to mark the end of processing a
+     * WHOIS message.
+     */
+    const RPL_WHOISCHANNELS = '319';
+
+    /**
+     * <nick> <user> <host> * <real name>
+     */
+    const RPL_WHOWASUSER = '314';
+
+    /**
+     * <nick> End of WHOWAS
+     *
+     * When replying to a WHOWAS message, a server must use the replies
+     * RPL_WHOWASUSER, RPL_WHOISSERVER or ERR_WASNOSUCHNICK for each nickname in the
+     * presented list. At the end of all reply batches, there must be RPL_ENDOFWHOWAS
+     * (even if there was only one reply and it was an error).
+     */
+    const RPL_ENDOFWHOWAS = '369';
+
+    /**
+     * Channel Users Name
+     */
+    const RPL_LISTSTART = '321';
+
+    /**
+     * <channel> <# visible> <topic>
+     */
+    const RPL_LIST = '322';
+
+    /**
+     * End of /LIST
+     *
+     * Replies RPL_LISTSTART, RPL_LIST, RPL_LISTEND mark the start, actual replies
+     * with data and end of the server's response to a LIST command. If there are no
+     * channels available to return, only the start and end reply must be sent.
+     */
+    const RPL_LISTEND = '323';
+
+    /**
+     * <channel> <mode> <mode params>
+     */
+    const RPL_CHANNELMODEIS = '324';
+
+    /**
+     * <channel> No topic is set
+     */
+    const RPL_NOTOPIC = '331';
+
+    /**
+     * <channel> <topic>
+     *
+     * When sending a TOPIC message to determine the channel topic, one of two
+     * replies is sent. If the topic is set, RPL_TOPIC is sent back else RPL_NOTOPIC.
+     */
+    const RPL_TOPIC = '332';
+
+    /**
+     * <channel> <nick>
+     *
+     * Returned by the server to indicate that the attempted INVITE message was
+     * successful and is being passed onto the end client.
+     */
+    const RPL_INVITING = '341';
+
+    /**
+     * <user> Summoning user to IRC
+     *
+     * Returned by a server answering a SUMMON message to indicate that it is
+     * summoning that user.
+     */
+    const RPL_SUMMONING = '342';
+
+    /**
+     * <version>.<debuglevel> <server> <comments>
+     *
+     * Reply by the server showing its version details. The <version> is the version
+     * of the software being used (including any patchlevel revisions) and the
+     * <debuglevel> is used to indicate if the server is running in "debug mode". The
+     * "comments" field may contain any comments about the version or further version
+     * details.
+     */
+    const RPL_VERSION = '351';
+
+    /**
+     * <channel> <user> <host> <server> <nick> <H|G>[*][@|+] <hopcount> <real name>
+     */
+    const RPL_WHOREPLY = '352';
+
+    /**
+     * <name> End of /WHO list
+     *
+     * The RPL_WHOREPLY and RPL_ENDOFWHO pair are used to answer a WHO message. The
+     * RPL_WHOREPLY is only sent if there is an appropriate match to the WHO query.
+     * If there is a list of parameters supplied with a WHO message, a RPL_ENDOFWHO
+     * must be sent after processing each list item with <name> being the item.
+     */
+    const RPL_ENDOFWHO = '315';
+
+    /**
+     * <channel> [[@|+]<nick> [[@|+]<nick> [...]]]
+     */
+    const RPL_NAMREPLY = '353';
+
+    /**
+     * <channel> End of /NAMES list
+     *
+     * To reply to a NAMES message, a reply pair consisting of RPL_NAMREPLY and
+     * RPL_ENDOFNAMES is sent by the server back to the client. If there is no
+     * channel found as in the query, then only RPL_ENDOFNAMES is returned. The
+     * exception to this is when a NAMES message is sent with no parameters and all
+     * visible channels and contents are sent back in a series of RPL_NAMEREPLY
+     * messages with a RPL_ENDOFNAMES to mark the end.
+     */
+    const RPL_ENDOFNAMES = '366';
+
+    /**
+     * <mask> <server> <hopcount> <server info>
+     */
+    const RPL_LINKS = '364';
+
+    /**
+     * <mask> End of /LINKS list
+     *
+     * In replying to the LINKS message, a server must send replies back using the
+     * RPL_LINKS numeric and mark the end of the list using an RPL_ENDOFLINKS reply.v
+     */
+    const RPL_ENDOFLINKS = '365';
+
+    /**
+     * <channel> <banid>
+     */
+    const RPL_BANLIST = '367';
+
+    /**
+     * <channel> End of channel ban list
+     *
+     * When listing the active 'bans' for a given channel, a server is required to
+     * send the list back using the RPL_BANLIST and RPL_ENDOFBANLIST messages. A
+     * separate RPL_BANLIST is sent for each active banid. After the banids have been
+     * listed (or if none present) a RPL_ENDOFBANLIST must be sent.
+     */
+    const RPL_ENDOFBANLIST = '368';
+
+    /**
+     * <string>
+     */
+    const RPL_INFO = '371';
+
+    /**
+     * End of /INFO list
+     *
+     * A server responding to an INFO message is required to send all its 'info' in a
+     * series of RPL_INFO messages with a RPL_ENDOFINFO reply to indicate the end of
+     * the replies.
+     */
+    const RPL_ENDOFINFO = '374';
+
+    /**
+     * - <server> Message of the day -
+     */
+    const RPL_MOTDSTART = '375';
+
+    /**
+     * - <text>
+     */
+    const RPL_MOTD = '372';
+
+    /**
+     * End of /MOTD command
+     *
+     * When responding to the MOTD message and the MOTD file is found, the file is
+     * displayed line by line, with each line no longer than 80 characters, using
+     * RPL_MOTD format replies. These should be surrounded by a RPL_MOTDSTART (before
+     * the RPL_MOTDs) and an RPL_ENDOFMOTD (after).
+     */
+    const RPL_ENDOFMOTD = '376';
+
+    /**
+     * You are now an IRC operator
+     *
+     * RPL_YOUREOPER is sent back to a client which has just successfully issued an
+     * OPER message and gained operator status.
+     */
+    const RPL_YOUREOPER = '381';
+
+    /**
+     * <config file> Rehashing
+     *
+     * If the REHASH option is used and an operator sends a REHASH message, an
+     * RPL_REHASHING is sent back to the operator.
+     */
+    const RPL_REHASHING = '382';
+
+    /**
+     * <server> <string showing server's local time>
+     *
+     * When replying to the TIME message, a server must send the reply using the
+     * RPL_TIME format above. The string showing the time need only contain the
+     * correct day and time there. There is no further requirement for the time
+     * string.
+     */
+    const RPL_TIME = '391';
+
+    /**
+     * UserID Terminal Host
+     */
+    const RPL_USERSSTART = '392';
+
+    /**
+     * %-8s %-9s %-8s
+     */
+    const RPL_USERS = '393';
+
+    /**
+     * End of users
+     */
+    const RPL_ENDOFUSERS = '394';
+
+    /**
+     * Nobody logged in
+     *
+     * If the USERS message is handled by a server, the replies RPL_USERSTART,
+     * RPL_USERS, RPL_ENDOFUSERS and RPL_NOUSERS are used. RPL_USERSSTART must be
+     * sent first, following by either a sequence of RPL_USERS or a single
+     * RPL_NOUSER. Following this is RPL_ENDOFUSERS.
+     */
+    const RPL_NOUSERS = '395';
+
+    /**
+     * Link <version & debug level> <destination> <next server>
+     */
+    const RPL_TRACELINK = '200';
+
+    /**
+     * Try. <class> <server>
+     */
+    const RPL_TRACECONNECTING = '201';
+
+    /**
+     * H.S. <class> <server>
+     */
+    const RPL_TRACEHANDSHAKE = '202';
+
+    /**
+     * ???? <class> [<client IP address in dot form>]
+     */
+    const RPL_TRACEUNKNOWN = '203';
+
+    /**
+     * Oper <class> <nick>
+     */
+    const RPL_TRACEOPERATOR = '204';
+
+    /**
+     * User <class> <nick>
+     */
+    const RPL_TRACEUSER = '205';
+
+    /**
+     * Serv <class> <int>S <int>C <server> <nick!user|*!*>@<host|server>
+     */
+    const RPL_TRACESERVER = '206';
+
+    /**
+     * <newtype> 0 <client name>
+     */
+    const RPL_TRACENEWTYPE = '208';
+
+    /**
+     * File <logfile> <debug level>
+     *
+     * The RPL_TRACE* are all returned by the server in response to the TRACE
+     * message. How many are returned is dependent on the the TRACE message and
+     * whether it was sent by an operator or not. There is no predefined order for
+     * which occurs first. Replies RPL_TRACEUNKNOWN, RPL_TRACECONNECTING and
+     * RPL_TRACEHANDSHAKE are all used for connections which have not been fully
+     * established and are either unknown, still attempting to connect or in the
+     * process of completing the 'server handshake'. RPL_TRACELINK is sent by any
+     * server which handles a TRACE message and has to pass it on to another server.
+     * The list of RPL_TRACELINKs sent in response to a TRACE command traversing the
+     * IRC network should reflect the actual connectivity of the servers themselves
+     * along that path. RPL_TRACENEWTYPE is to be used for any connection which does
+     * not fit in the other categories but is being displayed anyway.
+     */
+    const RPL_TRACELOG = '261';
+
+    /**
+     * <linkname> <sendq> <sent messages> <sent bytes> <received messages> <received
+     * bytes> <time open>
+     */
+    const RPL_STATSLINKINFO = '211';
+
+    /**
+     * <command> <count>
+     */
+    const RPL_STATSCOMMANDS = '212';
+
+    /**
+     * C <host> * <name> <port> <class>
+     */
+    const RPL_STATSCLINE = '213';
+
+    /**
+     * N <host> * <name> <port> <class>
+     */
+    const RPL_STATSNLINE = '214';
+
+    /**
+     * I <host> * <host> <port> <class>
+     */
+    const RPL_STATSILINE = '215';
+
+    /**
+     * K <host> * <username> <port> <class>
+     */
+    const RPL_STATSKLINE = '216';
+
+    /**
+     * Y <class> <ping frequency> <connect frequency> <max sendq>
+     */
+    const RPL_STATSYLINE = '218';
+
+    /**
+     * <stats letter> End of /STATS report
+     */
+    const RPL_ENDOFSTATS = '219';
+
+    /**
+     * L <hostmask> * <servername> <maxdepth>
+     */
+    const RPL_STATSLLINE = '241';
+
+    /**
+     * Server Up %d days %d%02d%02d
+     */
+    const RPL_STATSUPTIME = '242';
+
+    /**
+     * O <hostmask> * <name>
+     */
+    const RPL_STATSOLINE = '243';
+
+    /**
+     * H <hostmask> * <servername>
+     */
+    const RPL_STATSHLINE = '244';
+
+    /**
+     * <user mode string>
+     *
+     * To answer a query about a client's own mode, RPL_UMODEIS is sent back.
+     */
+    const RPL_UMODEIS = '221';
+
+    /**
+     * There are <integer> users and <integer> invisible on <integer> servers
+     */
+    const RPL_LUSERCLIENT = '251';
+
+    /**
+     * <integer> operator(s) online
+     */
+    const RPL_LUSEROP = '252';
+
+    /**
+     * <integer> unknown connection(s)
+     */
+    const RPL_LUSERUNKNOWN = '253';
+
+    /**
+     * <integer> channels formed
+     */
+    const RPL_LUSERCHANNELS = '254';
+
+    /**
+     * I have <integer> clients and <integer> servers
+     *
+     * In processing an LUSERS message, the server sends a set of replies from
+     * RPL_LUSERCLIENT, RPL_LUSEROP, RPL_USERUNKNOWN, RPL_LUSERCHANNELS and
+     * RPL_LUSERME. When replying, a server must send back RPL_LUSERCLIENT and
+     * RPL_LUSERME. The other replies are only sent back if a non-zero count is found
+     * for them.
+     */
+    const RPL_LUSERME = '255';
+
+    /**
+     * <server> Administrative info
+     */
+    const RPL_ADMINME = '256';
+
+    /**
+     * <admin info>
+     */
+    const RPL_ADMINLOC1 = '257';
+
+    /**
+     * <admin info>
+     */
+    const RPL_ADMINLOC2 = '258';
+
+    /**
+     * <admin info>
+     *
+     * When replying to an ADMIN message, a server is expected to use replies
+     * RLP_ADMINME through to RPL_ADMINEMAIL and provide a text message with each.
+     * For RPL_ADMINLOC1 a description of what city, state and country the server is
+     * in is expected, followed by details of the university and department
+     * (RPL_ADMINLOC2) and finally the administrative contact for the server (an
+     * email address here is required) in RPL_ADMINEMAIL.
+     */
+    const RPL_ADMINEMAIL = '259';
+
+    /**
+     * Reply code sent by the server, which can be compared to the ERR_* and
+     * RPL_* constants
+     *
+     * @var string
+     */
+    protected $code;
+
+    /**
+     * Reply code description sent by the server.
+     *
+     * @var string
+     */
+    protected $description;
+
+    /**
+     * Raw data sent by the server
+     *
+     * @var string
+     */
+    protected $rawData;
+
+    /**
+     * Event type
+     *
+     * @var string
+     */
+    protected $type = 'response';
+
+    /**
+     * Sets the reply code sent by the server.
+     *
+     * @param string $code Reply code
+     *
+     * @return Phergie_Event_Response Provides a fluent interface
+     */
+    public function setCode($code)
+    {
+        $this->code = $code;
+        return $this;
+    }
+
+    /**
+     * Returns the reply code sent by the server.
+     *
+     * @return string
+     */
+    public function getCode()
+    {
+        return $this->code;
+    }
+
+    /**
+     * Sets the reply code description sent by the server.
+     *
+     * @param string $description Reply code description
+     *
+     * @return Phergie_Event_Response Provides a fluent interface
+     */
+    public function setDescription($description)
+    {
+        $this->description = $description;
+        return $this;
+    }
+
+    /**
+     * Returns the reply code description sent by the server.
+     *
+     * @return string
+     */
+    public function getDescription()
+    {
+        return $this->description;
+    }
+
+    /**
+     * Sets the raw buffer for the given event
+     *
+     * @param string $buffer Raw event buffer
+     *
+     * @return Phergie_Event_Response Provides a fluent interface
+     */
+    public function setRawData($buffer)
+    {
+        $this->rawData = $buffer;
+        return $this;
+    }
+
+    /**
+     * Returns the raw buffer that was sent from the server for that event
+     *
+     * @return string
+     */
+    public function getRawData()
+    {
+        return $this->rawData;
+    }
+}
diff --git a/plugins/Irc/extlib/phergie/Phergie/Exception.php b/plugins/Irc/extlib/phergie/Phergie/Exception.php
new file mode 100755 (executable)
index 0000000..f4d71e5
--- /dev/null
@@ -0,0 +1,33 @@
+<?php
+/**
+ * Phergie 
+ *
+ * PHP version 5
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.
+ * It is also available through the world-wide-web at this URL:
+ * http://phergie.org/license
+ *
+ * @category  Phergie 
+ * @package   Phergie
+ * @author    Phergie Development Team <team@phergie.org>
+ * @copyright 2008-2010 Phergie Development Team (http://phergie.org)
+ * @license   http://phergie.org/license New BSD License
+ * @link      http://pear.phergie.org/package/Phergie
+ */
+
+/**
+ * Base class for all Phergie-related exceptions.
+ *
+ * @category Phergie 
+ * @package  Phergie
+ * @author   Phergie Development Team <team@phergie.org>
+ * @license  http://phergie.org/license New BSD License
+ * @link     http://pear.phergie.org/package/Phergie
+ */
+class Phergie_Exception extends Exception
+{
+}
diff --git a/plugins/Irc/extlib/phergie/Phergie/Hostmask.php b/plugins/Irc/extlib/phergie/Phergie/Hostmask.php
new file mode 100755 (executable)
index 0000000..b13842f
--- /dev/null
@@ -0,0 +1,217 @@
+<?php
+/**
+ * Phergie 
+ *
+ * PHP version 5
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.
+ * It is also available through the world-wide-web at this URL:
+ * http://phergie.org/license
+ *
+ * @category  Phergie 
+ * @package   Phergie
+ * @author    Phergie Development Team <team@phergie.org>
+ * @copyright 2008-2010 Phergie Development Team (http://phergie.org)
+ * @license   http://phergie.org/license New BSD License
+ * @link      http://pear.phergie.org/package/Phergie
+ */
+
+/**
+ * Data structure for a hostmask.
+ *
+ * @category Phergie 
+ * @package  Phergie
+ * @author   Phergie Development Team <team@phergie.org>
+ * @license  http://phergie.org/license New BSD License
+ * @link     http://pear.phergie.org/package/Phergie
+ */
+class Phergie_Hostmask
+{
+    /**
+     * Host
+     *
+     * @var string
+     */
+    protected $host;
+
+    /**
+     * Nick
+     *
+     * @var string
+     */
+    protected $nick;
+
+    /**
+     * Username
+     *
+     * @var string
+     */
+    protected $username;
+
+    /**
+     * Regular expression used to parse a hostmask
+     *
+     * @var string
+     */
+    protected static $regex = '/^([^!@]+)!(?:[ni]=)?([^@]+)@([^ ]+)/';
+
+    /**
+     * Constructor to initialize components of the hostmask.
+     *
+     * @param string $nick     Nick component
+     * @param string $username Username component
+     * @param string $host     Host component
+     *
+     * @return void
+     */
+    public function __construct($nick, $username, $host)
+    {
+        $this->nick = $nick;
+        $this->username = $username;
+        $this->host = $host;
+    }
+
+    /**
+     * Returns whether a given string appears to be a valid hostmask.
+     *
+     * @param string $string Alleged hostmask string
+     *
+     * @return bool TRUE if the string appears to be a valid hostmask, FALSE 
+     *         otherwise
+     */
+    public static function isValid($string)
+    {
+        return (preg_match(self::$regex, $string) > 0);
+    }
+
+    /**
+     * Parses a string containing the entire hostmask into a new instance of 
+     * this class.
+     *
+     * @param string $hostmask Entire hostmask including the nick, username, 
+     *        and host components
+     *
+     * @return Phergie_Hostmask New instance populated with data parsed from 
+     *         the provided hostmask string
+     * @throws Phergie_Hostmask_Exception
+     */
+    public static function fromString($hostmask)
+    {
+        if (preg_match(self::$regex, $hostmask, $match)) {
+            list(, $nick, $username, $host) = $match; 
+            return new self($nick, $username, $host);
+        }
+
+        throw new Phergie_Hostmask_Exception(
+            'Invalid hostmask specified: "' . $hostmask . '"',
+            Phergie_Hostmask_Exception::ERR_INVALID_HOSTMASK
+        );
+    }
+
+    /**
+     * Sets the hostname.
+     *
+     * @param string $host Hostname
+     *
+     * @return Phergie_Hostmask Provides a fluent interface
+     */
+    public function setHost($host)
+    {
+        $this->host = $host;
+
+        return $this;
+    }
+
+    /**
+     * Returns the hostname.
+     *
+     * @return string
+     */
+    public function getHost()
+    {
+        return $this->host;
+    }
+
+    /**
+     * Sets the username of the user.
+     *
+     * @param string $username Username
+     *
+     * @return Phergie_Hostmask Provides a fluent interface
+     */
+    public function setUsername($username)
+    {
+        $this->username = $username;
+
+        return $this;
+    }
+
+    /**
+     * Returns the username of the user.
+     *
+     * @return string
+     */
+    public function getUsername()
+    {
+        return $this->username;
+    }
+
+    /**
+     * Sets the nick of the user.
+     *
+     * @param string $nick User nick
+     *
+     * @return Phergie_Hostmask Provides a fluent interface
+     */
+    public function setNick($nick)
+    {
+        $this->nick = $nick;
+
+        return $this;
+    }
+
+    /**
+     * Returns the nick of the user.
+     *
+     * @return string
+     */
+    public function getNick()
+    {
+        return $this->nick;
+    }
+
+    /**
+     * Returns the hostmask for the originating server or user.
+     *
+     * @return string
+     */
+    public function __toString()
+    {
+        return $this->nick . '!' . $this->username . '@' . $this->host;
+    }
+
+    /**
+     * Returns whether a given hostmask matches a given pattern.
+     *
+     * @param string $pattern  Pattern using conventions of a ban mask where 
+     *        represents a wildcard
+     * @param string $hostmask Optional hostmask to match against, if not 
+     *        the current hostmask instance
+     *
+     * @return bool TRUE if the hostmask matches the pattern, FALSE otherwise
+     * @link http://irchelp.org/irchelp/rfc/chapter4.html#c4_2_3 Examples
+     */
+    public function matches($pattern, $hostmask = null)
+    {
+        if (!$hostmask) {
+            $hostmask = (string) $this;
+        }
+
+        $pattern = str_replace('*', '.*', $pattern);
+
+        return (preg_match('#^' . $pattern . '$#', $hostmask) > 0);
+    }
+}
diff --git a/plugins/Irc/extlib/phergie/Phergie/Hostmask/Exception.php b/plugins/Irc/extlib/phergie/Phergie/Hostmask/Exception.php
new file mode 100644 (file)
index 0000000..590f020
--- /dev/null
@@ -0,0 +1,37 @@
+<?php
+/**
+ * Phergie 
+ *
+ * PHP version 5
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.
+ * It is also available through the world-wide-web at this URL:
+ * http://phergie.org/license
+ *
+ * @category  Phergie 
+ * @package   Phergie
+ * @author    Phergie Development Team <team@phergie.org>
+ * @copyright 2008-2010 Phergie Development Team (http://phergie.org)
+ * @license   http://phergie.org/license New BSD License
+ * @link      http://pear.phergie.org/package/Phergie
+ */
+
+/**
+ * Exception related to hostmask handling.
+ *
+ * @category Phergie 
+ * @package  Phergie
+ * @author   Phergie Development Team <team@phergie.org>
+ * @license  http://phergie.org/license New BSD License
+ * @link     http://pear.phergie.org/package/Phergie
+ */
+class Phergie_Hostmask_Exception extends Phergie_Exception
+{
+    /**
+     * Error indicating that an invalid hostmask string was specified
+     */
+    const ERR_INVALID_HOSTMASK = 1;
+}
diff --git a/plugins/Irc/extlib/phergie/Phergie/Plugin/Abstract.php b/plugins/Irc/extlib/phergie/Phergie/Plugin/Abstract.php
new file mode 100755 (executable)
index 0000000..b7105ec
--- /dev/null
@@ -0,0 +1,605 @@
+<?php
+/**
+ * Phergie
+ *
+ * PHP version 5
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.
+ * It is also available through the world-wide-web at this URL:
+ * http://phergie.org/license
+ *
+ * @category  Phergie
+ * @package   Phergie
+ * @author    Phergie Development Team <team@phergie.org>
+ * @copyright 2008-2010 Phergie Development Team (http://phergie.org)
+ * @license   http://phergie.org/license New BSD License
+ * @link      http://pear.phergie.org/package/Phergie
+ */
+
+/**
+ * Base class for plugins to provide event handler stubs and commonly needed
+ * functionality.
+ *
+ * @category Phergie
+ * @package  Phergie
+ * @author   Phergie Development Team <team@phergie.org>
+ * @license  http://phergie.org/license New BSD License
+ * @link     http://pear.phergie.org/package/Phergie
+ */
+abstract class Phergie_Plugin_Abstract
+{
+    /**
+     * Current configuration handler
+     *
+     * @var Phergie_Config
+     */
+    protected $config;
+
+    /**
+     * Plugin handler used to provide access to other plugins
+     *
+     * @var Phergie_Plugin_Handler
+     */
+    protected $plugins;
+
+    /**
+     * Current event handler instance for outgoing events
+     *
+     * @var Phergie_Event_Handler
+     */
+    protected $events;
+
+    /**
+     * Current connection instance
+     *
+     * @var Phergie_Connection
+     */
+    protected $connection;
+
+    /**
+     * Current incoming event being handled
+     *
+     * @var Phergie_Event_Request|Phergie_Event_Response
+     */
+    protected $event;
+
+    /**
+     * Plugin short name
+     *
+     * @var string
+     */
+    protected $name;
+
+    /**
+     * Returns the short name for the plugin based on its class name.
+     *
+     * @return string
+     */
+    public function getName()
+    {
+        if (empty($this->name)) {
+            $this->name = substr(strrchr(get_class($this), '_'), 1);
+        }
+        return $this->name;
+    }
+
+    /**
+     * Sets the short name for the plugin.
+     *
+     * @param string $name Plugin short name
+     *
+     * @return Phergie_Plugin_Abstract Provides a fluent interface
+     */
+    public function setName($name)
+    {
+        $this->name = (string) $name;
+        return $this;
+    }
+
+    /**
+     * Indicates that the plugin failed to load due to an unsatisfied
+     * runtime requirement, such as a missing dependency.
+     *
+     * @param string $message Error message to provide more information
+     *        about the reason for the failure
+     *
+     * @return Phergie_Plugin_Abstract Provides a fluent interface
+     * @throws Phergie_Plugin_Exception Always
+     */
+    protected function fail($message)
+    {
+        throw new Phergie_Plugin_Exception(
+            $message,
+            Phergie_Plugin_Exception::ERR_REQUIREMENT_UNSATISFIED
+        );
+    }
+
+    /**
+     * Sets the current configuration handler.
+     *
+     * @param Phergie_Config $config Configuration handler
+     *
+     * @return Phergie_Plugin_Abstract Provides a fluent interface
+     */
+    public function setConfig(Phergie_Config $config)
+    {
+        $this->config = $config;
+        return $this;
+    }
+
+    /**
+     * Returns the current configuration handler or the value of a single
+     * setting from it.
+     *
+     * @param string $name    Optional name of a setting for which the value
+     *        should be returned instead of the entire configuration handler
+     * @param mixed  $default Optional default value to return if no value
+     *        is set for the setting indicated by $name
+     *
+     * @return Phergie_Config|mixed Configuration handler or value of the
+     *         setting specified by $name
+     * @throws Phergie_Plugin_Exception No configuration handler has been set
+     */
+    public function getConfig($name = null, $default = null)
+    {
+        if (empty($this->config)) {
+            throw new Phergie_Plugin_Exception(
+                'Configuration handler cannot be accessed before one is set',
+                Phergie_Plugin_Exception::ERR_NO_CONFIG_HANDLER
+            );
+        }
+        if (!is_null($name)) {
+            if (!isset($this->config[$name])) {
+                return $default;
+            }
+            return $this->config[$name];
+        }
+        return $this->config;
+    }
+
+    /**
+     * Sets the current plugin handler.
+     *
+     * @param Phergie_Plugin_Handler $handler Plugin handler
+     *
+     * @return Phergie_Plugin_Abstract Provides a fluent interface
+     */
+    public function setPluginHandler(Phergie_Plugin_Handler $handler)
+    {
+        $this->plugins = $handler;
+        return $this;
+    }
+
+    /**
+     * Returns the current plugin handler.
+     *
+     * @return Phergie_Plugin_Handler
+     * @throws Phergie_Plugin_Exception No plugin handler has been set
+     */
+    public function getPluginHandler()
+    {
+        if (empty($this->plugins)) {
+            throw new Phergie_Plugin_Exception(
+                'Plugin handler cannot be accessed before one is set',
+                Phergie_Plugin_Exception::ERR_NO_PLUGIN_HANDLER
+            );
+        }
+        return $this->plugins;
+    }
+
+    /**
+     * Sets the current event handler.
+     *
+     * @param Phergie_Event_Handler $handler Event handler
+     *
+     * @return Phergie_Plugin_Abstract Provides a fluent interface
+     */
+    public function setEventHandler(Phergie_Event_Handler $handler)
+    {
+        $this->events = $handler;
+        return $this;
+    }
+
+    /**
+     * Returns the current event handler.
+     *
+     * @return Phergie_Event_Handler
+     * @throws Phergie_Plugin_Exception No event handler has been set
+     */
+    public function getEventHandler()
+    {
+        if (empty($this->events)) {
+            throw new Phergie_Plugin_Exception(
+                'Event handler cannot be accessed before one is set',
+                Phergie_Plugin_Exception::ERR_NO_EVENT_HANDLER
+            );
+        }
+        return $this->events;
+    }
+
+    /**
+     * Sets the current connection.
+     *
+     * @param Phergie_Connection $connection Connection
+     *
+     * @return Phergie_Plugin_Abstract Provides a fluent interface
+     */
+    public function setConnection(Phergie_Connection $connection)
+    {
+        $this->connection = $connection;
+        return $this;
+    }
+
+    /**
+     * Returns the current event connection.
+     *
+     * @return Phergie_Connection
+     * @throws Phergie_Plugin_Exception No connection has been set
+     */
+    public function getConnection()
+    {
+        if (empty($this->connection)) {
+            throw new Phergie_Plugin_Exception(
+                'Connection cannot be accessed before one is set',
+                Phergie_Plugin_Exception::ERR_NO_CONNECTION
+            );
+        }
+        return $this->connection;
+    }
+
+    /**
+     * Sets the current incoming event to be handled.
+     *
+     * @param Phergie_Event_Request|Phergie_Event_Response $event Event
+     *
+     * @return Phergie_Plugin_Abstract Provides a fluent interface
+     */
+    public function setEvent($event)
+    {
+        $this->event = $event;
+        return $this;
+    }
+
+    /**
+     * Returns the current incoming event to be handled.
+     *
+     * @return Phergie_Event_Request|Phergie_Event_Response
+     */
+    public function getEvent()
+    {
+        if (empty($this->event)) {
+            throw new Phergie_Plugin_Exception(
+                'Event cannot be accessed before one is set',
+                Phergie_Plugin_Exception::ERR_NO_EVENT
+            );
+        }
+        return $this->event;
+    }
+
+    /**
+     * Provides do* methods with signatures identical to those of
+     * Phergie_Driver_Abstract but that queue up events to be dispatched
+     * later.
+     *
+     * @param string $name Name of the method called
+     * @param array  $args Arguments passed in the call
+     *
+     * @return mixed
+     */
+    public function __call($name, array $args)
+    {
+        $subcmd = substr($name, 0, 2);
+        if ($subcmd == 'do') {
+            $type = strtolower(substr($name, 2));
+            $this->getEventHandler()->addEvent($this, $type, $args);
+        } else if ($subcmd != 'on') {
+            throw new Phergie_Plugin_Exception(
+                'Called invalid method ' . $name . ' in ' . get_class($this),
+                Phergie_Plugin_Exception::ERR_INVALID_CALL
+            );
+        }
+    }
+
+    /**
+     * Handler for when the plugin is initially loaded - useful for checking
+     * runtime dependencies or performing any setup necessary for the plugin
+     * to function properly such as initializing a database.
+     *
+     * @return void
+     */
+    public function onLoad()
+    {
+    }
+
+    /**
+     * Handler for when the bot initially connects to a server.
+     *
+     * @return void
+     */
+    public function onConnect()
+    {
+    }
+
+    /**
+     * Handler for each tick, a single iteration of the continuous loop
+     * executed by the bot to receive, handle, and send events - useful for
+     * repeated execution of tasks on a time interval.
+     *
+     * @return void
+     */
+    public function onTick()
+    {
+    }
+
+    /**
+     * Handler for when any event is received but has not yet been dispatched
+     * to the plugin handler method specific to its event type.
+     *
+     * @return bool|null|void FALSE to short-circuit further event
+     *         processing, TRUE or NULL otherwise
+     */
+    public function preEvent()
+    {
+    }
+
+    /**
+     * Handler for after plugin processing of an event has concluded but
+     * before any events triggered in response by plugins are sent to the
+     * server - useful for modifying outgoing events before they are sent.
+     *
+     * @return void
+     */
+    public function preDispatch()
+    {
+    }
+
+    /**
+     * Handler for after any events triggered by plugins in response to a
+     * received event are sent to the server.
+     *
+     * @return void
+     */
+    public function postDispatch()
+    {
+    }
+
+    /**
+     * Handler for when the server prompts the client for a nick.
+     *
+     * @return void
+     * @link http://irchelp.org/irchelp/rfc/chapter4.html#c4_1_2
+     */
+    public function onNick()
+    {
+    }
+
+    /**
+     * Handler for when a user obtains operator privileges.
+     *
+     * @return void
+     * @link http://irchelp.org/irchelp/rfc/chapter4.html#c4_1_5
+     */
+    public function onOper()
+    {
+    }
+
+    /**
+     * Handler for when the client session is about to be terminated.
+     *
+     * @return void
+     * @link http://irchelp.org/irchelp/rfc/chapter4.html#c4_1_6
+     */
+    public function onQuit()
+    {
+    }
+
+    /**
+     * Handler for when a user joins a channel.
+     *
+     * @return void
+     * @link http://irchelp.org/irchelp/rfc/chapter4.html#c4_2_1
+     */
+    public function onJoin()
+    {
+    }
+
+    /**
+     * Handler for when a user leaves a channel.
+     *
+     * @return void
+     * @link http://irchelp.org/irchelp/rfc/chapter4.html#c4_2_2
+     */
+    public function onPart()
+    {
+    }
+
+    /**
+     * Handler for when a user or channel mode is changed.
+     *
+     * @return void
+     * @link http://irchelp.org/irchelp/rfc/chapter4.html#c4_2_3
+     */
+    public function onMode()
+    {
+    }
+
+    /**
+     * Handler for when a channel topic is viewed or changed.
+     *
+     * @return void
+     * @link http://irchelp.org/irchelp/rfc/chapter4.html#c4_2_4
+     */
+    public function onTopic()
+    {
+    }
+
+    /**
+     * Handler for when a message is received from a channel or user.
+     *
+     * @return void
+     * @link http://irchelp.org/irchelp/rfc/chapter4.html#c4_4_1
+     */
+    public function onPrivmsg()
+    {
+    }
+
+    /**
+     * Handler for when the bot receives a CTCP ACTION request.
+     *
+     * @return void
+     * @link http://www.invlogic.com/irc/ctcp.html#4.4
+     */
+    public function onAction()
+    {
+    }
+
+    /**
+     * Handler for when a notice is received.
+     *
+     * @return void
+     * @link http://irchelp.org/irchelp/rfc/chapter4.html#c4_4_2
+     */
+    public function onNotice()
+    {
+    }
+
+    /**
+     * Handler for when a user is kicked from a channel.
+     *
+     * @return void
+     * @link http://irchelp.org/irchelp/rfc/chapter4.html#c4_2_8
+     */
+    public function onKick()
+    {
+    }
+
+    /**
+     * Handler for when the bot receives a ping event from a server, at
+     * which point it is expected to respond with a pong request within
+     * a short period else the server may terminate its connection.
+     *
+     * @return void
+     * @link http://irchelp.org/irchelp/rfc/chapter4.html#c4_6_2
+     */
+    public function onPing()
+    {
+    }
+
+    /**
+     * Handler for when the bot receives a CTCP TIME request.
+     *
+     * @return void
+     * @link http://www.invlogic.com/irc/ctcp.html#4.6
+     */
+    public function onTime()
+    {
+    }
+
+    /**
+     * Handler for when the bot receives a CTCP VERSION request.
+     *
+     * @return void
+     * @link http://www.invlogic.com/irc/ctcp.html#4.1
+     */
+    public function onVersion()
+    {
+    }
+
+    /**
+     * Handler for when the bot receives a CTCP PING request.
+     *
+     * @return void
+     * @link http://www.invlogic.com/irc/ctcp.html#4.2
+     */
+    public function onCtcpPing()
+    {
+    }
+
+    /**
+     * Handler for when the bot receives a CTCP request of an unknown type.
+     *
+     * @return void
+     * @link http://www.invlogic.com/irc/ctcp.html
+     */
+    public function onCtcp()
+    {
+    }
+
+    /**
+     * Handler for when a reply is received for a CTCP PING request sent by
+     * the bot.
+     *
+     * @return void
+     * @link http://www.invlogic.com/irc/ctcp.html#4.2
+     */
+    public function onPingReply()
+    {
+    }
+
+    /**
+     * Handler for when a reply is received for a CTCP TIME request sent by
+     * the bot.
+     *
+     * @return void
+     * @link http://www.invlogic.com/irc/ctcp.html#4.6
+     */
+    public function onTimeReply()
+    {
+    }
+
+    /**
+     * Handler for when a reply is received for a CTCP VERSION request sent
+     * by the bot.
+     *
+     * @return void
+     * @link http://www.invlogic.com/irc/ctcp.html#4.1
+     */
+    public function onVersionReply()
+    {
+    }
+
+    /**
+     * Handler for when a reply received for a CTCP request of an unknown
+     * type.
+     *
+     * @return void
+     * @link http://www.invlogic.com/irc/ctcp.html
+     */
+    public function onCtcpReply()
+    {
+    }
+
+    /**
+     * Handler for when the bot receives a kill request from a server.
+     *
+     * @return void
+     * @link http://irchelp.org/irchelp/rfc/chapter4.html#c4_6_1
+     */
+    public function onKill()
+    {
+    }
+
+    /**
+     * Handler for when the bot receives an invitation to join a channel.
+     *
+     * @return void
+     * @link http://irchelp.org/irchelp/rfc/chapter4.html#c4_2_7
+     */
+    public function onInvite()
+    {
+    }
+
+    /**
+     * Handler for when a server response is received to a command issued by
+     * the bot.
+     *
+     * @return void
+     * @link http://irchelp.org/irchelp/rfc/chapter6.html
+     */
+    public function onResponse()
+    {
+    }
+}
diff --git a/plugins/Irc/extlib/phergie/Phergie/Plugin/Acl.php b/plugins/Irc/extlib/phergie/Phergie/Plugin/Acl.php
new file mode 100755 (executable)
index 0000000..e209e32
--- /dev/null
@@ -0,0 +1,186 @@
+<?php
+/**
+ * Phergie
+ *
+ * PHP version 5
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.
+ * It is also available through the world-wide-web at this URL:
+ * http://phergie.org/license
+ *
+ * @category  Phergie
+ * @package   Phergie_Plugin_Acl
+ * @author    Phergie Development Team <team@phergie.org>
+ * @copyright 2008-2010 Phergie Development Team (http://phergie.org)
+ * @license   http://phergie.org/license New BSD License
+ * @link      http://pear.phergie.org/package/Phergie_Plugin_Acl
+ */
+
+/**
+ * Provides an access control system to limit reponses to events based on
+ * the users who originate them.
+ *
+ * Configuration settings:
+ * acl.whitelist - mapping of user hostmask patterns (optionally by host) to
+ *                 plugins and methods where those plugins and methods will
+ *                 only be accessible to those users (i.e. and inaccessible
+ *                 to other users)
+ * acl.blacklist - mapping of user hostmasks (optionally by host) to plugins
+ *                 and methods where where those plugins and methods will be
+ *                 inaccessible to those users but accessible to other users
+ * acl.ops       - TRUE to automatically give access to whitelisted plugins
+ *                 and methods to users with ops for events they initiate in
+ *                 channels where they have ops
+ *
+ * The whitelist and blacklist settings are formatted like so:
+ * <code>
+ * 'acl.whitelist' => array(
+ *     'hostname1' => array(
+ *         'pattern1' => array(
+ *             'plugins' => array(
+ *                 'ShortPluginName'
+ *             ),
+ *             'methods' => array(
+ *                 'methodName'
+ *             )
+ *         ),
+ *     )
+ * ),
+ * </code>
+ *
+ * The hostname array dimension is optional; if not used, rules will be
+ * applied across all connections. The pattern is a user hostmask pattern
+ * where asterisks (*) are used for wildcards. Plugins and methods do not
+ * need to be set to empty arrays if they are not used; simply exclude them.
+ *
+ * @category Phergie
+ * @package  Phergie_Plugin_Acl
+ * @author   Phergie Development Team <team@phergie.org>
+ * @license  http://phergie.org/license New BSD License
+ * @link     http://pear.phergie.org/package/Phergie_Plugin_Acl
+ * @uses     Phergie_Plugin_UserInfo pear.phergie.org
+ */
+class Phergie_Plugin_Acl extends Phergie_Plugin_Abstract
+{
+    /**
+     * Checks for permission settings and removes the plugin if none are set.
+     *
+     * @return void
+     */
+    public function onLoad()
+    {
+        $this->plugins->getPlugin('UserInfo');
+
+        if (!$this->getConfig('acl.blacklist')
+            && !$this->getConfig('acl.whitelist')
+        ) {
+            $this->plugins->removePlugin($this);
+        }
+    }
+
+    /**
+     * Applies a set of rules to a plugin handler iterator.
+     *
+     * @param Phergie_Plugin_Iterator $iterator Iterator to receive rules
+     * @param array                   $rules    Associate array containing
+     *        either a 'plugins' key pointing to an array containing plugin
+     *        short names to filter, a 'methods' key pointing to an array
+     *        containing method names to filter, or both
+     *
+     * @return void
+     */
+    protected function applyRules(Phergie_Plugin_Iterator $iterator, array $rules)
+    {
+        if (!empty($rules['plugins'])) {
+            $iterator->addPluginFilter($rules['plugins']);
+        }
+        if (!empty($rules['methods'])) {
+            $iterator->addMethodFilter($rules['methods']);
+        }
+    }
+
+    /**
+     * Checks permission settings and short-circuits event processing for
+     * blacklisted users.
+     *
+     * @return void
+     */
+    public function preEvent()
+    {
+        // Ignore server responses
+        if ($this->event instanceof Phergie_Event_Response) {
+            return;
+        }
+
+        // Ignore server-initiated events
+        if (!$this->event->isFromUser()) {
+            return;
+        }
+
+        // Get the iterator used to filter plugins when processing events
+        $iterator = $this->plugins->getIterator();
+
+        // Get configuration setting values
+        $whitelist = $this->getConfig('acl.whitelist', array());
+        $blacklist = $this->getConfig('acl.blacklist', array());
+        $ops = $this->getConfig('acl.ops', false);
+
+        // Support host-specific lists
+        $host = $this->connection->getHost();
+        foreach (array('whitelist', 'blacklist') as $var) {
+            foreach ($$var as $pattern => $rules) {
+                $regex = '/^' . str_replace('*', '.*', $pattern) . '$/i';
+                if (preg_match($regex, $host)) {
+                    ${$var} = ${$var}[$pattern];
+                    break;
+                }
+            }
+        }
+
+        // Get information on the user initiating the current event
+        $hostmask = $this->event->getHostmask();
+        $isOp = $ops
+              && $this->event->isInChannel()
+              && $this->plugins->userInfo->isOp(
+                $this->event->getNick(),
+                $this->event->getSource()
+              );
+
+        // Filter whitelisted commands if the user is not on the whitelist
+        if (!$isOp) {
+            $whitelisted = false;
+            foreach ($whitelist as $pattern => $rules) {
+                if ($hostmask->matches($pattern)) {
+                    $whitelisted = true;
+                }
+            }
+            if (!$whitelisted) {
+                foreach ($whitelist as $pattern => $rules) {
+                    $this->applyRules($iterator, $rules);
+                }
+            }
+        }
+
+        // Filter blacklisted commands if the user is on the blacklist
+        $blacklisted = false;
+        foreach ($blacklist as $pattern => $rules) {
+            if ($hostmask->matches($pattern)) {
+                $this->applyRules($iterator, $rules);
+                break;
+            }
+        }
+    }
+
+    /**
+     * Clears filters on the plugin handler iterator.
+     *
+     * @return void
+     */
+    public function postDispatch()
+    {
+        $this->plugins->getIterator()->clearFilters();
+    }
+}
diff --git a/plugins/Irc/extlib/phergie/Phergie/Plugin/AltNick.php b/plugins/Irc/extlib/phergie/Phergie/Plugin/AltNick.php
new file mode 100755 (executable)
index 0000000..16d5f9b
--- /dev/null
@@ -0,0 +1,95 @@
+<?php
+/**
+ * Phergie 
+ *
+ * PHP version 5
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.
+ * It is also available through the world-wide-web at this URL:
+ * http://phergie.org/license
+ *
+ * @category  Phergie 
+ * @package   Phergie_Plugin_AltNick
+ * @author    Phergie Development Team <team@phergie.org>
+ * @copyright 2008-2010 Phergie Development Team (http://phergie.org)
+ * @license   http://phergie.org/license New BSD License
+ * @link      http://pear.phergie.org/package/Phergie_Plugin_AltNick
+ */
+
+/**
+ * Handles switching to alternate nicks in cases where the primary nick is 
+ * not available for use.
+ *
+ * @category Phergie 
+ * @package  Phergie_Plugin_AltNick
+ * @author   Phergie Development Team <team@phergie.org>
+ * @license  http://phergie.org/license New BSD License
+ * @link     http://pear.phergie.org/package/Phergie_Plugin_AltNick
+ * @uses     extension spl 
+ */
+class Phergie_Plugin_AltNick extends Phergie_Plugin_Abstract
+{
+    /**
+     * Iterator for the alternate nick list
+     *
+     * @var ArrayIterator 
+     */
+    protected $iterator;
+
+    /**
+     * Initializes instance variables.
+     *
+     * @return void
+     */
+    public function onConnect()
+    {
+        if ($this->config['altnick.nicks']) {
+            if (is_string($this->config['altnick.nicks'])) {
+                $this->config['altnick.nicks'] 
+                    = array($this->config['altnick.nicks']);
+            }
+            $this->iterator = new ArrayIterator($this->config['altnick.nicks']);
+        }
+    }
+
+    /**
+     * Switches to alternate nicks as needed when nick collisions occur.
+     *
+     * @return void
+     */
+    public function onResponse()
+    {
+        // If no alternate nick list was found, return
+        if (empty($this->iterator)) {
+            return;
+        }
+
+        // If the response event indicates that the nick set is in use...
+        $code = $this->getEvent()->getCode();
+        if ($code == Phergie_Event_Response::ERR_NICKNAMEINUSE) {
+
+            // Attempt to move to the next nick in the alternate nick list
+            $this->iterator->next();
+
+            // If another nick is available...
+            if ($this->iterator->valid()) {
+                
+                // Switch to the new nick
+                $altNick = $this->iterator->current();
+                $this->doNick($altNick);
+
+                // Update the connection to reflect the nick change
+                $this->getConnection()->setNick($altNick);
+
+            } else {
+                // If no other nicks are available...
+
+                // Terminate the connection
+                $this->doQuit('All specified alternate nicks are in use');
+            }
+        }
+    }
+}
diff --git a/plugins/Irc/extlib/phergie/Phergie/Plugin/AudioScrobbler.php b/plugins/Irc/extlib/phergie/Phergie/Plugin/AudioScrobbler.php
new file mode 100755 (executable)
index 0000000..ed4030a
--- /dev/null
@@ -0,0 +1,191 @@
+<?php
+/**
+ * Phergie
+ *
+ * PHP version 5
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.
+ * It is also available through the world-wide-web at this URL:
+ * http://phergie.org/license
+ *
+ * @category  Phergie
+ * @package   Phergie_Plugin_AudioScrobbler
+ * @author    Phergie Development Team <team@phergie.org>
+ * @copyright 2008-2010 Phergie Development Team (http://phergie.org)
+ * @license   http://phergie.org/license New BSD License
+ * @link      http://pear.phergie.org/package/Phergie_Plugin_AudioScrobbler
+ */
+
+/**
+ * Provides commands to look up information on tracks played by specific 
+ * users on the Last.fm and Libre.fm services.
+ *
+ * TODO: Make the "nick-binding" use an SQLite database instead of having them
+ *       hard-coded in to the config file.
+ * 
+ * Configuration settings:
+ * "audioscrobbler.lastfm_api_key":  API given by last.fm (string).
+ * "audioscrobbler.librefm_api_key": API key given by libre.fm (string).
+ * 
+ * @category Phergie
+ * @package  Phergie_Plugin_AudioScrobbler
+ * @author   Phergie Development Team <team@phergie.org>
+ * @license  http://phergie.org/license New BSD License
+ * @link     http://pear.phergie.org/package/Phergie_Plugin_AudioScrobbler
+ * @uses     Phergie_Plugin_Command pear.phergie.org
+ * @uses     Phergie_Plugin_Http pear.phergie.org
+ * @uses     extension simplexml
+ */
+class Phergie_Plugin_AudioScrobbler extends Phergie_Plugin_Abstract
+{
+    /**
+     * Last.FM API entry point
+     *
+     * @var string
+     */
+    protected $lastfmUrl = 'http://ws.audioscrobbler.com/2.0/';
+    
+    /**
+     * Libre.FM API entry point
+     *
+     * @var string
+     */
+    protected $librefmUrl = 'http://alpha.dev.libre.fm/2.0/';
+    
+    /**
+     * Scrobbler query string for user.getRecentTracks
+     *
+     * @var string
+     */
+    protected $query = '?method=user.getrecenttracks&user=%s&api_key=%s';
+
+    /**
+     * HTTP plugin
+     *
+     * @var Phergie_Plugin_Http
+     */
+    protected $http;
+    
+    /**
+     * Check for dependencies.
+     *
+     * @return void
+     */
+    public function onLoad()
+    {
+        if (!extension_loaded('simplexml')) {
+            $this->fail('SimpleXML php extension is required');
+        }
+        
+        $plugins = $this->getPluginHandler();
+        $plugins->getPlugin('Command');
+        $this->http = $plugins->getPlugin('Http');
+    }
+    
+    /**
+     * Command function to get a user's status on last.fm.
+     * 
+     * @param string $user User identifier
+     *
+     * @return void
+     */
+    public function onCommandLastfm($user = null)
+    {
+        if ($key = $this->config['audioscrobbler.lastfm_api_key']) {
+            $scrobbled = $this->getScrobbled($user, $this->lastfmUrl, $key);
+            if ($scrobbled) {
+                $this->doPrivmsg($this->getEvent()->getSource(), $scrobbled);
+            }
+        }
+    }
+
+    /**
+     * Command function to get a user's status on libre.fm.
+     * 
+     * @param string $user User identifier
+     *
+     * @return void
+     */
+    public function onCommandLibrefm($user = null)
+    {
+        if ($key = $this->config['audioscrobbler.librefm_api_key']) {
+            $scrobbled = $this->getScrobbled($user, $this->librefmUrl, $key);
+            if ($scrobbled) {
+                $this->doPrivmsg($this->getEvent()->getSource(), $scrobbled);
+            }
+        }
+    }
+
+    /**
+     * Simple Scrobbler API function to get a formatted string of the most 
+     * recent track played by a user.
+     * 
+     * @param string $user Username to look up
+     * @param string $url  Base URL of the scrobbler service
+     * @param string $key  Scrobbler service API key
+     *
+     * @return string Formatted string of the most recent track played
+     */
+    public function getScrobbled($user, $url, $key)
+    {
+        $event = $this->getEvent();
+        $user = $user ? $user : $event->getNick();
+        $url = sprintf($url . $this->query, urlencode($user), urlencode($key));
+
+        $response = $this->http->get($url);
+        if ($response->isError()) {
+            $this->doNotice(
+                $event->getNick(),
+                'Can\'t find status for ' . $user . ': HTTP ' . 
+                $response->getCode() . ' ' . $response->getMessage()
+            );
+            return false; 
+        }
+        
+        $xml = $response->getContent();
+        if ($xml->error) {
+            $this->doNotice(
+                $event->getNick(),
+                'Can\'t find status for ' . $user . ': API ' . $xml->error
+            );
+            return false; 
+        }
+        
+        $recenttracks = $xml->recenttracks;
+        $track = $recenttracks->track[0];
+        
+        // If the user exists but has not scrobbled anything, the result will
+        // be empty.
+        if (empty($track->name) && empty($track->artist)) {
+            $this->doNotice(
+                $event->getNick(),
+                'Can\'t find track information for ' . $recenttracks['user']
+            );
+            return false;
+        }
+        
+        if (isset($track['nowplaying'])) {
+            $msg = sprintf(
+                '%s is listening to %s by %s',
+                $recenttracks['user'],
+                $track->name,
+                $track->artist
+            );
+        } else {
+            $msg = sprintf(
+                '%s, %s was listening to %s by %s',
+                date('j M Y, H:i', (int) $track->date['uts']),
+                $recenttracks['user'],
+                $track->name,
+                $track->artist
+            );
+        }
+        if ($track->streamable == 1) {
+            $msg .= ' - ' . $track->url;
+        }
+        return $msg;
+    }
+}
diff --git a/plugins/Irc/extlib/phergie/Phergie/Plugin/AutoJoin.php b/plugins/Irc/extlib/phergie/Phergie/Plugin/AutoJoin.php
new file mode 100755 (executable)
index 0000000..a6d1adf
--- /dev/null
@@ -0,0 +1,69 @@
+<?php
+/**
+ * Phergie 
+ *
+ * PHP version 5
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.
+ * It is also available through the world-wide-web at this URL:
+ * http://phergie.org/license
+ *
+ * @category  Phergie 
+ * @package   Phergie_Plugin_AutoJoin
+ * @author    Phergie Development Team <team@phergie.org>
+ * @copyright 2008-2010 Phergie Development Team (http://phergie.org)
+ * @license   http://phergie.org/license New BSD License
+ * @link      http://pear.phergie.org/package/Phergie_Plugin_AutoJoin
+ */
+
+/**
+ * Automates the process of having the bot join one or more channels upon
+ * connection to the server.
+ *
+ * The configuration setting autojoin.channels is used to determine which 
+ * channels to join. This setting can point to a comma-delimited string or 
+ * enumerated array containing a single list of channels or an associative 
+ * array keyed by hostname where each value is a comma-delimited string or  
+ * enumerated array containing a list of channels to join on the server  
+ * corresponding to that hostname.
+ *
+ * @category Phergie 
+ * @package  Phergie_Plugin_AutoJoin
+ * @author   Phergie Development Team <team@phergie.org>
+ * @license  http://phergie.org/license New BSD License
+ * @link     http://pear.phergie.org/package/Phergie_Plugin_AutoJoin
+ */
+class Phergie_Plugin_AutoJoin extends Phergie_Plugin_Abstract
+{
+    /**
+     * Intercepts the end of the "message of the day" response and responds by
+     * joining the channels specified in the configuration file.
+     *
+     * @return void
+     */
+    public function onResponse()
+    {
+        switch ($this->getEvent()->getCode()) {
+        case Phergie_Event_Response::RPL_ENDOFMOTD:
+        case Phergie_Event_Response::ERR_NOMOTD:
+            if ($channels = $this->config['autojoin.channels']) {
+                if (is_array($channels)) {
+                    // Support autojoin.channels being in these formats:
+                    // 'hostname' => array('#channel1', '#channel2', ... )
+                    $host = $this->getConnection()->getHost();
+                    if (isset($channels[$host])) {
+                        $channels = $channels[$host];
+                    }
+                    if (is_array($channels)) {
+                        $channels = implode(',', $channels);
+                    }
+                }
+                $this->doJoin($channels);
+            }
+            $this->getPluginHandler()->removePlugin($this);
+        }
+    }
+}
diff --git a/plugins/Irc/extlib/phergie/Phergie/Plugin/Beer.php b/plugins/Irc/extlib/phergie/Phergie/Plugin/Beer.php
new file mode 100644 (file)
index 0000000..7213cde
--- /dev/null
@@ -0,0 +1,82 @@
+<?php
+/**
+ * Phergie
+ *
+ * PHP version 5
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.
+ * It is also available through the world-wide-web at this URL:
+ * http://phergie.org/license
+ *
+ * @category  Phergie
+ * @package   Phergie_Plugin_Beer
+ * @author    Phergie Development Team <team@phergie.org>
+ * @copyright 2008-2010 Phergie Development Team (http://phergie.org)
+ * @license   http://phergie.org/license New BSD License
+ * @link      http://pear.phergie.org/package/Phergie_Plugin_Beer
+ */
+
+/**
+ * Processes requests to serve users beer.
+ *
+ * @category Phergie
+ * @package  Phergie_Plugin_Beer
+ * @author   Phergie Development Team <team@phergie.org>
+ * @license  http://phergie.org/license New BSD License
+ * @link     http://pear.phergie.org/package/Phergie_Plugin_Beer
+ * @uses     Phergie_Plugin_Command pear.phergie.org
+ * @uses     Phergie_Plugin_Serve pear.phergie.org
+ */
+class Phergie_Plugin_Beer extends Phergie_Plugin_Abstract
+{
+    /**
+     * Checks for dependencies.
+     *
+     * @return void
+     */
+    public function onLoad()
+    {
+        $plugins = $this->plugins;
+        $plugins->getPlugin('Command');
+        $plugins->getPlugin('Serve');
+    }
+
+    /**
+     * Processes requests to serve a user a beer.
+     *
+     * @param string $request Request including the target and an optional
+     *        suggestion of what beer to serve
+     *
+     * @return void
+     */
+    public function onCommandBeer($request)
+    {
+        $format = $this->getConfig(
+            'beer.format',
+            'throws %target% %article% %item%.'
+        );
+
+        $this->plugins->getPlugin('Serve')->serve(
+            dirname(__FILE__) . '/Beer/beer.db',
+            'beer',
+            $format,
+            $request
+        );
+    }
+
+    /**
+     * Adds a "booze" alias for the "beer" command.
+     *
+     * @param string $request Request including the target and an optional
+     *        suggestion of what beer to serve
+     *
+     * @return void
+     */
+    public function onCommandBooze($request)
+    {
+        $this->onCommandBeer($request);
+    }
+}
diff --git a/plugins/Irc/extlib/phergie/Phergie/Plugin/Beer/db.php b/plugins/Irc/extlib/phergie/Phergie/Plugin/Beer/db.php
new file mode 100644 (file)
index 0000000..c7921e5
--- /dev/null
@@ -0,0 +1,81 @@
+<?php
+
+if (!defined('__DIR__')) {
+    define('__DIR__', dirname(__FILE__));
+}
+
+// Create database schema
+echo 'Creating database', PHP_EOL;
+$file = __DIR__ . '/beer.db';
+if (file_exists($file)) {
+    unlink($file);
+}
+$db = new PDO('sqlite:' . $file);
+$db->exec('CREATE TABLE beer (name VARCHAR(255), link VARCHAR(255))');
+$db->exec('CREATE UNIQUE INDEX beer_name ON beer (name)');
+$insert = $db->prepare('INSERT INTO beer (name, link) VALUES (:name, :link)');
+
+// Get raw beerme.com data set
+echo 'Downloading beerme.com data set', PHP_EOL;
+$file = __DIR__ . '/beerlist.txt';
+if (!file_exists($file)) {
+    copy('http://beerme.com/beerlist.php', $file);
+}
+$contents = file_get_contents($file);
+
+// Extract data from data set
+echo 'Processing beerme.com data', PHP_EOL;
+$contents = tidy_repair_string($contents);
+libxml_use_internal_errors(true);
+$doc = new DOMDocument;
+$doc->loadHTML($contents);
+libxml_clear_errors();
+$xpath = new DOMXPath($doc);
+$beers = $xpath->query('//table[@class="beerlist"]/tr/td[1]');
+$db->beginTransaction();
+foreach ($beers as $beer) {
+    $name = iconv('UTF-8', 'ISO-8859-1//TRANSLIT', $beer->textContent);
+    $name = preg_replace('/\h*\v+\h*/', '', $name);
+    $link = 'http://beerme.com' . $beer->childNodes->item(1)->getAttribute('href');
+    $insert->execute(array($name, $link));
+}
+$db->commit();
+
+// Clean up
+echo 'Cleaning up', PHP_EOL;
+unlink($file);
+
+// Get and decompress openbeerdb.com data set
+$archive = __DIR__ . '/beers.zip';
+if (!file_exists($archive)) {
+    echo 'Downloading openbeerdb.com data set', PHP_EOL;
+    copy('http://openbeerdb.googlecode.com/files/beers.zip', $archive);
+}
+
+echo 'Decompressing openbeerdb.com data set', PHP_EOL;
+$zip = new ZipArchive;
+$zip->open($archive);
+$zip->extractTo(__DIR__, 'beers/beers.csv');
+$zip->close();
+$file = __DIR__ . '/beers/beers.csv';
+
+// Extract data from data set
+echo 'Processing openbeerdb.com data', PHP_EOL;
+$fp = fopen($file, 'r');
+$columns = fgetcsv($fp, 0, '|');
+$db->beginTransaction();
+while ($line = fgetcsv($fp, 0, '|')) {
+    $line = array_combine($columns, $line);
+    $name = iconv('UTF-8', 'ISO-8859-1//TRANSLIT', $line['name']);
+    $name = preg_replace('/\h*\v+\h*/', '', $name);
+    $link = null;
+    $insert->execute(array($name, $link));
+}
+$db->commit();
+fclose($fp);
+
+// Clean up
+echo 'Cleaning up', PHP_EOL;
+unlink($file);
+unlink($archive);
+rmdir(__DIR__ . '/beers');
diff --git a/plugins/Irc/extlib/phergie/Phergie/Plugin/BeerScore.php b/plugins/Irc/extlib/phergie/Phergie/Plugin/BeerScore.php
new file mode 100644 (file)
index 0000000..16c671f
--- /dev/null
@@ -0,0 +1,156 @@
+<?php
+/**
+ * Phergie 
+ *
+ * PHP version 5
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.
+ * It is also available through the world-wide-web at this URL:
+ * http://phergie.org/license
+ *
+ * @category  Phergie 
+ * @package   Phergie_Plugin_BeerScore
+ * @author    Phergie Development Team <team@phergie.org> 
+ * @copyright 2008-2010 Phergie Development Team (http://phergie.org)
+ * @license   http://phergie.org/license New BSD License
+ * @link      http://pear.phergie.org/package/Phergie_Plugin_BeerScore
+ */
+
+/**
+ * Handles incoming requests for beer scores.
+ *
+ * @category Phergie 
+ * @package  Phergie_Plugin_BeerScore
+ * @author   Phergie Development Team <team@phergie.org> 
+ * @license  http://phergie.org/license New BSD License
+ * @link     http://pear.phergie.org/package/Phergie_Plugin_BeerScore
+ * @uses     Phergie_Plugin_Http pear.phergie.org
+ */
+class Phergie_Plugin_BeerScore extends Phergie_Plugin_Abstract
+{
+    /**
+     * Score result type
+     *
+     * @const string
+     */
+    const TYPE_SCORE = 'SCORE';
+
+    /**
+     * Search result type
+     *
+     * @const string
+     */
+    const TYPE_SEARCH = 'SEARCH';
+
+    /**
+     * Refine result type
+     *
+     * @const type
+     */
+    const TYPE_REFINE = 'REFINE';
+
+    /**
+     * Base API URL
+     *
+     * @const string
+     */
+    const API_BASE_URL = 'http://caedmon.net/beerscore/';
+
+    /**
+     * HTTP plugin
+     *
+     * @var Phergie_Plugin_Http
+     */
+    protected $http;
+
+    /** 
+     * Checks for dependencies.
+     *
+     * @return void
+     */
+    public function onLoad()
+    {
+        $this->http = $this->getPluginHandler()->getPlugin('Http');
+    }
+
+    /**
+     * Handles beerscore commands.
+     *
+     * @param string $searchstring String to use in seaching for beer scores
+     *
+     * @return void
+     */
+    public function onCommandBeerscore($searchstring)
+    {
+        $event = $this->getEvent();
+        $target = $event->getNick();
+        $source = $event->getSource();
+
+        $apiurl = self::API_BASE_URL . rawurlencode($searchstring);
+        $response = $this->http->get($apiurl);
+
+        if ($response->isError()) {
+            $this->doNotice($target, 'Score not found (or failed to contact API)');
+            return;
+        }
+
+        $result = $response->getContent();
+        switch ($result->type) {
+        case self::TYPE_SCORE:
+            // small enough number to get scores
+            foreach ($result->beer as $beer) {
+                if ($beer->score === -1) {
+                    $score = '(not rated)';
+                } else {
+                    $score = $beer->score;
+                }
+                $str 
+                    = "{$target}: rating for {$beer->name}" . 
+                    " = {$score} ({$beer->url})";
+                $this->doPrivmsg($source, $str);
+            }
+            break;
+
+        case self::TYPE_SEARCH:
+            // only beer names, no scores
+            $str = '';
+            $found = 0;
+            foreach ($result->beer as $beer) {
+                if (isset($beer->score)) {
+                    ++$found;
+                    if ($beer->score === -1) {
+                        $score = '(not rated)';
+                    } else {
+                        $score = $beer->score;
+                    }
+                    $str 
+                        = "{$target}: rating for {$beer->name}" . 
+                        " = {$score} ({$beer->url})";
+                    $this->doPrivmsg($source, $str);
+                } else {
+                    $str .= "({$beer->name} -> {$beer->url}) ";
+                }
+            }
+            $foundnum = $result->num - $found;
+            $more = $found ? 'more ' : '';
+            $str = "{$target}: {$foundnum} {$more}results... {$str}";
+            $this->doPrivmsg($source, $str);
+            break;
+
+        case self::TYPE_REFINE:
+            // Too many results; only output search URL
+            if ($result->num < 100) {
+                $num = $result->num;
+            } else {
+                $num = 'at least 100';
+            }
+            $resultsword = (($result->num > 1) ? 'results' : 'result');
+            $str = "{$target}: {$num} {$resultsword}; {$result->searchurl}";
+            $this->doPrivmsg($source, $str);
+            break;
+        }
+    }
+}
diff --git a/plugins/Irc/extlib/phergie/Phergie/Plugin/Cache.php b/plugins/Irc/extlib/phergie/Phergie/Plugin/Cache.php
new file mode 100644 (file)
index 0000000..2b54fab
--- /dev/null
@@ -0,0 +1,106 @@
+<?php
+/**
+ * Phergie
+ *
+ * PHP version 5
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.
+ * It is also available through the world-wide-web at this URL:
+ * http://phergie.org/license
+ *
+ * @category  Phergie
+ * @package   Phergie_Plugin_Cache
+ * @author    Phergie Development Team <team@phergie.org>
+ * @copyright 2008-2010 Phergie Development Team (http://phergie.org)
+ * @license   http://phergie.org/license New BSD License
+ * @link      http://pear.phergie.org/package/Phergie_Plugin_Cache
+ */
+
+/**
+ * Implements a generic cache to be used by other plugins.
+ *
+ * @category Phergie
+ * @package  Phergie_Plugin_Cache
+ * @author   Phergie Development Team <team@phergie.org>
+ * @license  http://phergie.org/license New BSD License
+ * @link     http://pear.phergie.org/package/Phergie_Plugin_Cache
+ */
+class Phergie_Plugin_Cache extends Phergie_Plugin_Abstract
+{
+    /**
+     * Key-value data storage for the cache 
+     * 
+     * @var array 
+     */
+    protected $cache = array();
+
+    /**
+     * Stores a value in the cache. 
+     *
+     * @param string   $key       Key to associate with the value 
+     * @param mixed    $data      Data to be stored
+     * @param int|null $ttl       Time to live in seconds or NULL for forever
+     * @param bool     $overwrite TRUE to overwrite any existing value 
+     *        associated with the specified key
+     *
+     * @return bool
+     */
+    public function store($key, $data, $ttl = 3600, $overwrite = true)
+    {
+        if (!$overwrite && isset($this->cache[$key])) {
+            return false;
+        }
+
+        if ($ttl) {
+            $expires = time()+$ttl;
+        } else {
+            $expires = null;
+        }
+
+        $this->cache[$key] = array('data' => $data, 'expires' => $expires);
+        return true;
+
+    }
+
+    /**
+     * Fetches a previously stored value. 
+     *
+     * @param string $key Key associated with the value 
+     *
+     * @return mixed Stored value or FALSE if no value or an expired value 
+     *         is associated with the specified key 
+     */
+    public function fetch($key)
+    {
+        if (!isset($this->cache[$key])) {
+            return false;
+        }
+
+        $item = $this->cache[$key];
+        if (!is_null($item['expires']) && $item['expires'] < time()) {
+            $this->expire($key);
+            return false;
+        }
+
+        return $item['data'];
+    }
+
+    /**
+     * Expires a value that has exceeded its time to live.
+     *
+     * @param string $key Key associated with the value to expire 
+     *
+     * @return bool
+     */
+    protected function expire($key)
+    {
+        if (!isset($this->cache[$key])) {
+            return false;
+        }
+        unset($this->cache[$key]);
+        return true;
+    }
+}
diff --git a/plugins/Irc/extlib/phergie/Phergie/Plugin/Caffeine.php b/plugins/Irc/extlib/phergie/Phergie/Plugin/Caffeine.php
new file mode 100644 (file)
index 0000000..2b76bd3
--- /dev/null
@@ -0,0 +1,69 @@
+<?php
+/**
+ * Phergie
+ *
+ * PHP version 5
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.
+ * It is also available through the world-wide-web at this URL:
+ * http://phergie.org/license
+ *
+ * @category  Phergie
+ * @package   Phergie_Plugin_Caffeine
+ * @author    Phergie Development Team <team@phergie.org>
+ * @copyright 2008-2010 Phergie Development Team (http://phergie.org)
+ * @license   http://phergie.org/license New BSD License
+ * @link      http://pear.phergie.org/package/Phergie_Plugin_Caffeine
+ */
+
+/**
+ * Processes requests to serve users caffeinated beverages.
+ *
+ * @category Phergie
+ * @package  Phergie_Plugin_Caffeine
+ * @author   Phergie Development Team <team@phergie.org>
+ * @license  http://phergie.org/license New BSD License
+ * @link     http://pear.phergie.org/package/Phergie_Plugin_Caffeine
+ * @uses     Phergie_Plugin_Command pear.phergie.org
+ * @uses     Phergie_Plugin_Serve pear.phergie.org
+ */
+class Phergie_Plugin_Caffeine extends Phergie_Plugin_Abstract
+{
+    /**
+     * Checks for dependencies.
+     *
+     * @return void
+     */
+    public function onLoad()
+    {
+        $plugins = $this->plugins;
+        $plugins->getPlugin('Command');
+        $plugins->getPlugin('Serve');
+    }
+
+    /**
+     * Processes requests to serve a user a caffeinated beverage.
+     *
+     * @param string $request Request including the target and an optional
+     *        suggestion of what caffeinated beverage to serve
+     *
+     * @return void
+     */
+    public function onCommandCaffeine($request)
+    {
+        $format = $this->getConfig(
+            'beer.format',
+            'throws %target% %article% %item%.'
+        );
+
+        $this->plugins->getPlugin('Serve')->serve(
+            dirname(__FILE__) . '/Caffeine/caffeine.db',
+            'caffeine',
+            $format,
+            $request
+        );
+    }
+}
diff --git a/plugins/Irc/extlib/phergie/Phergie/Plugin/Caffeine/db.php b/plugins/Irc/extlib/phergie/Phergie/Plugin/Caffeine/db.php
new file mode 100644 (file)
index 0000000..cdff52f
--- /dev/null
@@ -0,0 +1,51 @@
+<?php
+
+if (!defined('__DIR__')) {
+    define('__DIR__', dirname(__FILE__));
+}
+
+// Create database schema
+echo 'Creating database', PHP_EOL;
+$file = __DIR__ . '/caffeine.db';
+if (file_exists($file)) {
+    unlink($file);
+}
+$db = new PDO('sqlite:' . $file);
+$db->exec('CREATE TABLE caffeine (name VARCHAR(255), link VARCHAR(255))');
+$db->exec('CREATE UNIQUE INDEX caffeine_name ON caffeine (name)');
+$insert = $db->prepare('INSERT INTO caffeine (name, link) VALUES (:name, :link)');
+
+// Get raw energyfiend.com data set
+echo 'Downloading energyfiend.com data set', PHP_EOL;
+$file = __DIR__ . '/the-caffeine-database.html';
+if (!file_exists($file)) {
+    copy('http://www.energyfiend.com/the-caffeine-database', $file);
+}
+$contents = file_get_contents($file);
+
+// Extract data from data set
+echo 'Processing energyfiend.com data', PHP_EOL;
+$contents = tidy_repair_string($contents);
+libxml_use_internal_errors(true);
+$doc = new DOMDocument;
+$doc->loadHTML($contents);
+libxml_clear_errors();
+$xpath = new DOMXPath($doc);
+$caffeine = $xpath->query('//table[@id="caffeinedb"]//tr/td[1]');
+$db->beginTransaction();
+foreach ($caffeine as $drink) {
+    $name = iconv('UTF-8', 'ISO-8859-1//TRANSLIT', $drink->textContent);
+    $name = preg_replace('/\s*\v+\s*/', ' ', $name);
+    if ($drink->firstChild->nodeName == 'a') {
+        $link = 'http://energyfiend.com'
+              . $drink->firstChild->getAttribute('href');
+    } else {
+        $link = null;
+    }
+    $insert->execute(array($name, $link));
+}
+$db->commit();
+
+// Clean up
+echo 'Cleaning up', PHP_EOL;
+unlink($file);
diff --git a/plugins/Irc/extlib/phergie/Phergie/Plugin/Censor.php b/plugins/Irc/extlib/phergie/Phergie/Plugin/Censor.php
new file mode 100755 (executable)
index 0000000..99c69d8
--- /dev/null
@@ -0,0 +1,120 @@
+<?php
+/**
+ * Phergie
+ *
+ * PHP version 5
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.
+ * It is also available through the world-wide-web at this URL:
+ * http://phergie.org/license
+ *
+ * @category  Phergie
+ * @package   Phergie_Plugin_Censor
+ * @author    Phergie Development Team <team@phergie.org>
+ * @copyright 2008-2010 Phergie Development Team (http://phergie.org)
+ * @license   http://phergie.org/license New BSD License
+ * @link      http://pear.phergie.org/package/Phergie_Plugin_Censor
+ */
+
+/**
+ * Facilitates censoring of event content or discardment of events
+ * containing potentially offensive phrases depending on the value of the
+ * configuration setting censor.mode ('off', 'censor', 'discard'). Also
+ * provides access to a web service for detecting censored words so that
+ * other plugins may optionally integrate and adjust behavior accordingly to
+ * prevent discardment of events.
+ *
+ * @category Phergie
+ * @package  Phergie_Plugin_Censor
+ * @author   Phergie Development Team <team@phergie.org>
+ * @license  http://phergie.org/license New BSD License
+ * @link     http://pear.phergie.org/package/Phergie_Plugin_Censor
+ * @uses     extension soap
+ */
+class Phergie_Plugin_Censor extends Phergie_Plugin_Abstract
+{
+    /**
+     * SOAP client to interact with the CDYNE Profanity Filter API
+     *
+     * @var SoapClient
+     */
+    protected $soap;
+
+    /**
+     * Checks for dependencies.
+     *
+     * @return void
+     */
+    public function onLoad()
+    {
+        if (!extension_loaded('soap')) {
+            $this->fail('The PHP soap extension is required');
+        }
+
+        if (!in_array($this->config['censor.mode'], array('censor', 'discard'))) {
+            $this->plugins->removePlugin($this);
+        }
+    }
+
+    /**
+     * Returns a "clean" version of a given string.
+     *
+     * @param string $string String to clean
+     *
+     * @return string Cleaned string
+     */
+    public function cleanString($string)
+    {
+        if (empty($this->soap)) {
+            $this->soap = new SoapClient('http://ws.cdyne.com/ProfanityWS/Profanity.asmx?wsdl');
+        }
+        $params = array('Text' => $string);
+        $attempts = 0;
+        while ($attempts < 3) {
+            try {
+                $response = $this->soap->SimpleProfanityFilter($params);
+                break;
+            } catch (SoapFault $e) {
+                $attempts++;
+                sleep(1);
+            }
+        }
+        if ($attempts == 3) {
+            return $string;
+        }
+        return $response->SimpleProfanityFilterResult->CleanText;
+    }
+
+    /**
+     * Processes events before they are dispatched and either censors their
+     * content or discards them if they contain potentially offensive
+     * content.
+     *
+     * @return void
+     */
+    public function preDispatch()
+    {
+        $events = $this->events->getEvents();
+
+        foreach ($events as $event) {
+            switch ($event->getType()) {
+                case Phergie_Event_Request::TYPE_PRIVMSG:
+                case Phergie_Event_Request::TYPE_ACTION:
+                case Phergie_Event_Request::TYPE_NOTICE:
+                    $text = $event->getArgument(1);
+                    $clean = $this->cleanString($text);
+                    if ($text != $clean) {
+                        if ($this->config['censor.mode'] == 'censor') {
+                            $event->setArgument(1, $clean);
+                        } else {
+                            $this->events->removeEvent($event);
+                        }
+                    }
+                    break;
+            }
+        }
+    }
+}
diff --git a/plugins/Irc/extlib/phergie/Phergie/Plugin/Cocktail.php b/plugins/Irc/extlib/phergie/Phergie/Plugin/Cocktail.php
new file mode 100644 (file)
index 0000000..eafeb65
--- /dev/null
@@ -0,0 +1,71 @@
+<?php
+/**
+ * Phergie
+ *
+ * PHP version 5
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.
+ * It is also available through the world-wide-web at this URL:
+ * http://phergie.org/license
+ *
+ * @category  Phergie
+ * @package   Phergie_Plugin_Cocktail
+ * @author    Phergie Development Team <team@phergie.org>
+ * @copyright 2008-2010 Phergie Development Team (http://phergie.org)
+ * @license   http://phergie.org/license New BSD License
+ * @link      http://pear.phergie.org/package/Phergie_Plugin_Cocktail
+ */
+
+/**
+ * Processes requests to serve users cocktail.
+ *
+ * @category Phergie
+ * @package  Phergie_Plugin_Cocktail
+ * @author   Phergie Development Team <team@phergie.org>
+ * @license  http://phergie.org/license New BSD License
+ * @link     http://pear.phergie.org/package/Phergie_Plugin_Cocktail
+ * @uses     Phergie_Plugin_Command pear.phergie.org
+ * @uses     Phergie_Plugin_Serve pear.phergie.org
+ */
+class Phergie_Plugin_Cocktail extends Phergie_Plugin_Abstract
+{
+    /**
+     * Checks for dependencies.
+     *
+     * @return void
+     */
+    public function onLoad()
+    {
+        $plugins = $this->plugins;
+        $plugins->getPlugin('Command');
+        $plugins->getPlugin('Serve');
+    }
+
+    /**
+     * Processes requests to serve a user a cocktail.
+     *
+     * @param string $request Request including the target and an optional
+     *        suggestion of what cocktail to serve
+     *
+     * @return void
+     */
+    public function onCommandCocktail($request)
+    {
+        $format = $this->getConfig(
+            'cocktail.format',
+            'throws %target% %article% %item%.'
+        );
+
+        $this->plugins->getPlugin('Serve')->serve(
+            dirname(__FILE__) . '/Cocktail/cocktail.db',
+            'cocktail',
+            $format,
+            $request,
+            true
+        );
+    }
+}
+
diff --git a/plugins/Irc/extlib/phergie/Phergie/Plugin/Cocktail/db.php b/plugins/Irc/extlib/phergie/Phergie/Plugin/Cocktail/db.php
new file mode 100644 (file)
index 0000000..2e61dd0
--- /dev/null
@@ -0,0 +1,74 @@
+<?php
+
+if (!defined('__DIR__')) {
+    define('__DIR__', dirname(__FILE__));
+}
+
+// Create database schema
+echo 'Creating database', PHP_EOL;
+$file = __DIR__ . '/cocktail.db';
+if (file_exists($file)) {
+    unlink($file);
+}
+$db = new PDO('sqlite:' . $file);
+$db->exec('CREATE TABLE cocktail (name VARCHAR(255), link VARCHAR(255))');
+$db->exec('CREATE UNIQUE INDEX cocktail_name ON cocktail (name)');
+$insert = $db->prepare('INSERT INTO cocktail (name, link) VALUES (:name, :link)');
+
+// Get raw webtender.com data set
+echo 'Downloading webtender.com data set', PHP_EOL;
+$start = 1;
+do {
+    $file = __DIR__ . '/' . $start . '.html';
+    if (file_exists($file)) {
+        continue;
+    }
+    copy(
+        'http://www.webtender.com/db/browse?level=2&dir=drinks&char=%2A&start=' . $start,
+        $file
+    );
+    if (!isset($limit)) {
+        $contents = file_get_contents($file);
+        preg_match('/([0-9]+) found/', $contents, $match);
+        $limit = $match[1] + (150 - ($match[1] % 150));
+    }
+    echo 'Got records ', $start, ' - ', min($start + 150, $limit), ' of ', $limit, PHP_EOL;
+    $start += 150;
+} while ($start < $limit);
+
+// Extract data from data set
+$start = 1;
+while ($start < $limit) {
+    echo 'Processing ', $start, ' - ', min($start + 150, $limit), ' of ', $limit, PHP_EOL;
+
+    $file = __DIR__ . '/' . $start . '.html';
+    $contents = file_get_contents($file);
+    $contents = tidy_repair_string($contents);
+    libxml_use_internal_errors(true);
+    $doc = new DOMDocument;
+    $doc->loadHTML($contents);
+    libxml_clear_errors();
+    $xpath = new DOMXPath($doc);
+
+    $cocktails = $xpath->query('//li/a');
+    $db->beginTransaction();
+    foreach ($cocktails as $cocktail) {
+        $name = $cocktail->nodeValue;
+        $name = preg_replace('/ The$|^The |\s*\([^)]+\)\s*| #[0-9]+$/', '', $name);
+        $name = html_entity_decode($name);
+        $link = 'http://www.webtender.com' . $cocktail->getAttribute('href');
+        $insert->execute(array($name, $link));
+    }
+    $db->commit();
+
+    $start += 150;
+}
+
+// Clean up
+echo 'Cleaning up', PHP_EOL;
+$start = 1;
+while ($start < $limit) {
+    $file = __DIR__ . '/' . $start . '.html';
+    unlink($file);
+    $start += 150;
+}
diff --git a/plugins/Irc/extlib/phergie/Phergie/Plugin/Command.php b/plugins/Irc/extlib/phergie/Phergie/Plugin/Command.php
new file mode 100644 (file)
index 0000000..2058977
--- /dev/null
@@ -0,0 +1,146 @@
+<?php
+/**
+ * Phergie
+ *
+ * PHP version 5
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.
+ * It is also available through the world-wide-web at this URL:
+ * http://phergie.org/license
+ *
+ * @category  Phergie
+ * @package   Phergie_Plugin_Command
+ * @author    Phergie Development Team <team@phergie.org>
+ * @copyright 2008-2010 Phergie Development Team (http://phergie.org)
+ * @license   http://phergie.org/license New BSD License
+ * @link      http://pear.phergie.org/package/Phergie_Plugin_Command
+ */
+
+/**
+ * Handles parsing and execution of commands sent by users via messages sent
+ * to channels in which the bot is present or directly to the bot.
+ *
+ * @category Phergie
+ * @package  Phergie_Plugin_Command
+ * @author   Phergie Development Team <team@phergie.org>
+ * @license  http://phergie.org/license New BSD License
+ * @link     http://pear.phergie.org/package/Phergie_Plugin_Command
+ * @uses     extension reflection
+ * @uses     Phergie_Plugin_Message pear.phergie.org
+ */
+class Phergie_Plugin_Command extends Phergie_Plugin_Abstract
+{
+    /**
+     * Prefix for command method names
+     *
+     * @var string
+     */
+    const METHOD_PREFIX = 'onCommand';
+
+    /**
+     * Cache for command lookups used to confirm that methods exist and
+     * parameter counts match
+     *
+     * @var array
+     */
+    protected $methods = array();
+
+    /**
+     * Load the Message plugin
+     *
+     * @return void
+     */
+    public function onLoad()
+    {
+        $plugins = $this->getPluginHandler();
+        $plugins->getPlugin('Message');
+    }
+
+    /**
+     * Populates the methods cache.
+     *
+     * @return void
+     */
+    public function populateMethodCache()
+    {
+        foreach ($this->getPluginHandler()->getPlugins() as $plugin) {
+            $reflector = new ReflectionClass($plugin);
+            foreach ($reflector->getMethods() as $method) {
+                $name = $method->getName();
+                if (strpos($name, self::METHOD_PREFIX) === 0
+                    && !isset($this->methods[$name])
+                ) {
+                    $this->methods[$name] = array(
+                        'total' => $method->getNumberOfParameters(),
+                        'required' => $method->getNumberOfRequiredParameters()
+                    );
+                }
+            }
+        }
+    }
+
+    /**
+     * Parses a given message and, if its format corresponds to that of a
+     * defined command, calls the handler method for that command with any
+     * provided parameters.
+     *
+     * @return void
+     */
+    public function onPrivmsg()
+    {
+        // Populate the methods cache if needed
+        if (empty($this->methods)) {
+            $this->populateMethodCache();
+        }
+
+        // Check for a prefixed message
+        $msg = $this->plugins->message->getMessage();
+        if ($msg === false) {
+            return;
+        }
+
+        // Separate the command and arguments
+        $parsed = preg_split('/\s+/', $msg, 2);
+        $command = strtolower(array_shift($parsed));
+        $args = count($parsed) ? array_shift($parsed) : '';
+
+        // Resolve aliases to their corresponding commands
+        $aliases = $this->getConfig('command.aliases', array());
+        $result = preg_grep('/^' . preg_quote($command, '/') . '$/i', array_keys($aliases));
+        if ($result) {
+            $command = $aliases[array_shift($result)];
+        }
+
+        // Check to ensure the command exists
+        $method = self::METHOD_PREFIX . ucfirst($command);
+        if (empty($this->methods[$method])) {
+            return;
+        }
+
+        // If no arguments are passed...
+        if (empty($args)) {
+
+            // If the method requires no arguments, call it
+            if (empty($this->methods[$method]['required'])) {
+                $this->getPluginHandler()->$method();
+            }
+
+        } else {
+            // If arguments are passed...
+
+            // Parse the arguments
+            $args = preg_split('/\s+/', $args, $this->methods[$method]['total']);
+
+            // If the minimum arguments are passed, call the method
+            if ($this->methods[$method]['required'] <= count($args)) {
+                call_user_func_array(
+                    array($this->getPluginHandler(), $method),
+                    $args
+                );
+            }
+        }
+    }
+}
diff --git a/plugins/Irc/extlib/phergie/Phergie/Plugin/Cookie.php b/plugins/Irc/extlib/phergie/Phergie/Plugin/Cookie.php
new file mode 100644 (file)
index 0000000..4bc2cee
--- /dev/null
@@ -0,0 +1,69 @@
+<?php
+/**
+ * Phergie
+ *
+ * PHP version 5
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.
+ * It is also available through the world-wide-web at this URL:
+ * http://phergie.org/license
+ *
+ * @category  Phergie
+ * @package   Phergie_Plugin_Cookie
+ * @author    Phergie Development Team <team@phergie.org>
+ * @copyright 2008-2010 Phergie Development Team (http://phergie.org)
+ * @license   http://phergie.org/license New BSD License
+ * @link      http://pear.phergie.org/package/Phergie_Plugin_Cookie
+ */
+
+/**
+ * Processes requests to serve users cookies.
+ *
+ * @category Phergie
+ * @package  Phergie_Plugin_Cookie
+ * @author   Phergie Development Team <team@phergie.org>
+ * @license  http://phergie.org/license New BSD License
+ * @link     http://pear.phergie.org/package/Phergie_Plugin_Cookie
+ * @uses     Phergie_Plugin_Command pear.phergie.org
+ * @uses     Phergie_Plugin_Serve pear.phergie.org
+ */
+class Phergie_Plugin_Cookie extends Phergie_Plugin_Abstract
+{
+    /**
+     * Checks for dependencies.
+     *
+     * @return void
+     */
+    public function onLoad()
+    {
+        $plugins = $this->plugins;
+        $plugins->getPlugin('Command');
+        $plugins->getPlugin('Serve');
+    }
+
+    /**
+     * Processes requests to serve a user a cookie.
+     *
+     * @param string $request Request including the target and an optional
+     *        suggestion of what cookie to serve
+     *
+     * @return void
+     */
+    public function onCommandCookie($request)
+    {
+        $format = $this->getConfig(
+            'cookie.format',
+            'throws %target% %article% %item%.'
+        );
+
+        $this->plugins->getPlugin('Serve')->serve(
+            dirname(__FILE__) . '/Cookie/cookie.db',
+            'cookies',
+            $format,
+            $request
+        );
+    }
+}
diff --git a/plugins/Irc/extlib/phergie/Phergie/Plugin/Cookie/db.php b/plugins/Irc/extlib/phergie/Phergie/Plugin/Cookie/db.php
new file mode 100644 (file)
index 0000000..2776315
--- /dev/null
@@ -0,0 +1,55 @@
+<?php
+
+if (!defined('__DIR__')) {
+    define('__DIR__', dirname(__FILE__));
+}
+
+// Create database schema
+echo 'Creating database', PHP_EOL;
+$file = __DIR__ . '/cookie.db';
+if (file_exists($file)) {
+    unlink($file);
+}
+$db = new PDO('sqlite:' . $file);
+$db->exec('CREATE TABLE cookies (name VARCHAR(255), link VARCHAR(255))');
+$db->exec('CREATE UNIQUE INDEX cookie_name ON cookies (name)');
+$insert = $db->prepare('INSERT INTO cookies (name, link) VALUES (:name, :link)');
+
+// Get Cookies list from http://en.wikipedia.org/wiki/List_of_cookies
+echo 'Downloading data from Wikipedia', PHP_EOL;
+$file = __DIR__ . '/cookieslist.txt';
+if (!file_exists($file)) {
+    copy('http://en.wikipedia.org/wiki/List_of_cookies', $file);
+}
+$contents = file_get_contents($file);
+
+// Extract data from data set
+echo 'Processing Wikipedia\'s cookies list', PHP_EOL;
+$contents = tidy_repair_string($contents);
+libxml_use_internal_errors(true);
+$doc = new DOMDocument;
+$doc->loadHTML($contents);
+libxml_clear_errors();
+$xpath = new DOMXPath($doc);
+
+$cookies = $xpath->query('//table[@width="90%"]/tr/td[1]/a');
+
+foreach ($cookies as $cookie) {
+    $name = $cookie->textContent;
+    $name = str_replace(
+        array('(',')',"\n", 'cookies'),
+        array('','', ' ', 'cookie'),
+        $name
+    );
+    $name = iconv('UTF-8', 'ISO-8859-1//TRANSLIT', $name);
+    $name = trim($name);
+    $name = rtrim($name, 's');
+
+    $link =  'http://en.wikipedia.org' . $cookie->getAttribute('href');
+    $insert->execute(array($name, $link));
+    echo 'added [' . $name . '] -> '. $link . PHP_EOL;
+}
+
+// Clean up
+echo 'Cleaning up', PHP_EOL;
+unlink($file);
diff --git a/plugins/Irc/extlib/phergie/Phergie/Plugin/Cron.php b/plugins/Irc/extlib/phergie/Phergie/Plugin/Cron.php
new file mode 100644 (file)
index 0000000..1c27f97
--- /dev/null
@@ -0,0 +1,147 @@
+<?php
+/**
+ * Phergie
+ *
+ * PHP version 5
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.
+ * It is also available through the world-wide-web at this URL:
+ * http://phergie.org/license
+ *
+ * @category  Phergie
+ * @package   Phergie_Plugin_Cron
+ * @author    Phergie Development Team <team@phergie.org>
+ * @copyright 2008-2010 Phergie Development Team (http://phergie.org)
+ * @license   http://phergie.org/license New BSD License
+ * @link      http://pear.phergie.org/package/Phergie_Plugin_Cron
+ */
+
+/**
+ * Allows callbacks to be registered for asynchronous execution.
+ *
+ * @category Phergie
+ * @package  Phergie_Plugin_Cron
+ * @author   Phergie Development Team <team@phergie.org>
+ * @license  http://phergie.org/license New BSD License
+ * @link     http://pear.phergie.org/package/Phergie_Plugin_Cron
+ */
+class Phergie_Plugin_Cron extends Phergie_Plugin_Abstract
+{
+    /**
+     * Array of all registered callbacks with delays and arguments
+     *
+     * @var array
+     */
+    protected $callbacks;
+
+    /**
+     * Returns a human-readable representation of a callback for debugging
+     * purposes.
+     *
+     * @param callback $callback Callback to analyze
+     *
+     * @return string|boolean String representation of the callback or FALSE
+     *         if the specified value is not a valid callback
+     */
+    protected function getCallbackString($callback)
+    {
+        if (!is_callable($callback)) {
+            return false;
+        }
+
+        if (is_array($callback)) {
+            $class = is_string($callback[0]) ?
+                $callback[0] : get_class($callback[0]);
+            $method = $class . '::' . $callback[1];
+            return $method;
+        }
+
+        return $callback;
+    }
+
+    /**
+     * Registers a callback for execution sometime after a given delay
+     * relative to now.
+     *
+     * @param callback $callback  Callback to be registered
+     * @param int      $delay     Delay in seconds from now when the callback
+     *        will be executed
+     * @param array    $arguments Arguments to pass to the callback when
+     *        it's executed
+     * @param bool     $repeat    TRUE to automatically re-register the
+     *        callback for the same delay after it's executed, FALSE
+     *        otherwise
+     *
+     * @return void
+     */
+    public function registerCallback($callback, $delay,
+        array $arguments = array(), $repeat = false)
+    {
+        $callbackString = $this->getCallbackString($callback);
+        if ($callbackString === false) {
+            echo 'DEBUG(Cron): Invalid callback specified - ',
+                var_export($callback, true), PHP_EOL;
+            return;
+        }
+
+        $registered = time();
+        $scheduled = $registered + $delay;
+
+        $this->callbacks[] = array(
+            'callback'   => $callback,
+            'delay'      => $delay,
+            'arguments'  => $arguments,
+            'registered' => $registered,
+            'scheduled'  => $scheduled,
+            'repeat'     => $repeat,
+        );
+
+        echo 'DEBUG(Cron): Callback ', $callbackString,
+            ' scheduled for ', date('H:i:s', $scheduled), PHP_EOL;
+    }
+
+    /**
+     * Handles callback execution.
+     *
+     * @return void
+     */
+    public function onTick()
+    {
+        $now = time();
+        foreach ($this->callbacks as $key => &$callback) {
+            $callbackString = $this->getCallbackString($callback);
+
+            $scheduled = $callback['scheduled'];
+            if ($time < $scheduled) {
+                continue;
+            }
+
+            if (empty($callback['arguments'])) {
+                call_user_func($callback['callback']);
+            } else {
+                call_user_func_array(
+                    $callback['callback'],
+                    $callback['arguments']
+                );
+            }
+
+            echo 'DEBUG(Cron): Callback ', $callbackString,
+                ' scheduled for ', date('H:i:s', $scheduled), ',',
+                ' executed at ', date('H:i:s', $now), PHP_EOL;
+
+            if ($callback['repeat']) {
+                $callback['scheduled'] = $time + $callback['delay'];
+                echo 'DEBUG(Cron): Callback ', $callbackString,
+                    ' scheduled for ', date('H:i:s', $callback['scheduled']),
+                    PHP_EOL;
+            } else {
+                echo 'DEBUG(Cron): Callback ', $callbackString,
+                    ' removed from callback list', PHP_EOL;
+                unset($this->callbacks[$key]);
+            }
+        }
+    }
+}
diff --git a/plugins/Irc/extlib/phergie/Phergie/Plugin/Ctcp.php b/plugins/Irc/extlib/phergie/Phergie/Plugin/Ctcp.php
new file mode 100755 (executable)
index 0000000..e79fcf1
--- /dev/null
@@ -0,0 +1,91 @@
+<?php
+/**
+ * Phergie
+ *
+ * PHP version 5
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.
+ * It is also available through the world-wide-web at this URL:
+ * http://phergie.org/license
+ *
+ * @category  Phergie
+ * @package   Phergie_Plugin_Ctcp
+ * @author    Phergie Development Team <team@phergie.org>
+ * @copyright 2008-2010 Phergie Development Team (http://phergie.org)
+ * @license   http://phergie.org/license New BSD License
+ * @link      http://pear.phergie.org/package/Phergie_Plugin_Ctcp
+ */
+
+/**
+ * Responds to various CTCP requests sent by the server and users.
+ *
+ * @category Phergie
+ * @package  Phergie_Plugin_Ctcp
+ * @author   Phergie Development Team <team@phergie.org>
+ * @license  http://phergie.org/license New BSD License
+ * @link     http://pear.phergie.org/package/Phergie_Plugin_Ctcp
+ * @link     http://www.irchelp.org/irchelp/rfc/ctcpspec.html
+ */
+class Phergie_Plugin_Ctcp extends Phergie_Plugin_Abstract
+{
+    /**
+     * Responds to a CTCP TIME request from a user with the current local
+     * time.
+     *
+     * @return void
+     */
+    public function onTime()
+    {
+        $source = $this->getEvent()->getSource();
+        $this->doTime($source, strftime('%c %z'));
+    }
+
+    /**
+     * Responds to a CTCP VERSION request from a user with the codebase
+     * version.
+     *
+     * @return void
+     */
+    public function onVersion()
+    {
+        $source = $this->getEvent()->getSource();
+        $msg = 'Phergie ' . Phergie_Bot::VERSION . ' (http://phergie.org)';
+        $this->doVersion($source, $msg);
+    }
+
+    /**
+     * Responds to a CTCP PING request from a user.
+     *
+     * @return void
+     */
+    public function onCtcpPing()
+    {
+        $event = $this->getEvent();
+        $source = $event->getSource();
+        $handshake = $event->getArgument(1);
+        $this->doPing($source, $handshake);
+    }
+
+    /**
+     * Responds to a CTCP FINGER request from a user.
+     *
+     * @return void
+     */
+    public function onFinger()
+    {
+        $connection = $this->getConnection();
+        $name = $connection->getNick();
+        $realname = $connection->getRealname();
+        $username = $connection->getUsername();
+
+        $finger
+            = (empty($realname) ? $realname : $name) .
+            ' (' . (!empty($username) ? $username : $name) . ')';
+
+        $this->doFinger($source, $finger);
+    }
+}
+?>
diff --git a/plugins/Irc/extlib/phergie/Phergie/Plugin/Daddy.php b/plugins/Irc/extlib/phergie/Phergie/Plugin/Daddy.php
new file mode 100644 (file)
index 0000000..ed258e1
--- /dev/null
@@ -0,0 +1,56 @@
+<?php
+/**
+ * Phergie
+ *
+ * PHP version 5
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.
+ * It is also available through the world-wide-web at this URL:
+ * http://phergie.org/license
+ *
+ * @category  Phergie
+ * @package   Phergie_Plugin_Daddy
+ * @author    Phergie Development Team <team@phergie.org>
+ * @copyright 2008-2010 Phergie Development Team (http://phergie.org)
+ * @license   http://phergie.org/license New BSD License
+ * @link      http://pear.phergie.org/package/Phergie_Plugin_Daddy
+ */
+
+/**
+ * Simply responds to messages addressed to the bot that contain the phrase
+ * "Who's your daddy?" and related variations.
+ *
+ * @category Phergie
+ * @package  Phergie_Plugin_Daddy
+ * @author   Phergie Development Team <team@phergie.org>
+ * @license  http://phergie.org/license New BSD License
+ * @link     http://pear.phergie.org/package/Phergie_Plugin_Daddy
+ */
+class Phergie_Plugin_Daddy extends Phergie_Plugin_Abstract
+{
+    /**
+     * Checks messages for the question to which it should respond and sends a
+     * response when appropriate
+     *
+     * @return void
+     */
+    public function onPrivmsg()
+    {
+        $config = $this->getConfig();
+        $prefix = $config['command.prefix'];
+        $event = $this->getEvent();
+        $text = $event->getArgument(1);
+        $target = $event->getNick();
+        $source = $event->getSource();
+        $pattern
+            = '/' . preg_quote($prefix) .
+            '\s*?who\'?s y(?:our|a) ([^?]+)\??/iAD';
+        if (preg_match($pattern, $text, $m)) {
+            $msg = 'You\'re my ' . $m[1] . ', ' . $target . '!';
+            $this->doPrivmsg($source, $msg);
+        }
+    }
+}
diff --git a/plugins/Irc/extlib/phergie/Phergie/Plugin/Encoding.php b/plugins/Irc/extlib/phergie/Phergie/Plugin/Encoding.php
new file mode 100644 (file)
index 0000000..419322b
--- /dev/null
@@ -0,0 +1,182 @@
+<?php
+/**
+ * Phergie
+ *
+ * PHP version 5
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.
+ * It is also available through the world-wide-web at this URL:
+ * http://phergie.org/license
+ *
+ * @category  Phergie
+ * @package   Phergie_Plugin_Encoding
+ * @author    Phergie Development Team <team@phergie.org>
+ * @copyright 2008-2010 Phergie Development Team (http://phergie.org)
+ * @license   http://phergie.org/license New BSD License
+ * @link      http://pear.phergie.org/package/Phergie_Plugin_Encoding
+ */
+
+/**
+ * Handles decoding markup entities and converting text between character
+ * encodings.
+ *
+ * @category Phergie
+ * @package  Phergie_Plugin_Encoding
+ * @author   Phergie Development Team <team@phergie.org>
+ * @license  http://phergie.org/license New BSD License
+ * @link     http://pear.phergie.org/package/Phergie_Plugin_Encoding
+ */
+class Phergie_Plugin_Encoding extends Phergie_Plugin_Abstract
+{
+    /**
+     * Lookup table for entity conversions not supported by
+     * html_entity_decode()
+     *
+     * @var array
+     * @link http://us.php.net/manual/en/function.get-html-translation-table.php#73409
+     * @link http://us.php.net/manual/en/function.get-html-translation-table.php#73410
+     */
+    protected static $entities = array(
+        '&alpha;' => 913,
+        '&apos;' => 39,
+        '&beta;' => 914,
+        '&bull;' => 149,
+        '&chi;' => 935,
+        '&circ;' => 94,
+        '&delta;' => 916,
+        '&epsilon;' => 917,
+        '&eta;' => 919,
+        '&fnof;' => 402,
+        '&gamma;' => 915,
+        '&iota;' => 921,
+        '&kappa;' => 922,
+        '&lambda;' => 923,
+        '&ldquo;' => 147,
+        '&lsaquo;' => 139,
+        '&lsquo;' => 145,
+        '&mdash;' => 151,
+        '&minus;' => 45,
+        '&mu;' => 924,
+        '&ndash;' => 150,
+        '&nu;' => 925,
+        '&oelig;' => 140,
+        '&omega;' => 937,
+        '&omicron;' => 927,
+        '&phi;' => 934,
+        '&pi;' => 928,
+        '&piv;' => 982,
+        '&psi;' => 936,
+        '&rdquo;' => 148,
+        '&rho;' => 929,
+        '&rsaquo;' => 155,
+        '&rsquo;' => 146,
+        '&scaron;' => 138,
+        '&sigma;' => 931,
+        '&sigmaf;' => 962,
+        '&tau;' => 932,
+        '&theta;' => 920,
+        '&thetasym;' => 977,
+        '&tilde;' => 126,
+        '&trade;' => 153,
+        '&upsih;' => 978,
+        '&upsilon;' => 933,
+        '&xi;' => 926,
+        '&yuml;' => 159,
+        '&zeta;' => 918,
+    );
+
+    /**
+     * Decodes markup entities in a given string.
+     *
+     * @param string $string  String containing markup entities
+     * @param string $charset Optional character set name to use in decoding
+     *        entities, defaults to UTF-8
+     *
+     * @return string String with markup entities decoded
+     */
+    public function decodeEntities($string, $charset = 'UTF-8')
+    {
+        $string = str_ireplace(
+            array_keys(self::$entities),
+            array_map('chr', self::$entities),
+            $string
+        );
+        $string = html_entity_decode($string, ENT_QUOTES, $charset);
+        $string = preg_replace(
+            array('/&#0*([0-9]+);/me', '/&#x0*([a-f0-9]+);/mei'),
+            array('$this->codeToUtf(\\1)', '$this->codeToUtf(hexdec(\\1))'),
+            $string
+        );
+        return $string;
+    }
+
+    /**
+     * Converts a given unicode to its UTF-8 equivalent.
+     *
+     * @param int $code Code to convert
+     * @return string Character corresponding to code
+     */
+    public function codeToUtf8($code)
+    {
+        $code = (int) $code;
+        switch ($code) {
+            // 1 byte, 7 bits
+            case 0:
+                return chr(0);
+            case ($code & 0x7F):
+                return chr($code);
+
+            // 2 bytes, 11 bits
+            case ($code & 0x7FF):
+                return chr(0xC0 | (($code >> 6) & 0x1F)) .
+                       chr(0x80 | ($code & 0x3F));
+
+            // 3 bytes, 16 bits
+            case ($code & 0xFFFF):
+                return chr(0xE0 | (($code >> 12) & 0x0F)) .
+                       chr(0x80 | (($code >> 6) & 0x3F)) .
+                       chr(0x80 | ($code & 0x3F));
+
+            // 4 bytes, 21 bits
+            case ($code & 0x1FFFFF):
+                return chr(0xF0 | ($code >> 18)) .
+                       chr(0x80 | (($code >> 12) & 0x3F)) .
+                       chr(0x80 | (($code >> 6) & 0x3F)) .
+                       chr(0x80 | ($code & 0x3F));
+        }
+    }
+
+    /**
+     * Transliterates characters in a given string where possible.
+     *
+     * @param string $string      String containing characters to
+     *        transliterate
+     * @param string $charsetFrom Optional character set of the string,
+     *        defaults to UTF-8
+     * @param string $charsetTo   Optional character set to which the string
+     *        should be converted, defaults to ISO-8859-1
+     *
+     * @return string String with characters transliterated or the original
+     *         string if transliteration was not possible
+     */
+    public function transliterate($string, $charsetFrom = 'UTF-8', $charsetTo = 'ISO-8859-1')
+    {
+        // @link http://pecl.php.net/package/translit
+        if (function_exists('transliterate')) {
+            $string = transliterate($string, array('han_transliterate', 'diacritical_remove'), $charsetFrom, $charsetTo);
+        } elseif (function_exists('iconv')) {
+            $string = iconv($charsetFrom, $charsetTo . '//TRANSLIT', $string);
+        } else {
+            // @link http://stackoverflow.com/questions/1284535/php-transliteration/1285491#1285491
+            $string = preg_replace(
+                '~&([a-z]{1,2})(acute|cedil|circ|grave|lig|orn|ring|slash|th|tilde|uml);~i',
+                '$1',
+                htmlentities($string, ENT_COMPAT, $charsetFrom)
+            );
+        }
+        return $string;
+    }
+}
diff --git a/plugins/Irc/extlib/phergie/Phergie/Plugin/Exception.php b/plugins/Irc/extlib/phergie/Phergie/Plugin/Exception.php
new file mode 100755 (executable)
index 0000000..ca4d53f
--- /dev/null
@@ -0,0 +1,120 @@
+<?php
+/**
+ * Phergie
+ *
+ * PHP version 5
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.
+ * It is also available through the world-wide-web at this URL:
+ * http://phergie.org/license
+ *
+ * @category  Phergie
+ * @package   Phergie
+ * @author    Phergie Development Team <team@phergie.org>
+ * @copyright 2008-2010 Phergie Development Team (http://phergie.org)
+ * @license   http://phergie.org/license New BSD License
+ * @link      http://pear.phergie.org/package/Phergie
+ */
+
+/**
+ * Exception related to plugin handling.
+ *
+ * @category Phergie
+ * @package  Phergie
+ * @author   Phergie Development Team <team@phergie.org>
+ * @license  http://phergie.org/license New BSD License
+ * @link     http://pear.phergie.org/package/Phergie
+ */
+class Phergie_Plugin_Exception extends Phergie_Exception
+{
+    /**
+     * Error indicating that a path containing plugins was specified, but
+     * did not reference a readable directory
+     */
+    const ERR_DIRECTORY_NOT_READABLE = 1;
+
+    /**
+     * Error indicating that an attempt was made to locate the class for a
+     * specified plugin, but the class could not be found
+     */
+    const ERR_CLASS_NOT_FOUND = 2;
+
+    /**
+     * Error indicating that an attempt was made to locate the class for a
+     * specified plugin, but that the found class did not extend the base
+     * plugin class
+     */
+    const ERR_INCORRECT_BASE_CLASS = 3;
+
+    /**
+     * Error indicating that an attempt was made to locate the class for a
+     * specified plugin, but that the found class cannot be instantiated
+     */
+    const ERR_CLASS_NOT_INSTANTIABLE = 4;
+
+    /**
+     * Error indicating that an attempt was made to access a plugin that had
+     * not been loaded and autoloading was not enabled to load it
+     */
+    const ERR_PLUGIN_NOT_LOADED = 5;
+
+    /**
+     * Error indicating that an attempt was made to access the configuration
+     * handler before one had been set
+     */
+    const ERR_NO_CONFIG_HANDLER = 6;
+
+    /**
+     * Error indicating that an attempt was made to access the plugin
+     * handler before one had been set
+     */
+    const ERR_NO_PLUGIN_HANDLER = 7;
+
+    /**
+     * Error indicating that an attempt was made to access the event
+     * handler before one had been set
+     */
+    const ERR_NO_EVENT_HANDLER = 8;
+
+    /**
+     * Error indicating that an attempt was made to access the connection
+     * before one had been set
+     */
+    const ERR_NO_CONNECTION = 9;
+
+    /**
+     * Error indicating that an attempt was made to access the current
+     * incoming event before one had been set
+     */
+    const ERR_NO_EVENT = 10;
+
+    /**
+     * Error indicating that a dependency of the plugin was unavailable at
+     * the time that an attempt was made to load it
+     */
+    const ERR_REQUIREMENT_UNSATISFIED = 11;
+
+    /**
+     * Error indicating that a call was made to a nonexistent plugin method
+     * and that its __call() implementation did not process that call as an
+     * attempt to trigger an event - this is intended to aid in debugging of
+     * such situations
+     */
+    const ERR_INVALID_CALL = 12;
+
+    /**
+     * Error indicating that a fatal runtime issue was encountered within a
+     * plugin
+     */
+    const ERR_FATAL_ERROR = 13;
+
+    /**
+     * Error indicating that a class specified to be used for iterating
+     * plugins cannot be found by the autoloader or does not extend
+     * FilterIterator
+     */
+    const ERR_INVALID_ITERATOR_CLASS = 14;
+}
diff --git a/plugins/Irc/extlib/phergie/Phergie/Plugin/Google.php b/plugins/Irc/extlib/phergie/Phergie/Plugin/Google.php
new file mode 100644 (file)
index 0000000..d2a9d4d
--- /dev/null
@@ -0,0 +1,459 @@
+<?php
+/**
+ * Phergie
+ *
+ * PHP version 5
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.
+ * It is also available through the world-wide-web at this URL:
+ * http://phergie.org/license
+ *
+ * @category  Phergie
+ * @package   Phergie_Plugin_Google
+ * @author    Phergie Development Team <team@phergie.org>
+ * @copyright 2008-2010 Phergie Development Team (http://phergie.org)
+ * @license   http://phergie.org/license New BSD License
+ * @link      http://pear.phergie.org/package/Phergie_Plugin_Google
+ */
+
+/**
+ * Provides commands used to access several services offered by Google
+ * including search, translation, weather, maps, and currency and general
+ * value unit conversion.
+ *
+ * @category Phergie
+ * @package  Phergie_Plugin_Google
+ * @author   Phergie Development Team <team@phergie.org>
+ * @license  http://phergie.org/license New BSD License
+ * @link     http://pear.phergie.org/package/Phergie_Plugin_Google
+ * @uses     Phergie_Plugin_Command pear.phergie.org
+ * @uses     Phergie_Plugin_Http pear.phergie.org
+ * @uses     Phergie_Plugin_Temperature pear.phergie.org
+ */
+class Phergie_Plugin_Google extends Phergie_Plugin_Abstract
+{
+    /**
+     * Checks for dependencies.
+     *
+     * @return void
+     */
+    public function onLoad()
+    {
+        $plugins = $this->getPluginHandler();
+        $plugins->getPlugin('Command');
+        $plugins->getPlugin('Http');
+        $plugins->getPlugin('Weather');
+    }
+
+    /**
+     * Returns the first result of a Google search.
+     *
+     * @param string $query Search term
+     *
+     * @return void
+     * @todo Implement use of URL shortening here
+     */
+    public function onCommandG($query)
+    {
+        $url = 'http://ajax.googleapis.com/ajax/services/search/web';
+        $params = array(
+            'v' => '1.0',
+            'q' => $query
+        );
+        $response = $this->plugins->http->get($url, $params);
+        $json = $response->getContent()->responseData;
+        $event = $this->getEvent();
+        $source = $event->getSource();
+        $nick = $event->getNick();
+        if ($json->cursor->estimatedResultCount > 0) {
+            $msg
+                = $nick
+                . ': [ '
+                . $json->results[0]->titleNoFormatting
+                . ' ] - '
+                . $json->results[0]->url
+                . ' - More results: '
+                . $json->cursor->moreResultsUrl;
+            $this->doPrivmsg($source, $msg);
+        } else {
+            $msg = $nick . ': No results for this query.';
+            $this->doPrivmsg($source, $msg);
+        }
+    }
+
+    /**
+     * Performs a Google Count search for the given term.
+     *
+     * @param string $query Search term
+     *
+     * @return void
+     */
+    public function onCommandGc($query)
+    {
+        $url = 'http://ajax.googleapis.com/ajax/services/search/web';
+        $params = array(
+            'v' => '1.0',
+            'q' => $query
+        );
+        $response = $this->plugins->http->get($url, $params);
+        $json = $response->getContent()->responseData->cursor;
+        $count = $json->estimatedResultCount;
+        $event = $this->getEvent();
+        $source = $event->getSource();
+        $nick = $event->getNick();
+        if ($count) {
+            $msg
+                = $nick . ': ' .
+                number_format($count, 0) .
+                ' estimated results for ' . $query;
+            $this->doPrivmsg($source, $msg);
+        } else {
+            $this->doPrivmsg($source, $nick . ': No results for this query.');
+        }
+    }
+
+    /**
+     * Performs a Google Translate search for the given term.
+     *
+     * @param string $from  Language of the search term
+     * @param string $to    Language to which the search term should be
+     *        translated
+     * @param string $query Term to translate
+     *
+     * @return void
+     */
+    public function onCommandGt($from, $to, $query)
+    {
+        $url = 'http://ajax.googleapis.com/ajax/services/language/translate';
+        $params = array(
+            'v' => '1.0',
+            'q' => $query,
+            'langpair' => $from . '|' . $to
+        );
+        $response = $this->plugins->http->get($url, $params);
+        $json = $response->getContent();
+        $event = $this->getEvent();
+        $source = $event->getSource();
+        $nick = $event->getNick();
+        if (empty($json->responseData->translatedText)) {
+            $this->doPrivmsg($source, $nick . ': ' . $json->responseDetails);
+        } else {
+            $this->doPrivmsg(
+                $source,
+                $nick . ': ' . $json->responseData->translatedText
+            );
+        }
+    }
+
+    /**
+     * Performs a Google Weather search for the given term.
+     *
+     * @param string $location Location to search for
+     * @param int    $offset   Optional day offset from the current date
+     *        between 0 and 3 to get the forecast
+     *
+     * @return void
+     */
+    public function onCommandGw($location, $offset = null)
+    {
+        $url = 'http://www.google.com/ig/api';
+        $params = array(
+            'weather' => $location,
+            'hl' => $this->getConfig('google.lang', 'en'),
+            'oe' => 'UTF-8'
+        );
+        $response = $this->plugins->http->get($url, $params);
+        $xml = $response->getContent()->weather;
+
+        $event = $this->getEvent();
+        $source = $event->getSource();
+        $msg = '';
+        if ($event->isInChannel()) {
+            $msg .= $event->getNick() . ': ';
+        }
+
+        if (isset($xml->problem_cause)) {
+            $msg .= $xml->problem_cause->attributes()->data[0];
+            $this->doPrivmsg($source, $msg);
+            return;
+        }
+
+        $temperature = $this->plugins->getPlugin('Temperature');
+
+        $forecast = $xml->forecast_information;
+        $city = $forecast->city->attributes()->data[0];
+        $zip = $forecast->postal_code->attributes()->data[0];
+
+        if ($offset !== null) {
+            $offset = (int) $offset;
+            if ($offset < 0) {
+                $this->doNotice($source, 'Past weather data is not available');
+                return;
+            } elseif ($offset > 3) {
+                $this->doNotice($source, 'Future weather data is limited to 3 days from today');
+                return;
+            }
+
+            $linha = $xml->forecast_conditions[$offset];
+            $low = $linha->low->attributes()->data[0];
+            $high = $linha->high->attributes()->data[0];
+            $units = $forecast->unit_system->attributes()->data[0];
+            $condition = $linha->condition->attributes()->data[0];
+            $day = $linha->day_of_week->attributes()->data[0];
+
+            $date = ($offset == 0) ? time() : strtotime('next ' . $day);
+            $day = ucfirst($day) . ' ' . date('n/j/y', $date);
+
+            if ($units == 'US') {
+                $lowF = $low;
+                $lowC = $temperature->convertFahrenheitToCelsius($low);
+                $highF = $high;
+                $highC = $temperature->convertFahrenheitToCelsius($high);
+            } else {
+                $lowC = $low;
+                $lowF = $temperature->convertCelsiusToFahrenheit($lowC);
+                $highC = $high;
+                $highF = $temperature->convertCelsiusToFahrenheit($high);
+            }
+
+            $msg .= 'Forecast for ' . $city . ' (' . $zip . ')'
+                . ' on ' . $day . ' ::'
+                . ' Low: ' . $lowF . 'F/' . $lowC . 'C,'
+                . ' High: ' . $highF . 'F/' . $highC . 'C,'
+                . ' Conditions: ' . $condition;
+        } else {
+            $conditions = $xml->current_conditions;
+            $condition = $conditions->condition->attributes()->data[0];
+            $tempF = $conditions->temp_f->attributes()->data[0];
+            $tempC = $conditions->temp_c->attributes()->data[0];
+            $humidity = $conditions->humidity->attributes()->data[0];
+            $wind = $conditions->wind_condition->attributes()->data[0];
+            $time = $forecast->current_date_time->attributes()->data[0];
+            $time = date('n/j/y g:i A', strtotime($time)) . ' +0000';
+
+            $hiF = $temperature->getHeatIndex($tempF, $humidity);
+            $hiC = $temperature->convertFahrenheitToCelsius($hiF);
+
+            $msg .= 'Weather for ' . $city . ' (' . $zip . ') -'
+                . ' Temperature: ' . $tempF . 'F/' . $tempC . 'C,'
+                . ' ' . $humidity . ','
+                . ' Heat Index: ' . $hiF . 'F/' . $hiC . 'C,'
+                . ' Conditions: ' . $condition . ','
+                . ' Updated: ' . $time;
+        }
+
+        $this->doPrivmsg($source, $msg);
+    }
+
+    /**
+     * Performs a Google Maps search for the given term.
+     *
+     * @param string $location Location to search for
+     *
+     * @return void
+     */
+    public function onCommandGmap($location)
+    {
+        $event = $this->getEvent();
+        $source = $event->getSource();
+        $nick = $event->getNick();
+
+        $location = utf8_encode($location);
+        $url = 'http://maps.google.com/maps/geo';
+        $params = array(
+            'q' => $location,
+            'output' => 'json',
+            'gl' => $this->getConfig('google.lang', 'en'),
+            'sensor' => 'false',
+            'oe' => 'utf8',
+            'mrt' => 'all',
+            'key' => $this->getConfig('google.key')
+        );
+        $response = $this->plugins->http->get($url, $params);
+        $json =  $response->getContent();
+        if (!empty($json)) {
+            $qtd = count($json->Placemark);
+            if ($qtd > 1) {
+                if ($qtd <= 3) {
+                    foreach ($json->Placemark as $places) {
+                        $xy = $places->Point->coordinates;
+                        $address = utf8_decode($places->address);
+                        $url = 'http://maps.google.com/maps?sll=' . $xy[1] . ','
+                            . $xy[0] . '&z=15';
+                        $msg = $nick . ' -> ' . $address . ' - ' . $url;
+                        $this->doPrivmsg($source, $msg);
+                    }
+                } else {
+                    $msg
+                        = $nick .
+                        ', there are a lot of places with that query.' .
+                        ' Try to be more specific!';
+                    $this->doPrivmsg($source, $msg);
+                }
+            } elseif ($qtd == 1) {
+                $xy = $json->Placemark[0]->Point->coordinates;
+                $address = utf8_decode($json->Placemark[0]->address);
+                $url = 'http://maps.google.com/maps?sll=' . $xy[1] . ',' . $xy[0]
+                    . '&z=15';
+                $msg = $nick . ' -> ' . $address . ' - ' . $url;
+                $this->doPrivmsg($source, $msg);
+            } else {
+                $this->doPrivmsg($source, $nick . ', I found nothing.');
+            }
+        } else {
+            $this->doPrivmsg($source, $nick . ', we have a problem.');
+        }
+    }
+
+    /**
+     * Perform a Google Convert query to convert a value from one metric to
+     * another.
+     *
+     * @param string $value Value to convert
+     * @param string $from  Source metric
+     * @param string $to    Destination metric
+     *
+     * @return void
+     */
+    public function onCommandGconvert($value, $from, $to)
+    {
+        $url = 'http://www.google.com/finance/converter';
+        $params = array(
+            'a' => $value,
+            'from' => $from,
+            'to' => $to
+        );
+        $response = $this->plugins->http->get($url, $params);
+        $contents = $response->getContent();
+        $event = $this->getEvent();
+        $source = $event->getSource();
+        $nick = $event->getNick();
+        if ($contents) {
+            libxml_use_internal_errors(true);
+            $doc = new DOMDocument;
+            $doc->loadHTML($contents);
+            libxml_clear_errors();
+            $xpath = new DOMXPath($doc);
+            $result = $xpath->query('//div[@id="currency_converter_result"]');
+            $div = $result->item(0);
+            $text = rtrim($div->textContent);
+            $this->doPrivmsg($source, $text);
+        }
+    }
+
+    /**
+     * Performs a Google search to convert a value from one unit to another.
+     *
+     * @param string $query Query of the form "[quantity] [unit] to [unit2]"
+     *
+     * @return void
+     *
+     * @pluginCmd [quantity] [unit] to [unit2] Convert a value from one
+     *            metric to another
+     */
+    public function onCommandConvert($query)
+    {
+        $url = 'http://www.google.com/search?q=' . urlencode($query);
+        $response = $this->plugins->http->get($url);
+        $contents = $response->getContent();
+        $event = $this->getEvent();
+        $source = $event->getSource();
+        $nick = $event->getNick();
+
+        if ($response->isError()) {
+            $code = $response->getCode();
+            $message = $response->getMessage();
+            $this->doNotice($nick, 'ERROR: ' . $code . ' ' . $message);
+            return;
+        }
+
+        $start = strpos($contents, '<h3 class=r>');
+        if ($start !== false) {
+            $end = strpos($contents, '</b>', $start);
+            $text = strip_tags(substr($contents, $start, $end - $start));
+            $text = str_replace(
+                array(chr(195), chr(151), chr(160)),
+                array('x', '', ' '),
+                $text
+            );
+        }
+
+        if (isset($text)) {
+            $this->doPrivmsg($source, $nick . ': ' . $text);
+        } else {
+            $this->doNotice($nick, 'Sorry I couldn\'t find an answer.');
+        }
+    }
+
+
+    /**
+     * Returns the first definition of a Google Dictionary search.
+     *
+     * @param string $query Word to get the definition
+     *
+     * @return void
+     * @todo Implement use of URL shortening here
+     */
+    public function onCommandDefine($query)
+    {
+        $lang = $this->getConfig('google.lang', 'en');
+        $url = 'http://www.google.com/dictionary/json';
+        $params = array(
+            'callback' => 'result',
+            'q' => $query,
+            'sl' => $lang,
+            'tl' => $lang,
+            'restrict' => 'pr,de'
+        );
+        $response = $this->plugins->http->get($url, $params);
+        $json = $response->getContent();
+
+        // Remove some garbage from the JSON and decode it
+        $json = str_replace(array('result(', ',200,null)'), '', $json);
+        $json = str_replace('"', '¿?¿', $json);
+        $json = strip_tags(stripcslashes($json));
+        $json = str_replace('"', "'", $json);
+        $json = str_replace('¿?¿', '"', $json);
+        $json = json_decode($json);
+
+        $event = $this->getEvent();
+        $source = $event->getSource();
+        $nick = $event->getNick();
+        if (!empty($json->webDefinitions)) {
+            $results = 0;
+            foreach ($json->primaries[0]->entries as $entry) {
+                if ($entry->type == 'meaning') {
+                    $results++;
+                    if (empty($text)) {
+                        foreach ($entry->terms as $term) {
+                            if ($term->type == 'text') {
+                                $text = trim($term->text);
+                            }
+                        }
+                    }
+                }
+            }
+            $more = $results > 1 ? ($results - 1) . ' ' : '';
+            $lang_code = substr($lang, 0, 2);
+            $msg = $nick . ': ' . $text
+                 . ' - You can find ' . $more . 'more results at '
+                 . 'http://www.google.com/dictionary'
+                 . '?aq=f'
+                 . '&langpair=' . $lang_code . '%7C' . $lang_code
+                 . '&q=' . $query
+                 . '&hl=' . $lang_code;
+            $this->doPrivmsg($source, $msg);
+        } else {
+            if ($lang != 'en'){
+               $lang = 'en';
+               $this->onCommandDefine($query);
+            } else {
+               $msg = $nick . ': No results for this query.';
+               $this->doPrivmsg($source, $msg);
+            }
+        }
+    }
+}
diff --git a/plugins/Irc/extlib/phergie/Phergie/Plugin/Handler.php b/plugins/Irc/extlib/phergie/Phergie/Plugin/Handler.php
new file mode 100755 (executable)
index 0000000..335c10f
--- /dev/null
@@ -0,0 +1,488 @@
+<?php
+/**
+ * Phergie
+ *
+ * PHP version 5
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.
+ * It is also available through the world-wide-web at this URL:
+ * http://phergie.org/license
+ *
+ * @category  Phergie
+ * @package   Phergie
+ * @author    Phergie Development Team <team@phergie.org>
+ * @copyright 2008-2010 Phergie Development Team (http://phergie.org)
+ * @license   http://phergie.org/license New BSD License
+ * @link      http://pear.phergie.org/package/Phergie
+ */
+
+/**
+ * Handles on-demand loading of, iteration over, and access to plugins.
+ *
+ * @category Phergie
+ * @package  Phergie
+ * @author   Phergie Development Team <team@phergie.org>
+ * @license  http://phergie.org/license New BSD License
+ * @link     http://pear.phergie.org/package/Phergie
+ */
+class Phergie_Plugin_Handler implements IteratorAggregate, Countable
+{
+    /**
+     * Current list of plugin instances
+     *
+     * @var array
+     */
+    protected $plugins;
+
+    /**
+     * Paths in which to search for plugin class files
+     *
+     * @var array
+     */
+    protected $paths;
+
+    /**
+     * Flag indicating whether plugin classes should be instantiated on
+     * demand if they are requested but no instance currently exists
+     *
+     * @var bool
+     */
+    protected $autoload;
+
+    /**
+     * Phergie_Config instance that should be passed in to any plugin
+     * instantiated within the handler
+     *
+     * @var Phergie_Config
+     */
+    protected $config;
+
+    /**
+     * Phergie_Event_Handler instance that should be passed in to any plugin
+     * instantiated within the handler
+     *
+     * @var Phergie_Event_Handler
+     */
+    protected $events;
+
+    /**
+     * Name of the class to use for iterating over all currently loaded
+     * plugins
+     *
+     * @var string
+     */
+    protected $iteratorClass = 'Phergie_Plugin_Iterator';
+
+    /**
+     * Constructor to initialize class properties and add the path for core
+     * plugins.
+     *
+     * @param Phergie_Config        $config configuration to pass to any
+     *        instantiated plugin
+     * @param Phergie_Event_Handler $events event handler to pass to any
+     *        instantiated plugin
+     *
+     * @return void
+     */
+    public function __construct(
+        Phergie_Config $config,
+        Phergie_Event_Handler $events
+    ) {
+        $this->config = $config;
+        $this->events = $events;
+
+        $this->plugins = array();
+        $this->paths = array();
+        $this->autoload = false;
+
+        $this->addPath(dirname(__FILE__), 'Phergie_Plugin_');
+    }
+
+
+    /**
+     * Adds a path to search for plugin class files. Paths are searched in
+     * the reverse order in which they are added.
+     *
+     * @param string $path   Filesystem directory path
+     * @param string $prefix Optional class name prefix corresponding to the
+     *        path
+     *
+     * @return Phergie_Plugin_Handler Provides a fluent interface
+     * @throws Phergie_Plugin_Exception
+     */
+    public function addPath($path, $prefix = '')
+    {
+        if (!is_readable($path)) {
+            throw new Phergie_Plugin_Exception(
+                'Path "' . $path . '" does not reference a readable directory',
+                Phergie_Plugin_Exception::ERR_DIRECTORY_NOT_READABLE
+            );
+        }
+
+        $this->paths[] = array(
+            'path' => rtrim($path, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR,
+            'prefix' => $prefix
+        );
+
+        return $this;
+    }
+
+    /**
+     * Returns metadata corresponding to a specified plugin.
+     *
+     * @param string $plugin Short name of the plugin class
+     * @throws Phergie_Plugin_Exception Class file can't be found
+     *
+     * @return array|boolean Associative array containing the path to the
+     *         class file and its containing directory as well as the full
+     *         class name
+     */
+    public function getPluginInfo($plugin)
+    {
+       foreach (array_reverse($this->paths) as $path) {
+            $file = $path['path'] . $plugin . '.php';
+            if (file_exists($file)) {
+                $path = array(
+                    'dir' => $path['path'],
+                    'file' => $file,
+                    'class' => $path['prefix'] . $plugin,
+                );
+                return $path;
+            }
+        }
+
+        // If the class can't be found, display an error
+        throw new Phergie_Plugin_Exception(
+            'Class file for plugin "' . $plugin . '" cannot be found',
+            Phergie_Plugin_Exception::ERR_CLASS_NOT_FOUND
+        );
+    }
+
+    /**
+     * Adds a plugin instance to the handler.
+     *
+     * @param string|Phergie_Plugin_Abstract $plugin Short name of the
+     *        plugin class or a plugin object
+     * @param array                          $args   Optional array of
+     *        arguments to pass to the plugin constructor if a short name is
+     *        passed for $plugin
+     *
+     * @return Phergie_Plugin_Abstract New plugin instance
+     */
+    public function addPlugin($plugin, array $args = null)
+    {
+        // If a short plugin name is specified...
+        if (is_string($plugin)) {
+            $index = strtolower($plugin);
+            if (isset($this->plugins[$index])) {
+                return $this->plugins[$index];
+            }
+
+            // Attempt to locate and load the class
+            $info = $this->getPluginInfo($plugin);
+            $file = $info['file'];
+            $class = $info['class'];
+            include_once $file;
+            if (!class_exists($class, false)) {
+                throw new Phergie_Plugin_Exception(
+                    'File "' . $file . '" does not contain class "' . $class . '"',
+                    Phergie_Plugin_Exception::ERR_CLASS_NOT_FOUND
+                );
+            }
+
+            // Check to ensure the class is a plugin class
+            if (!is_subclass_of($class, 'Phergie_Plugin_Abstract')) {
+                $msg
+                    = 'Class for plugin "' . $plugin .
+                    '" does not extend Phergie_Plugin_Abstract';
+                throw new Phergie_Plugin_Exception(
+                    $msg,
+                    Phergie_Plugin_Exception::ERR_INCORRECT_BASE_CLASS
+                );
+            }
+
+            // Check to ensure the class can be instantiated
+            $reflection = new ReflectionClass($class);
+            if (!$reflection->isInstantiable()) {
+                throw new Phergie_Plugin_Exception(
+                    'Class for plugin "' . $plugin . '" cannot be instantiated',
+                    Phergie_Plugin_Exception::ERR_CLASS_NOT_INSTANTIABLE
+                );
+            }
+
+            // If the class is found, instantiate it
+            if (!empty($args)) {
+                $instance = $reflection->newInstanceArgs($args);
+            } else {
+                $instance = new $class;
+            }
+
+            // Store the instance
+            $this->plugins[$index] = $instance;
+            $plugin = $instance;
+
+        } elseif ($plugin instanceof Phergie_Plugin_Abstract) {
+            // If a plugin instance is specified...
+
+            // Add the plugin instance to the list of plugins
+            $this->plugins[strtolower($plugin->getName())] = $plugin;
+        }
+
+        // Configure and initialize the instance
+        $plugin->setPluginHandler($this);
+        $plugin->setConfig($this->config);
+        $plugin->setEventHandler($this->events);
+        $plugin->onLoad();
+
+        return $plugin;
+    }
+
+    /**
+     * Adds multiple plugin instances to the handler.
+     *
+     * @param array $plugins List of elements where each is of the form
+     *        'ShortPluginName' or array('ShortPluginName', array($arg1,
+     *        ..., $argN))
+     *
+     * @return Phergie_Plugin_Handler Provides a fluent interface
+     */
+    public function addPlugins(array $plugins)
+    {
+        foreach ($plugins as $plugin) {
+            if (is_array($plugin)) {
+                $this->addPlugin($plugin[0], $plugin[1]);
+            } else {
+                $this->addPlugin($plugin);
+            }
+        }
+
+        return $this;
+    }
+
+    /**
+     * Removes a plugin instance from the handler.
+     *
+     * @param string|Phergie_Plugin_Abstract $plugin Short name of the
+     *        plugin class or a plugin object
+     *
+     * @return Phergie_Plugin_Handler Provides a fluent interface
+     */
+    public function removePlugin($plugin)
+    {
+        if ($plugin instanceof Phergie_Plugin_Abstract) {
+            $plugin = $plugin->getName();
+        }
+        $plugin = strtolower($plugin);
+
+        unset($this->plugins[$plugin]);
+
+        return $this;
+    }
+
+    /**
+     * Returns the corresponding instance for a specified plugin, loading it
+     * if it is not already loaded and autoloading is enabled.
+     *
+     * @param string $name Short name of the plugin class
+     *
+     * @return Phergie_Plugin_Abstract Plugin instance
+     */
+    public function getPlugin($name)
+    {
+        // If the plugin is loaded, return the instance
+        $lower = strtolower($name);
+        if (isset($this->plugins[$lower])) {
+            return $this->plugins[$lower];
+        }
+
+        // If autoloading is disabled, display an error
+        if (!$this->autoload) {
+            $msg
+                = 'Plugin "' . $name . '" has been requested, ' .
+                'is not loaded, and autoload is disabled';
+            throw new Phergie_Plugin_Exception(
+                $msg,
+                Phergie_Plugin_Exception::ERR_PLUGIN_NOT_LOADED
+            );
+        }
+
+        // If autoloading is enabled, attempt to load the plugin
+        $plugin = $this->addPlugin($name);
+
+        // Return the added plugin
+        return $plugin;
+    }
+
+    /**
+     * Returns the corresponding instances for multiple specified plugins,
+     * loading them if they are not already loaded and autoloading is
+     * enabled.
+     *
+     * @param array $names Optional list of short names of the plugin
+     *        classes to which the returned plugin list will be limited,
+     *        defaults to all presently loaded plugins
+     *
+     * @return array Associative array mapping lowercased plugin class short
+     *         names to corresponding plugin instances
+     */
+    public function getPlugins(array $names = array())
+    {
+        if (empty($names)) {
+            return $this->plugins;
+        }
+
+        $plugins = array();
+        foreach ($names as $name) {
+            $plugins[strtolower($name)] = $this->getPlugin($name);
+        }
+        return $plugins;
+    }
+
+    /**
+     * Returns whether or not at least one instance of a specified plugin
+     * class is loaded.
+     *
+     * @param string $name Short name of the plugin class
+     *
+     * @return bool TRUE if an instance exists, FALSE otherwise
+     */
+    public function hasPlugin($name)
+    {
+        return isset($this->plugins[strtolower($name)]);
+    }
+
+    /**
+     * Sets a flag used to determine whether plugins should be loaded
+     * automatically if they have not been explicitly loaded.
+     *
+     * @param bool $flag TRUE to have plugins autoload (default), FALSE
+     *        otherwise
+     *
+     * @return Phergie_Plugin_Handler Provides a fluent interface.
+     */
+    public function setAutoload($flag = true)
+    {
+        $this->autoload = $flag;
+
+        return $this;
+    }
+
+    /**
+     * Returns the value of a flag used to determine whether plugins should
+     * be loaded automatically if they have not been explicitly loaded.
+     *
+     * @return bool TRUE if autoloading is enabled, FALSE otherwise
+     */
+    public function getAutoload()
+    {
+        return $this->autoload;
+    }
+
+    /**
+     * Allows plugin instances to be accessed as properties of the handler.
+     *
+     * @param string $name Short name of the plugin
+     *
+     * @return Phergie_Plugin_Abstract Requested plugin instance
+     */
+    public function __get($name)
+    {
+        return $this->getPlugin($name);
+    }
+
+    /**
+     * Allows plugin instances to be detected as properties of the handler.
+     *
+     * @param string $name Short name of the plugin
+     *
+     * @return bool TRUE if the plugin is loaded, FALSE otherwise
+     */
+    public function __isset($name)
+    {
+        return $this->hasPlugin($name);
+    }
+
+    /**
+     * Allows plugin instances to be removed as properties of handler.
+     *
+     * @param string $name Short name of the plugin
+     *
+     * @return void
+     */
+    public function __unset($name)
+    {
+        $this->removePlugin($name);
+    }
+
+    /**
+     * Returns an iterator for all currently loaded plugin instances.
+     *
+     * @return ArrayIterator
+     */
+    public function getIterator()
+    {
+        return new $this->iteratorClass(
+            new ArrayIterator($this->plugins)
+        );
+    }
+
+    /**
+     * Sets the iterator class used for all currently loaded plugin
+     * instances.
+     *
+     * @param string $class Name of a class that extends FilterIterator
+     *
+     * @return Phergie_Plugin_Handler Provides a fluent API
+     * @throws Phergie_Plugin_Exception Class cannot be found or is not an
+     *         FilterIterator-based class
+     */
+    public function setIteratorClass($class)
+    {
+        $valid = true;
+
+        try {
+            $r = new ReflectionClass($class);
+            $valid = $r->isSubclassOf('FilterIterator');
+        } catch (ReflectionException $e) {
+            $valid = false;
+        }
+
+        if (!$valid) {
+            throw new Phergie_Plugin_Exception(
+                $e->getMessage(),
+                Phergie_Plugin_Exception::ERR_INVALID_ITERATOR_CLASS
+            );
+        }
+
+        $this->iteratorClass = $class;
+    }
+
+    /**
+     * Proxies method calls to all plugins containing the called method.
+     *
+     * @param string $name Name of the method called
+     * @param array  $args Arguments passed in the method call
+     *
+     * @return void
+     */
+    public function __call($name, array $args)
+    {
+        foreach ($this->getIterator() as $plugin) {
+            call_user_func_array(array($plugin, $name), $args);
+        }
+        return true;
+    }
+
+    /**
+     * Returns the number of plugins contained within the handler.
+     *
+     * @return int Plugin count
+     */
+    public function count()
+    {
+        return count($this->plugins);
+    }
+}
diff --git a/plugins/Irc/extlib/phergie/Phergie/Plugin/Help.php b/plugins/Irc/extlib/phergie/Phergie/Plugin/Help.php
new file mode 100644 (file)
index 0000000..4c2c49b
--- /dev/null
@@ -0,0 +1,276 @@
+<?php
+/**
+ * Phergie
+ *
+ * PHP version 5
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.
+ * It is also available through the world-wide-web at this URL:
+ * http://phergie.org/license
+ *
+ * @category  Phergie
+ * @package   Phergie_Plugin_Help
+ * @author    Phergie Development Team <team@phergie.org>
+ * @copyright 2008-2010 Phergie Development Team (http://phergie.org)
+ * @license   http://phergie.org/license New BSD License
+ * @link      http://pear.phergie.org/package/Phergie_Plugin_Help
+ */
+
+/**
+ * Provides access to descriptions of plugins and the commands they provide.
+ *
+ * @category Phergie
+ * @package  Phergie_Plugin_Help
+ * @author   Phergie Development Team <team@phergie.org>
+ * @license  http://phergie.org/license New BSD License
+ * @link     http://pear.phergie.org/package/Phergie_Plugin_Help
+ * @uses     Phergie_Plugin_Command pear.phergie.org
+ */
+class Phergie_Plugin_Help extends Phergie_Plugin_Abstract
+{
+    /**
+     * Registry of help data indexed by plugin name
+     *
+     * @var array
+     */
+    protected $registry;
+
+    /**
+     * Checks for dependencies.
+     *
+     * @return void
+     */
+    public function onLoad()
+    {
+        $this->getPluginHandler()->getPlugin('Command');
+    }
+
+    /**
+     * Creates a registry of plugin metadata on connect.
+     *
+     * @return void
+     */
+    public function onConnect()
+    {
+        $this->populateRegistry();
+    }
+
+    /**
+     * Creates a registry of plugin metadata.
+     *
+     * @return void
+     */
+    public function populateRegistry()
+    {
+        $this->registry = array();
+
+        foreach ($this->plugins as $plugin) {
+            $class = new ReflectionClass($plugin);
+            $pluginName = strtolower($plugin->getName());
+
+            // Parse the plugin description
+            $docblock = $class->getDocComment();
+            $annotations = $this->getAnnotations($docblock);
+            if (isset($annotations['pluginDesc'])) {
+                $pluginDesc = implode(' ', $annotations['pluginDesc']);
+            } else {
+                $pluginDesc = $this->parseShortDescription($docblock);
+            }
+            $this->registry[$pluginName] = array(
+                'desc' => $pluginDesc,
+                'cmds' => array()
+            );
+
+            // Parse command method descriptions
+            $methodPrefix = Phergie_Plugin_Command::METHOD_PREFIX;
+            $methodPrefixLength = strlen($methodPrefix);
+            foreach ($class->getMethods() as $method) {
+                if (strpos($method->getName(), $methodPrefix) !== 0) {
+                    continue;
+                }
+
+                $cmd = strtolower(substr($method->getName(), $methodPrefixLength));
+                $docblock = $method->getDocComment();
+                $annotations = $this->getAnnotations($docblock);
+
+                if (isset($annotations['pluginCmd'])) {
+                    $cmdDesc = implode(' ', $annotations['pluginCmd']);
+                } else {
+                    $cmdDesc = $this->parseShortDescription($docblock);
+                }
+
+                $cmdParams = array();
+                if (!empty($annotations['param'])) {
+                    foreach ($annotations['param'] as $param) {
+                        $match = null;
+                        if (preg_match('/\h+\$([^\h]+)\h+/', $param, $match)) {
+                            $cmdParams[] = $match[1];
+                        }
+                    }
+                }
+
+                $this->registry[$pluginName]['cmds'][$cmd] = array(
+                    'desc' => $cmdDesc,
+                    'params' => $cmdParams
+                );
+            }
+
+            if (empty($this->registry[$pluginName]['cmds'])) {
+                unset($this->registry[$pluginName]);
+            }
+        }
+    }
+
+    /**
+     * Displays a list of plugins with help information available or
+     * commands available for a specific plugin.
+     *
+     * @param string $query Optional short name of a plugin for which commands
+     *        should be returned or a command; if unspecified, a list of
+     *        plugins with help information available is returned
+     *
+     * @return void
+     */
+    public function onCommandHelp($query = null)
+    {
+        if ($query == 'refresh') {
+            $this->populateRegistry();
+        }
+
+        $nick = $this->getEvent()->getNick();
+        $delay = $this->getConfig('help.delay', 2);
+
+        // Handle requests for a plugin list
+        if (!$query) {
+            $msg = 'These plugins have help information available: '
+                 . implode(', ', array_keys($this->registry));
+            $this->doPrivmsg($nick, $msg);
+            return;
+        }
+
+        // Handle requests for plugin information
+        $query = strtolower($query);
+        if (isset($this->registry[$query])
+            && empty($this->registry[$query]['cmds'][$query])) {
+            $msg = $query . ' - ' . $this->registry[$query]['desc'];
+            $this->doPrivmsg($nick, $msg);
+
+            $msg = 'Available commands - '
+                 . implode(', ', array_keys($this->registry[$query]['cmds']));
+            $this->doPrivmsg($nick, $msg);
+
+            if ($this->getConfig('command.prefix')) {
+                $msg
+                    = 'Note that these commands must be prefixed with "'
+                    . $this->getConfig('command.prefix')
+                    . '" (without quotes) when issued in a public channel.';
+                $this->doPrivmsg($nick, $msg);
+            }
+
+            return;
+        }
+
+        // Handle requests for command information
+        foreach ($this->registry as $plugin => $data) {
+            if (empty($data['cmds'])) {
+                continue;
+            }
+
+            $result = preg_grep('/^' . $query . '$/i', array_keys($data['cmds']));
+            if (!$result) {
+                continue;
+            }
+
+            $cmd = $data['cmds'][array_shift($result)];
+            $msg = $query;
+            if (!empty($cmd['params'])) {
+                $msg .= ' [' . implode('] [', $cmd['params']) . ']';
+            }
+            $msg .= ' - ' . $cmd['desc'];
+            $this->doPrivmsg($nick, $msg);
+        }
+    }
+
+    /**
+     * Parses and returns the short description from a docblock.
+     *
+     * @param string $docblock Docblock comment code
+     *
+     * @return string Short description (i.e. content from the start of the
+     *         docblock up to the first double-newline)
+     */
+    protected function parseShortDescription($docblock)
+    {
+        $desc = preg_replace(
+            array('#^\h*\*\h*#m', '#^/\*\*\h*\v+\h*#', '#(?:\r?\n){2,}.*#s', '#\s*\v+\s*#'),
+            array('', '', '', ' '),
+            $docblock
+        );
+        return $desc;
+    }
+
+    /**
+     * Taken from PHPUnit/Util/Test.php and modified to fix an issue with
+     * tag content spanning multiple lines.
+     *
+     * PHPUnit
+     *
+     * Copyright (c) 2002-2010, Sebastian Bergmann <sb@sebastian-bergmann.de>.
+     * All rights reserved.
+     *
+     * Redistribution and use in source and binary forms, with or without
+     * modification, are permitted provided that the following conditions
+     * are met:
+     *
+     *   * Redistributions of source code must retain the above copyright
+     *     notice, this list of conditions and the following disclaimer.
+     *
+     *   * Redistributions in binary form must reproduce the above copyright
+     *     notice, this list of conditions and the following disclaimer in
+     *     the documentation and/or other materials provided with the
+     *     distribution.
+     *
+     *   * Neither the name of Sebastian Bergmann nor the names of his
+     *     contributors may be used to endorse or promote products derived
+     *     from this software without specific prior written permission.
+     *
+     * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+     * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+     * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+     * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+     * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+     * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+     * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+     * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+     * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+     * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+     * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+     * POSSIBILITY OF SUCH DAMAGE.
+     *
+     * @param string $docblock docblock to parse
+     *
+     * @return array
+     */
+    protected function getAnnotations($docblock)
+    {
+        $annotations = array();
+
+        $regex = '/@(?P<name>[A-Za-z_-]+)(?:[ \t]+(?P<value>.*?))?(?:\*\/|\* @)/ms';
+
+        if (preg_match_all($regex, $docblock, $matches)) {
+            $numMatches = count($matches[0]);
+
+            for ($i = 0; $i < $numMatches; ++$i) {
+                $annotation = $matches['value'][$i];
+                $annotation = preg_replace('/\s*\v+\s*\*\s*/', ' ', $annotation);
+                $annotation = rtrim($annotation);
+                $annotations[$matches['name'][$i]][] = $annotation;
+            }
+        }
+
+        return $annotations;
+    }
+}
diff --git a/plugins/Irc/extlib/phergie/Phergie/Plugin/Http.php b/plugins/Irc/extlib/phergie/Phergie/Plugin/Http.php
new file mode 100644 (file)
index 0000000..7c5c870
--- /dev/null
@@ -0,0 +1,284 @@
+<?php
+/**
+ * Phergie
+ *
+ * PHP version 5
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.
+ * It is also available through the world-wide-web at this URL:
+ * http://phergie.org/license
+ *
+ * @category  Phergie
+ * @package   Phergie_Plugin_Http
+ * @author    Phergie Development Team <team@phergie.org>
+ * @copyright 2008-2010 Phergie Development Team (http://phergie.org)
+ * @license   http://phergie.org/license New BSD License
+ * @link      http://pear.phergie.org/package/Phergie_Plugin_Http
+ */
+
+/**
+ * Provides an HTTP client for plugins to use in contacting web services or
+ * retrieving feeds or web pages.
+ *
+ * @category Phergie
+ * @package  Phergie_Plugin_Http
+ * @author   Phergie Development Team <team@phergie.org>
+ * @license  http://phergie.org/license New BSD License
+ * @link     http://pear.phergie.org/package/Phergie_Plugin_Http
+ * @uses     extension simplexml optional
+ * @uses     extension json optional
+ */
+class Phergie_Plugin_Http extends Phergie_Plugin_Abstract
+{
+    /**
+     * Response to the last executed HTTP request
+     *
+     * @var Phergie_Plugin_Http_Response
+     */
+    protected $response;
+
+    /**
+     * Mapping of content types to handlers for them
+     *
+     * @var array
+     */
+    protected $handlers;
+
+    /**
+     * Initializes the handler lookup table.
+     *
+     * @return void
+     */
+    public function onLoad()
+    {
+        $this->handlers = array(
+            '(?:application|text)/xml(?:;.*)?'
+                => 'simplexml_load_string',
+            '(?:(?:application|text)/(?:x-)?json)|text/javascript.*'
+                => 'json_decode',
+        );
+
+        if (is_array($this->config['http.handlers'])) {
+            $this->handlers = array_merge(
+                $this->handlers,
+                $this->config['http.handlers']
+            );
+        }
+    }
+
+    /**
+     * Sets a handler callback for a content type, which is called when a
+     * response of that content type is received to perform any needed
+     * transformations on the response body content before storing it in the
+     * response object. Note that the calling plugin is responsible for
+     * indicating any dependencies related to specified handler callbacks.
+     *
+     * @param string   $type     PCRE regular expression (without delimiters) that
+     *        matches one or more MIME types
+     * @param callback $callback Callback to execute when a response of a content
+     *        type matched by $type is encountered
+     *
+     * @return Phergie_Plugin_Http Provides a fluent interface
+     */
+    public function setHandler($type, $callback)
+    {
+        if (!is_callable($callback)) {
+            throw new Phergie_Plugin_Exception(
+                'Invalid callback specified',
+                Phergie_Plugin_Exception::ERR_FATAL_ERROR
+            );
+        }
+
+        $this->handlers[$type] = $callback;
+
+        return $this;
+    }
+
+    /**
+     * Supporting method that parses the status line of an HTTP response
+     * message.
+     *
+     * @param string $status Status line
+     *
+     * @return array Associative array containing the HTTP version, response
+     *         code, and response description
+     */
+    protected function parseStatusLine($status)
+    {
+        $parts = explode(' ', $status, 3);
+        $parsed = array(
+            'version' => str_replace('HTTP/', '', $parts[0]),
+            'code' => $parts[1],
+            'message' => rtrim($parts[2])
+        );
+        return $parsed;
+    }
+
+    /**
+     * Supporting method that acts as an error handler to intercept HTTP
+     * responses resulting in PHP-level errors.
+     *
+     * @param int    $errno   Level of the error raised
+     * @param string $errstr  Error message
+     * @param string $errfile Name of the file in which the error was raised
+     * @param string $errline Line number on which the error was raised
+     *
+     * @return bool Always returns TRUE to allow normal execution to
+     *         continue once this method terminates
+     */
+    public function handleError($errno, $errstr, $errfile, $errline)
+    {
+        if ($httperr = strstr($errstr, 'HTTP/')) {
+            $parts = $this->parseStatusLine($httperr);
+            $this->response
+                ->setCode($parts['code'])
+                ->setMessage($parts['message']);
+        }
+
+        return true;
+    }
+
+    /**
+     * Supporting method that executes a request and handles the response.
+     *
+     * @param string $url     URL to request
+     * @param array  $context Associative array of stream context parameters
+     *
+     * @return Phergie_Plugin_Http_Response Object representing the response
+     *         resulting from the request
+     */
+    public function request($url, array $context)
+    {
+        $this->response = new Phergie_Plugin_Http_Response;
+
+        $url = (string) $url;
+        $context = stream_context_create(array('http' => $context));
+
+        set_error_handler(array($this, 'handleError'), E_WARNING);
+        $stream = fopen($url, 'r', false, $context);
+        if ($stream) {
+            $meta = stream_get_meta_data($stream);
+            $status = $this->parseStatusLine($meta['wrapper_data'][0]);
+            $code = $status['code'];
+            $message = $status['message'];
+            $headers = array();
+            foreach (array_slice($meta['wrapper_data'], 1) as $header) {
+                if (!strpos($header, ':')) {
+                    continue;
+                }
+                list($name, $value) = explode(': ', $header, 2);
+                $headers[$name] = $value;
+            }
+            unset($meta['wrapper_data']);
+
+            $this->response
+                ->setCode($code)
+                ->setMessage($message)
+                ->setHeaders($headers)
+                ->setMeta($meta);
+
+            $body = stream_get_contents($stream);
+            $type = $this->response->getHeaders('content-type');
+            foreach ($this->handlers as $expr => $handler) {
+                if (preg_match('#^' . $expr . '$#i', $type)) {
+                    $handled = call_user_func($handler, $body);
+                    if (!empty($handled)) {
+                        $body = $handled;
+                    }
+                }
+            }
+
+            $this->response->setContent($body);
+        }
+        restore_error_handler();
+
+        return $this->response;
+    }
+
+    /**
+     * Performs a GET request.
+     *
+     * @param string $url     URL for the request
+     * @param array  $query   Optional associative array of parameters
+     *        constituting the URL query string if $url has none
+     * @param array  $context Optional associative array of additional stream
+     *        context parameters
+     *
+     * @return Phergie_Plugin_Http_Response Received response data
+     */
+    public function get($url, array $query = array(), array $context = array())
+    {
+        if (!empty($query)) {
+            $url .= '?' . http_build_query($query);
+        }
+
+        $context['method'] = 'GET';
+
+        return $this->request($url, $context);
+    }
+
+    /**
+     * Performs a HEAD request.
+     *
+     * @param string $url     URL for the request
+     * @param array  $query   Optional associative array of parameters
+     *        constituting the URL query string if $url has none
+     * @param array  $context Optional associative array of additional stream
+     *        context parameters
+     *
+     * @return Phergie_Plugin_Http_Response Received response data
+     */
+    public function head($url, array $query = array(), array $context = array())
+    {
+        if (!empty($query)) {
+            $url .= '?' . http_build_query($query);
+        }
+
+        $context['method'] = 'HEAD';
+
+        return $this->request($url, $context);
+    }
+
+    /**
+     * Performs a POST request.
+     *
+     * @param string $url     URL for the request
+     * @param array  $query   Optional associative array of parameters
+     *        constituting the URL query string if $url has none
+     * @param array  $post    Optional associative array of parameters
+     *        constituting the POST request body if it is using the
+     *        traditional URL-encoded format
+     * @param array  $context Optional associative array of additional stream
+     *        context parameters
+     *
+     * @return Phergie_Plugin_Http_Response Received response data
+     */
+    public function post($url, array $query = array(),
+        array $post = array(), array $context = array()
+    ) {
+        if (!empty($query)) {
+            $url .= '?' . http_build_query($query);
+        }
+
+        $context['method'] = 'POST';
+
+        if (!empty($post)
+            && (!empty($context['header'])
+            xor stripos($context['header'], 'Content-Type'))
+        ) {
+            if (!empty($context['header'])) {
+                $context['header'] .= "\r\n";
+            } else {
+                $context['header'] = '';
+            }
+            $context['header'] .=
+                'Content-Type: application/x-www-form-urlencoded';
+            $context['content'] = http_build_query($post);
+        }
+
+        return $this->request($url, $context);
+    }
+}
\ No newline at end of file
diff --git a/plugins/Irc/extlib/phergie/Phergie/Plugin/Http/Response.php b/plugins/Irc/extlib/phergie/Phergie/Plugin/Http/Response.php
new file mode 100644 (file)
index 0000000..b9e377c
--- /dev/null
@@ -0,0 +1,281 @@
+<?php
+/**
+ * Phergie
+ *
+ * PHP version 5
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.
+ * It is also available through the world-wide-web at this URL:
+ * http://phergie.org/license
+ *
+ * @category  Phergie
+ * @package   Phergie_Plugin_Http
+ * @author    Phergie Development Team <team@phergie.org>
+ * @copyright 2008-2010 Phergie Development Team (http://phergie.org)
+ * @license   http://phergie.org/license New BSD License
+ * @link      http://pear.phergie.org/package/Phergie_Plugin_Http
+ */
+
+/**
+ * Data structure for HTTP response information.
+ *
+ * @category Phergie
+ * @package  Phergie_Plugin_Http
+ * @author   Phergie Development Team <team@phergie.org>
+ * @license  http://phergie.org/license New BSD License
+ * @link     http://pear.phergie.org/package/Phergie_Plugin_Http
+ */
+class Phergie_Plugin_Http_Response
+{
+    /**
+     * HTTP response code or 0 if no HTTP response was received
+     *
+     * @var string
+     */
+    protected $code;
+
+    /**
+     * HTTP response strings
+     *
+     * @var array
+     */
+    protected static $codeStrings = array(
+        0   => 'No Response',
+        100 => 'Continue',
+        200 => 'OK',
+        201 => 'Created',
+        204 => 'No Content',
+        206 => 'Partial Content',
+        300 => 'Multiple Choices',
+        301 => 'Moved Permanently',
+        302 => 'Found',
+        303 => 'See Other',
+        304 => 'Not Modified',
+        307 => 'Temporary Redirect',
+        400 => 'Bad Request',
+        401 => 'Unauthorized',
+        403 => 'Forbidden',
+        404 => 'Not Found',
+        405 => 'Method Not Allowed',
+        406 => 'Not Acceptable',
+        408 => 'Request Timeout',
+        410 => 'Gone',
+        413 => 'Request Entity Too Large',
+        414 => 'Request URI Too Long',
+        415 => 'Unsupported Media Type',
+        416 => 'Requested Range Not Satisfiable',
+        417 => 'Expectation Failed',
+        500 => 'Internal Server Error',
+        501 => 'Method Not Implemented',
+        503 => 'Service Unavailable',
+        506 => 'Variant Also Negotiates'
+    );
+
+    /**
+     * Description of the HTTP response code or the error message if no HTTP
+     * response was received
+     *
+     * @var string
+     */
+    protected $message;
+
+    /**
+     * Content of the response body, decoded for supported content types
+     *
+     * @var mixed
+     */
+    protected $content;
+
+    /**
+     * Associative array mapping response header names to their values
+     *
+     * @var array
+     */
+    protected $headers;
+
+    /**
+     * Associative array containing other metadata about the response
+     *
+     * @var array
+     */
+    protected $meta;
+
+    /**
+     * Sets the HTTP response code.
+     *
+     * @param string $code Response code
+     *
+     * @return Phergie_Plugin_Http_Response Provides a fluent interface
+     */
+    public function setCode($code)
+    {
+        $this->code = $code;
+        return $this;
+    }
+
+    /**
+     * Returns the HTTP response code.
+     *
+     * @return string Response code
+     */
+    public function getCode()
+    {
+        return $this->code;
+    }
+
+    /**
+     * Returns the HTTP response code text.
+     *
+     * @return string Response code text
+     */
+    public function getCodeAsString()
+    {
+        $code = $this->code;
+
+        if (!isset(self::$codeStrings[$code])) {
+            return 'Unkown HTTP Status';
+        }
+
+        return self::$codeStrings[$code];
+    }
+
+    /**
+     * Returns whether the response indicates a client- or server-side error.
+     *
+     * @return bool TRUE if the response indicates an error, FALSE otherwise
+     */
+    public function isError()
+    {
+        switch (substr($this->code, 0, 1)) {
+        case '0':
+        case '4':
+        case '5':
+            return true;
+        default:
+            return false;
+        }
+    }
+
+    /**
+     * Sets the HTTP response description.
+     *
+     * @param string $message Response description
+     *
+     * @return Phergie_Plugin_Http_Response Provides a fluent interface
+     */
+    public function setMessage($message)
+    {
+        $this->message = $message;
+        return $this;
+    }
+
+    /**
+     * Returns the HTTP response description.
+     *
+     * @return string
+     */
+    public function getMessage()
+    {
+        return $this->message;
+    }
+
+    /**
+     * Sets the content of the response body.
+     *
+     * @param mixed $content Response body content
+     *
+     * @return Phergie_Plugin_Http_Response Provides a fluent interface
+     */
+    public function setContent($content)
+    {
+        $this->content = $content;
+        return $this;
+    }
+
+    /**
+     * Returns the content of the response body.
+     *
+     * @return mixed Response body content, decoded for supported content
+     *         types
+     */
+    public function getContent()
+    {
+        return $this->content;
+    }
+
+    /**
+     * Sets the response headers.
+     *
+     * @param array $headers Associative array of response headers indexed
+     *        by header name
+     *
+     * @return Phergie_Plugin_Http_Response Provides a fluent interface
+     */
+    public function setHeaders(array $headers)
+    {
+        $names = array_map('strtolower', array_keys($headers));
+        $values = array_values($headers);
+        $this->headers = array_combine($names, $values);
+        return $this;
+    }
+
+    /**
+     * Returns all response headers or the value of a single specified
+     * response header.
+     *
+     * @param string $name Optional name of a single header for which the
+     *        associated value should be returned
+     *
+     * @return array|string Associative array of all header values, a string
+     *         containing the value of the header indicated by $name if one
+     *         is set, or null if one is not
+     */
+    public function getHeaders($name = null)
+    {
+        if ($name) {
+            $name = strtolower($name);
+            if (empty($this->headers[$name])) {
+                return null;
+            }
+            return $this->headers[$name];
+        }
+        return $this->headers;
+    }
+
+    /**
+     * Sets the response metadata.
+     *
+     * @param array $meta Associative array of response metadata
+     *
+     * @return Phergie_Plugin_Http_Response Provides a fluent interface
+     */
+    public function setMeta(array $meta)
+    {
+        $this->meta = $meta;
+        return $this;
+    }
+
+    /**
+     * Returns all metadata or the value of a single specified metadatum.
+     *
+     * @param string $name Optional name of a single metadatum for which the
+     *        associated value should be returned
+     *
+     * @return array|string|null Associative array of all metadata values, a
+     *         string containing the value of the metadatum indicated by
+     *         $name if one is set, or null if one is not
+     */
+    public function getMeta($name = null)
+    {
+        if ($name) {
+            if (empty($this->meta[$name])) {
+                return null;
+            }
+            return $this->meta[$name];
+        }
+        return $this->meta;
+    }
+}
diff --git a/plugins/Irc/extlib/phergie/Phergie/Plugin/Ideone.php b/plugins/Irc/extlib/phergie/Phergie/Plugin/Ideone.php
new file mode 100644 (file)
index 0000000..3af88dd
--- /dev/null
@@ -0,0 +1,180 @@
+<?php
+/**
+ * Phergie
+ *
+ * PHP version 5
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.
+ * It is also available through the world-wide-web at this URL:
+ * http://phergie.org/license
+ *
+ * @category  Phergie
+ * @package   Phergie_Plugin_Ideone
+ * @author    Phergie Development Team <team@phergie.org>
+ * @copyright 2008-2010 Phergie Development Team (http://phergie.org)
+ * @license   http://phergie.org/license New BSD License
+ * @link      http://pear.phergie.org/package/Phergie_Plugin_Ideone
+ */
+
+/**
+ * Interfaces with ideone.com to execute code and return the result.
+ *
+ * @category Phergie
+ * @package  Phergie_Plugin_Ideone
+ * @author   Phergie Development Team <team@phergie.org>
+ * @license  http://phergie.org/license New BSD License
+ * @link     http://pear.phergie.org/package/Phergie_Plugin_Ideone
+ * @uses     Phergie_Plugin_Command pear.phergie.org
+ */
+class Phergie_Plugin_Ideone extends Phergie_Plugin_Abstract
+{
+    /**
+     * Checks for dependencies.
+     *
+     * @return void
+     */
+    public function onLoad()
+    {
+        $this->plugins->getPlugin('Command');
+    }
+
+    /**
+     * Checks a service response for an error, sends a notice to the event
+     * source if an error has occurred, and returns whether an error was found.
+     *
+     * @param array $result Associative array representing the service response
+     *
+     * @return boolean TRUE if an error is found, FALSE otherwise
+     */
+    protected function isError($result)
+    {
+        if ($result['error'] != 'OK') {
+            $this->doNotice($this->event->getNick(), 'ideone error: ' . $result['error']);
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Executes a source code sequence in a specified language and returns
+     * the result.
+     *
+     * @param string $language Programming language the source code is in
+     * @param string $code     Source code to execute
+     *
+     * @return void
+     */
+    public function onCommandIdeone($language, $code)
+    {
+        $source = $this->event->getSource();
+        $nick = $this->event->getNick();
+
+        // Get authentication credentials
+        $user = $this->getConfig('ideone.user', 'test');
+        $pass = $this->getConfig('ideone.pass', 'test');
+
+        // Normalize the command parameters
+        $language = strtolower($language);
+
+        // Massage PHP code to allow for convenient shorthand
+        if ($language == 'php') {
+            if (!preg_match('/^<\?(?:php)?/', $code)) {
+                $code = '<?php ' . $code;
+            }
+            switch (substr($code, -1)) {
+                case '}':
+                case ';':
+                    break;
+                default:
+                    $code .= ';';
+                    break;
+            }
+        }
+
+        // Identify the language to use
+        $client = new SoapClient('http://ideone.com/api/1/service.wsdl');
+        $response = $client->getLanguages($user, $pass);
+        if ($this->isError($response)) {
+            return;
+        }
+        $languageLength = strlen($language);
+        foreach ($response['languages'] as $languageId => $languageName) {
+            if (strncasecmp($language, $languageName, $languageLength) == 0) {
+                break;
+            }
+        }
+
+        // Send the paste data
+        $response = $client->createSubmission(
+            $user,
+            $pass,
+            $code,
+            $languageId,
+            null, // string input - data from stdin
+            true, // boolean run - TRUE to execute the code
+            false // boolean private - FALSE to make the paste public
+        );
+        if ($this->isError($response)) {
+            return;
+        }
+        $link = $response['link'];
+
+        // Wait until the paste data is processed or the service fails
+        $attempts = $this->getConfig('ideone.attempts', 10);
+        foreach (range(1, $attempts) as $attempt) {
+            $response = $client->getSubmissionStatus($user, $pass, $link);
+            if ($this->isError($response)) {
+                return;
+            }
+            if ($response['status'] == 0) {
+                $result = $response['result'];
+                break;
+            } else {
+                $result = null;
+                sleep(1);
+            }
+        }
+        if ($result == null) {
+            $this->doNotice($nick, 'ideone error: Timed out');
+            return;
+        }
+        if ($result != 15) {
+            $this->doNotice($nick, 'ideone error: Status code ' . $result);
+            return;
+        }
+
+        // Get details for the created paste
+        $response = $client->getSubmissionDetails(
+            $user,
+            $pass,
+            $link,
+            false, // boolean withSource - FALSE to not return the source code
+            false, // boolean withInput - FALSE to not return stdin data
+            true,  // boolean withOutput - TRUE to include output
+            true,  // boolean withStderr - TRUE to return stderr data
+            false  // boolean withCmpinfo - TRUE to return compilation info
+        );
+        if ($this->isError($response)) {
+            return;
+        }
+
+        // Replace the output if it exceeds a specified maximum length
+        $outputLimit = $this->getConfig('ideone.output_limit', 100);
+        var_dump($response);
+        if ($outputLimit && strlen($response['output']) > $outputLimit) {
+            $response['output'] = 'Output is too long to post';
+        }
+
+        // Format the message
+        $msg = $this->getConfig('ideone.format', '%nick%: [ %link% ] %output%');
+        $response['nick'] = $nick;
+        $response['link'] = 'http://ideone.com/' . $link;
+        foreach ($response as $key => $value) {
+            $msg = str_replace('%' . $key . '%', $value, $msg);
+        }
+        $this->doPrivmsg($source, $msg);
+    }
+}
diff --git a/plugins/Irc/extlib/phergie/Phergie/Plugin/Invisible.php b/plugins/Irc/extlib/phergie/Phergie/Plugin/Invisible.php
new file mode 100755 (executable)
index 0000000..622f7d3
--- /dev/null
@@ -0,0 +1,44 @@
+<?php
+/**
+ * Phergie 
+ *
+ * PHP version 5
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.
+ * It is also available through the world-wide-web at this URL:
+ * http://phergie.org/license
+ *
+ * @category  Phergie 
+ * @package   Phergie_Plugin_Invisible
+ * @author    Phergie Development Team <team@phergie.org>
+ * @copyright 2008-2010 Phergie Development Team (http://phergie.org)
+ * @license   http://phergie.org/license New BSD License
+ * @link      http://pear.phergie.org/package/Phergie_Plugin_Invisible
+ */
+
+/**
+ * Marks the bot as invisible when it connects to the server.
+ *
+ * @category Phergie 
+ * @package  Phergie_Plugin_Invisible
+ * @author   Phergie Development Team <team@phergie.org>
+ * @license  http://phergie.org/license New BSD License
+ * @link     http://pear.phergie.org/package/Phergie_Plugin_Invisible
+ * @link     http://irchelp.org/irchelp/rfc/chapter4.html#c4_2_3_2
+ */
+class Phergie_Plugin_Invisible extends Phergie_Plugin_Abstract
+{
+    /**
+     * Marks the bot as invisible when it connects to the server.
+     *
+     * @return void
+     */
+    public function onConnect()
+    {
+        $this->doMode($this->getConnection()->getNick(), '+i');
+        $this->getPluginHandler()->removePlugin($this);
+    }
+}
diff --git a/plugins/Irc/extlib/phergie/Phergie/Plugin/Iterator.php b/plugins/Irc/extlib/phergie/Phergie/Plugin/Iterator.php
new file mode 100644 (file)
index 0000000..d610962
--- /dev/null
@@ -0,0 +1,142 @@
+<?php
+/**
+ * Phergie
+ *
+ * PHP version 5
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.
+ * It is also available through the world-wide-web at this URL:
+ * http://phergie.org/license
+ *
+ * @category  Phergie
+ * @package   Phergie
+ * @author    Phergie Development Team <team@phergie.org>
+ * @copyright 2008-2010 Phergie Development Team (http://phergie.org)
+ * @license   http://phergie.org/license New BSD License
+ * @link      http://pear.phergie.org/package/Phergie
+ */
+
+/**
+ * Implements a filtering iterator for limiting executing of methods across
+ * a group of plugins.
+ *
+ * @category Phergie
+ * @package  Phergie
+ * @author   Phergie Development Team <team@phergie.org>
+ * @license  http://phergie.org/license New BSD License
+ * @link     http://pear.phergie.org/package/Phergie
+ */
+class Phergie_Plugin_Iterator extends FilterIterator
+{
+    /**
+     * List of short names of plugins to exclude when iterating
+     *
+     * @var array
+     */
+    protected $plugins = array();
+
+    /**
+     * List of method names where plugins with these methods will be
+     * excluded when iterating
+     *
+     * @var array
+     */
+    protected $methods = array();
+
+    /**
+     * Overrides the parent constructor to reset the internal iterator's
+     * pointer to the current item, which the parent class errantly does not
+     * do.
+     *
+     * @param Iterator $iterator Iterator to filter
+     *
+     * @return void
+     * @link http://bugs.php.net/bug.php?id=52560
+     */
+    public function __construct(Iterator $iterator)
+    {
+        parent::__construct($iterator);
+        $this->rewind();
+    }
+
+    /**
+     * Adds to a list of plugins to exclude when iterating.
+     *
+     * @param mixed $plugins String containing the short name of a single
+     *        plugin to exclude or an array of short names of multiple
+     *        plugins to exclude
+     *
+     * @return Phergie_Plugin_Iterator Provides a fluent interface
+     */
+    public function addPluginFilter($plugins)
+    {
+        if (is_array($plugins)) {
+            $this->plugins = array_unique(
+                array_merge($this->plugins, $plugins)
+            );
+        } else {
+            $this->plugins[] = $plugins;
+        }
+        return $this;
+    }
+
+    /**
+     * Adds to a list of method names where plugins defining these methods
+     * will be excluded when iterating.
+     *
+     * @param mixed $methods String containing the name of a single method
+     *        or an array containing the name of multiple methods
+     *
+     * @return Phergie_Plugin_Iterator Provides a fluent interface
+     */
+    public function addMethodFilter($methods)
+    {
+        if (is_array($methods)) {
+            $this->methods = array_merge($this->methods, $methods);
+        } else {
+            $this->methods[]= $methods;
+        }
+        return $this;
+    }
+
+    /**
+     * Clears any existing plugin and methods filters.
+     *
+     * @return Phergie_Plugin_Iterator Provides a fluent interface
+     */
+    public function clearFilters()
+    {
+        $this->plugins = array();
+        $this->methods = array();
+    }
+
+    /**
+     * Implements FilterIterator::accept().
+     *
+     * @return boolean TRUE to include the current item in those by returned
+     *         during iteration, FALSE otherwise
+     */
+    public function accept()
+    {
+        if (!$this->plugins && !$this->methods) {
+            return true;
+        }
+
+        $current = $this->current();
+
+        if (in_array($current->getName(), $this->plugins)) {
+            return false;
+        }
+
+        foreach ($this->methods as $method) {
+            if (method_exists($current, $method)) {
+                return false;
+            }
+        }
+
+        return true;
+    }
+}
diff --git a/plugins/Irc/extlib/phergie/Phergie/Plugin/Join.php b/plugins/Irc/extlib/phergie/Phergie/Plugin/Join.php
new file mode 100755 (executable)
index 0000000..50c3ad8
--- /dev/null
@@ -0,0 +1,56 @@
+<?php
+/**
+ * Phergie 
+ *
+ * PHP version 5
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.
+ * It is also available through the world-wide-web at this URL:
+ * http://phergie.org/license
+ *
+ * @category  Phergie 
+ * @package   Phergie_Plugin_Join
+ * @author    Phergie Development Team <team@phergie.org>
+ * @copyright 2008-2010 Phergie Development Team (http://phergie.org)
+ * @license   http://phergie.org/license New BSD License
+ * @link      http://pear.phergie.org/package/Phergie_Plugin_Join
+ */
+
+/**
+ * Joins a specified channel on command from a user.
+ *
+ * @category Phergie 
+ * @package  Phergie_Plugin_Join
+ * @author   Phergie Development Team <team@phergie.org>
+ * @license  http://phergie.org/license New BSD License
+ * @link     http://pear.phergie.org/package/Phergie_Plugin_Join
+ * @uses     Phergie_Plugin_Command pear.phergie.org
+ */
+class Phergie_Plugin_Join extends Phergie_Plugin_Abstract
+{
+    /**
+     * Checks for dependencies.
+     *
+     * @return void
+     */
+    public function onLoad()
+    {
+        $this->getPluginHandler()->getPlugin('Command');
+    }
+
+    /**
+     * Joins a channel.
+     *
+     * @param string $channels Comma-delimited list of channels to join
+     * @param string $keys     Optional comma-delimited list of channel keys
+     *
+     * @return void
+     */
+    public function onCommandJoin($channels, $keys = null)
+    {
+        $this->doJoin($channels, $keys);
+    }
+}
diff --git a/plugins/Irc/extlib/phergie/Phergie/Plugin/Karma.php b/plugins/Irc/extlib/phergie/Phergie/Plugin/Karma.php
new file mode 100644 (file)
index 0000000..e6227be
--- /dev/null
@@ -0,0 +1,427 @@
+<?php
+/**
+ * Phergie
+ *
+ * PHP version 5
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.
+ * It is also available through the world-wide-web at this URL:
+ * http://phergie.org/license
+ *
+ * @category  Phergie
+ * @package   Phergie_Plugin_Karma
+ * @author    Phergie Development Team <team@phergie.org>
+ * @copyright 2008-2010 Phergie Development Team (http://phergie.org)
+ * @license   http://phergie.org/license New BSD License
+ * @link      http://pear.phergie.org/package/Phergie_Plugin_Karma
+ */
+
+/**
+ * Handles requests for incrementation or decrementation of a maintained list
+ * of counters for specified terms.
+ *
+ * @category Phergie
+ * @package  Phergie_Plugin_Karma
+ * @author   Phergie Development Team <team@phergie.org>
+ * @license  http://phergie.org/license New BSD License
+ * @link     http://pear.phergie.org/package/Phergie_Plugin_Karma
+ * @uses     extension PDO
+ * @uses     extension pdo_sqlite
+ * @uses     Phergie_Plugin_Command pear.phergie.org
+ * @uses     Phergie_Plugin_Message pear.phergie.org
+ */
+class Phergie_Plugin_Karma extends Phergie_Plugin_Abstract
+{
+    /**
+     * SQLite object
+     *
+     * @var resource
+     */
+    protected $db = null;
+
+    /**
+     * Prepared statement to add a new karma record
+     *
+     * @var PDOStatement
+     */
+    protected $insertKarma;
+
+    /**
+     * Prepared statement to update an existing karma record
+     *
+     * @var PDOStatement
+     */
+    protected $updateKarma;
+
+    /**
+     * Retrieves an existing karma record
+     *
+     * @var PDOStatement
+     */
+    protected $fetchKarma;
+
+    /**
+     * Retrieves an existing fixed karma record
+     *
+     * @var PDOStatement
+     */
+    protected $fetchFixedKarma;
+
+    /**
+     * Retrieves a positive answer for a karma comparison
+     *
+     * @var PDOStatement
+     */
+    protected $fetchPositiveAnswer;
+
+    /**
+     * Retrieves a negative answer for a karma comparison
+     *
+     * @var PDOStatement
+     */
+    protected $fetchNegativeAnswer;
+
+    /**
+     * Check for dependencies and initializes a database connection and
+     * prepared statements.
+     *
+     * @return void
+     */
+    public function onLoad()
+    {
+        $plugins = $this->getPluginHandler();
+        $plugins->getPlugin('Command');
+        $plugins->getPlugin('Message');
+
+        $file = dirname(__FILE__) . '/Karma/karma.db';
+        $this->db = new PDO('sqlite:' . $file);
+
+        $this->fetchKarma = $this->db->prepare('
+            SELECT karma
+            FROM karmas
+            WHERE term = :term
+            LIMIT 1
+        ');
+
+        $this->insertKarma = $this->db->prepare('
+            INSERT INTO karmas (term, karma)
+            VALUES (:term, :karma)
+        ');
+
+        $this->updateKarma = $this->db->prepare('
+            UPDATE karmas
+            SET karma = :karma
+            WHERE term = :term
+        ');
+
+        $this->fetchFixedKarma = $this->db->prepare('
+            SELECT karma
+            FROM fixed_karmas
+            WHERE term = :term
+            LIMIT 1
+        ');
+
+        $this->fetchPositiveAnswer = $this->db->prepare('
+            SELECT answer
+            FROM positive_answers
+            ORDER BY RANDOM()
+            LIMIT 1
+        ');
+
+        $this->fetchNegativeAnswer = $this->db->prepare('
+            SELECT answer
+            FROM negative_answers
+            ORDER BY RANDOM()
+            LIMIT 1
+        ');
+    }
+
+    /**
+     * Get the canonical form of a given term.
+     *
+     * In the canonical form all sequences of whitespace
+     * are replaced by a single space and all characters
+     * are lowercased.
+     *
+     * @param string $term Term for which a canonical form is required
+     *
+     * @return string Canonical term
+     */
+    protected function getCanonicalTerm($term)
+    {
+        $canonicalTerm = strtolower(preg_replace('|\s+|', ' ', trim($term, '()')));
+        switch ($canonicalTerm) {
+            case 'me':
+                $canonicalTerm = strtolower($this->event->getNick());
+                break;
+            case 'all':
+            case '*':
+            case 'everything':
+                $canonicalTerm = 'everything';
+                break;
+        }
+        return $canonicalTerm;
+    }
+
+    /**
+     * Intercepts a message and processes any contained recognized commands.
+     *
+     * @return void
+     */
+    public function onPrivmsg()
+    {
+        $message = $this->getEvent()->getText();
+
+        $termPattern = '\S+?|\([^<>]+?\)+';
+        $actionPattern = '(?P<action>\+\+|--)';
+
+        $modifyPattern = <<<REGEX
+               {^
+               (?J) # allow overwriting capture names
+               \s*  # ignore leading whitespace
+
+               (?:  # start with ++ or -- before the term
+            $actionPattern
+            (?P<term>$termPattern)
+               |   # follow the term with ++ or --
+            (?P<term>$termPattern)
+                       $actionPattern # allow no whitespace between the term and the action
+               )
+               $}ix
+REGEX;
+
+        $versusPattern = <<<REGEX
+        {^
+               (?P<term0>$termPattern)
+                       \s+(?P<method><|>)\s+
+               (?P<term1>$termPattern)$#
+        $}ix
+REGEX;
+
+        $match = null;
+
+        if (preg_match($modifyPattern, $message, $match)) {
+            $action = $match['action'];
+            $term = $this->getCanonicalTerm($match['term']);
+            $this->modifyKarma($term, $action);
+        } elseif (preg_match($versusPattern, $message, $match)) {
+            $term0 = trim($match['term0']);
+            $term1 = trim($match['term1']);
+            $method = $match['method'];
+            $this->compareKarma($term0, $term1, $method);
+        }
+    }
+
+    /**
+     * Get the karma rating for a given term.
+     *
+     * @param string $term Term for which the karma rating needs to be
+     *        retrieved
+     *
+     * @return void
+     */
+    public function onCommandKarma($term)
+    {
+        $source = $this->getEvent()->getSource();
+        $nick = $this->getEvent()->getNick();
+
+        if (empty($term)) {
+            return;
+        }
+
+        $canonicalTerm = $this->getCanonicalTerm($term);
+
+        $fixedKarma = $this->fetchFixedKarma($canonicalTerm);
+        if ($fixedKarma) {
+            $message = $nick . ': ' . $term . ' ' . $fixedKarma . '.';
+            $this->doPrivmsg($source, $message);
+            return;
+        }
+
+        $karma = $this->fetchKarma($canonicalTerm);
+
+        $message = $nick . ': ';
+
+        if ($term == 'me') {
+            $message .= 'You have';
+        } else {
+            $message .= $term . ' has';
+        }
+
+        $message .= ' ';
+
+        if ($karma) {
+            $message .= 'karma of ' . $karma;
+        } else {
+            $message .= 'neutral karma';
+        }
+
+        $message .= '.';
+
+        $this->doPrivmsg($source, $message);
+    }
+
+    /**
+     * Resets the karma for a term to 0.
+     *
+     * @param string $term Term for which to reset the karma rating
+     *
+     * @return void
+     */
+    public function onCommandReincarnate($term)
+    {
+        $data = array(
+            ':term' => $term,
+            ':karma' => 0
+        );
+        $this->updateKarma->execute($data);
+    }
+
+    /**
+     * Compares the karma between two terms. Optionally increases/decreases
+     * the karma of either term.
+     *
+     * @param string $term0  First term
+     * @param string $term1  Second term
+     * @param string $method Comparison method (< or >)
+     *
+     * @return void
+     */
+    protected function compareKarma($term0, $term1, $method)
+    {
+        $event = $this->getEvent();
+        $nick = $event->getNick();
+        $source = $event->getSource();
+
+        $canonicalTerm0 = $this->getCanonicalTerm($term0);
+        $canonicalTerm1 = $this->getCanonicalTerm($term1);
+
+        $fixedKarma0 = $this->fetchFixedKarma($canonicalTerm0);
+        $fixedKarma1 = $this->fetchFixedKarma($canonicalTerm1);
+
+        if ($fixedKarma0
+            || $fixedKarma1
+            || empty($canonicalTerm0)
+            || empty($canonicalTerm1)
+        ) {
+            return;
+        }
+
+        if ($canonicalTerm0 == 'everything') {
+            $change = $method == '<' ? '++' : '--';
+            $this->modifyKarma($canonicalTerm1, $change);
+            $karma0 = 0;
+            $karma1 = $this->fetchKarma($canonicalTerm1);
+        } elseif ($canonicalTerm1 == 'everything') {
+            $change = $method == '<' ? '--' : '++';
+            $this->modifyKarma($canonicalTerm0, $change);
+            $karma0 = $this->fetchKarma($canonicalTerm1);
+            $karma1 = 0;
+        } else {
+            $karma0 = $this->fetchKarma($canonicalTerm0);
+            $karma1 = $this->fetchKarma($canonicalTerm1);
+        }
+
+        if (($method == '<'
+            && $karma0 < $karma1)
+            || ($method == '>'
+            && $karma0 > $karma1)) {
+            $replies = $this->fetchPositiveAnswer;
+        } else {
+            $replies = $this->fetchNegativeAnswer;
+        }
+        $replies->execute();
+        $reply = $replies->fetchColumn();
+
+        if (max($karma0, $karma1) == $karma1) {
+            list($canonicalTerm0, $canonicalTerm1) =
+                array($canonicalTerm1, $canonicalTerm0);
+        }
+
+        $message = str_replace(
+            array('%owner%','%owned%'),
+            array($canonicalTerm0, $canonicalTerm1),
+            $reply
+        );
+
+        $this->doPrivmsg($source, $message);
+    }
+
+    /**
+     * Modifes a term's karma.
+     *
+     * @param string $term   Term to modify
+     * @param string $action Karma action (either ++ or --)
+     *
+     * @return void
+     */
+    protected function modifyKarma($term, $action)
+    {
+        if (empty($term)) {
+            return;
+        }
+
+        $karma = $this->fetchKarma($term);
+        if ($karma !== false) {
+            $statement = $this->updateKarma;
+        } else {
+            $statement = $this->insertKarma;
+        }
+
+        $karma += ($action == '++') ? 1 : -1;
+
+        $args = array(
+            ':term'  => $term,
+            ':karma' => $karma
+        );
+        $statement->execute($args);
+    }
+
+    /**
+     * Returns the karma rating for a specified term for which the karma
+     * rating can be modified.
+     *
+     * @param string $term Term for which to fetch the corresponding karma
+     *        rating
+     *
+     * @return integer|boolean Integer value denoting the term's karma or
+     *         FALSE if there is the specified term has no associated karma
+     *         rating
+     */
+    protected function fetchKarma($term)
+    {
+        $this->fetchKarma->execute(array(':term' => $term));
+        $result = $this->fetchKarma->fetch(PDO::FETCH_ASSOC);
+
+        if ($result === false) {
+            return false;
+        }
+
+        return (int) $result['karma'];
+    }
+
+    /**
+     * Returns a phrase describing the karma rating for a specified term for
+     * which the karma rating is fixed.
+     *
+     * @param string $term Term for which to fetch the corresponding karma
+     *        rating
+     *
+     * @return string Phrase describing the karma rating, which may be append
+     *         to the term to form a complete response
+     */
+    protected function fetchFixedKarma($term)
+    {
+        $this->fetchFixedKarma->execute(array(':term' => $term));
+        $result = $this->fetchFixedKarma->fetch(PDO::FETCH_ASSOC);
+
+        if ($result === false) {
+            return false;
+        }
+
+        return $result['karma'];
+    }
+}
diff --git a/plugins/Irc/extlib/phergie/Phergie/Plugin/Lart.php b/plugins/Irc/extlib/phergie/Phergie/Plugin/Lart.php
new file mode 100644 (file)
index 0000000..d00cae0
--- /dev/null
@@ -0,0 +1,303 @@
+<?php
+/**
+ * Phergie
+ *
+ * PHP version 5
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.
+ * It is also available through the world-wide-web at this URL:
+ * http://phergie.org/license
+ *
+ * @category  Phergie
+ * @package   Phergie_Plugin_Lart
+ * @author    Phergie Development Team <team@phergie.org>
+ * @copyright 2008-2010 Phergie Development Team (http://phergie.org)
+ * @license   http://phergie.org/license New BSD License
+ * @link      http://pear.phergie.org/package/Phergie_Plugin_Lart
+ */
+
+/**
+ * Accepts terms and corresponding definitions for storage to a local data
+ * source and performs and returns the result of lookups for term definitions
+ * as they are requested.
+ *
+ * @category Phergie
+ * @package  Phergie_Plugin_Lart
+ * @author   Phergie Development Team <team@phergie.org>
+ * @license  http://phergie.org/license New BSD License
+ * @link     http://pear.phergie.org/package/Phergie_Plugin_Lart
+ * @uses     Phergie_Plugin_Command pear.phergie.org
+ * @uses     extension PDO
+ * @uses     extension pdo_sqlite
+ */
+class Phergie_Plugin_Lart extends Phergie_Plugin_Abstract
+{
+    /**
+     * PDO instance for the database
+     *
+     * @var PDO
+     */
+    protected $db;
+
+    /**
+     * Prepared statement for inserting a new definition
+     *
+     * @var PDOStatement
+     */
+    protected $save;
+
+    /**
+     * Prepared statement for deleting the definition for a given term
+     *
+     * @var PDOStatement
+     */
+    protected $delete;
+
+    /**
+     * Prepared statement for searching for a definition for which the term
+     * matches as a regular expression against a given search string
+     *
+     * @var PDOStatement
+     */
+    protected $process;
+
+    /**
+     * Prepared statement for searching for a definition by its exact term
+     *
+     * @var PDOStatement
+     */
+    protected $select;
+
+    /**
+     * Checks for dependencies and initializes the database.
+     *
+     * @return void
+     */
+    public function onLoad()
+    {
+        if (!extension_loaded('PDO') || !extension_loaded('pdo_sqlite')) {
+            $this->fail('PDO and pdo_sqlite extensions must be installed');
+        }
+
+        $this->plugins->getPlugin('Command');
+
+        $dir = dirname(__FILE__) . '/' . $this->getName();
+        $path = $dir . '/lart.db';
+        $exists = file_exists($path);
+        if (!$exists) {
+            mkdir($dir);
+        }
+
+        try {
+            $this->db = new PDO('sqlite:' . $path);
+        } catch (PDO_Exception $e) {
+            throw new Phergie_Plugin_Exception($e->getMessage());
+        }
+
+        $this->db->sqliteCreateFunction('preg_match', 'preg_match');
+
+        if (!$exists) {
+            $this->db->exec('
+                CREATE TABLE lart (
+                    name VARCHAR(255),
+                    definition TEXT,
+                    hostmask VARCHAR(50),
+                    tstamp VARCHAR(19)
+                )
+            ');
+            $this->db->exec('
+                CREATE UNIQUE INDEX lart_name ON lart (name)
+            ');
+        }
+
+        $this->save = $this->db->prepare('
+            REPLACE INTO lart (name, definition, hostmask, tstamp)
+            VALUES (:name, :definition, :hostmask, :tstamp)
+        ');
+
+        $this->process = $this->db->prepare('
+            SELECT *
+            FROM lart
+            WHERE preg_match(name, :name)
+        ');
+
+        $this->select = $this->db->prepare('
+            SELECT *
+            FROM lart
+            WHERE name = :name
+        ');
+
+        $this->delete = $this->db->prepare('
+            DELETE FROM lart
+            WHERE name = :name
+        ');
+    }
+
+    /**
+     * Retrieves the definition for a given term if it exists.
+     *
+     * @param string $term Term to search for
+     *
+     * @return mixed String containing the definition or FALSE if no definition
+     *               exists
+     */
+    protected function getLart($term)
+    {
+        $this->process->execute(array(':name' => $term));
+        $row = $this->process->fetchObject();
+        if ($row === false) {
+            return false;
+        }
+        preg_match($row->name, $term, $match);
+        $definition = preg_replace(
+            "/(?:\\\\|\\$)([0-9]+)/e",
+            '$match[\1]',
+            $row->definition
+        );
+        $event = $this->getEvent();
+        $definition = str_replace(
+            array('$source', '$nick'),
+            array($event->getSource(), $event->getNick()),
+            $definition
+        );
+        return $definition;
+    }
+
+    /**
+     * Deletes a given definition.
+     *
+     * @param string $term Term for which the definition should be deleted
+     *
+     * @return boolean TRUE if the definition was found and deleted, FALSE
+     *         otherwise
+     */
+    protected function deleteLart($term)
+    {
+        $this->delete->execute(array(':name' => $term));
+        return ($this->delete->rowCount() > 0);
+    }
+
+    /**
+     * Saves a given definition.
+     *
+     * @param string $term       Term to trigger a response containing the
+     *        corresponding definition, may be a regular expression
+     * @param string $definition Definition corresponding to the term
+     *
+     * @return boolean TRUE if the definition was saved successfully, FALSE
+     *         otherwise
+     */
+    protected function saveLart($term, $definition)
+    {
+        $data = array(
+            ':name' => $term,
+            ':definition' => $definition,
+            ':hostmask' => (string) $this->getEvent()->getHostmask(),
+            ':tstamp' => time()
+        );
+        $this->save->execute($data);
+        return ($this->save->rowCount() > 0);
+    }
+
+    /**
+     * Returns information about a definition.
+     *
+     * @param string $term Term about which to return information
+     *
+     * @return void
+     */
+    public function onCommandLartinfo($term)
+    {
+        $this->select->execute(array(':name' => $term));
+        $row = $this->select->fetchObject();
+        $msg = $this->getEvent()->getNick() . ': ';
+        if (!$row) {
+            $msg .= 'Lart not found';
+        } else {
+            $msg .= 'Term: ' . $row->name
+                . ', Definition: ' . $row->definition
+                . ', User: ' . $row->hostmask
+                . ', Added: ' . date('n/j/y g:i A', $row->tstamp);
+        }
+        $this->doNotice($this->getEvent()->getSource(), $msg);
+    }
+
+    /**
+     * Creates a new definition.
+     *
+     * @param string $term       Term to add
+     * @param string $definition Definition to add
+     *
+     * @return void
+     */
+    public function onCommandAddlart($term, $definition)
+    {
+        $result = $this->saveLart($term, $definition);
+        if ($result) {
+            $msg = 'Lart saved successfully';
+        } else {
+            $msg = 'Lart could not be saved';
+        }
+        $this->doNotice($this->getEvent()->getSource(), $msg);
+    }
+
+    /**
+     * Removes an existing definition.
+     *
+     * @param string $term Term for which the definition should be removed
+     *
+     * @return void
+     */
+    public function onCommandDeletelart($term)
+    {
+        $source = $this->getEvent()->getSource();
+        if ($this->deleteLart($term)) {
+            $msg = 'Lart deleted successfully';
+        } else {
+            $msg = 'Lart not found';
+        }
+        $this->doNotice($source, $msg);
+    }
+
+    /**
+     * Processes definition triggers in the text of the current event.
+     *
+     * @return void
+     */
+    protected function processLart()
+    {
+        $lart = $this->getLart($this->getEvent()->getText());
+        if ($lart) {
+            if (strpos($lart, '/me') === 0) {
+                $lart = substr($lart, 4);
+                $method = 'doAction';
+            } else {
+                $method = 'doPrivmsg';
+            }
+            $this->$method($this->getEvent()->getSource(), $lart);
+        }
+    }
+
+    /**
+     * Processes definition triggers in messages.
+     *
+     * @return void
+     */
+    public function onPrivmsg()
+    {
+        $this->processLart();
+    }
+
+    /**
+     * Processes definition triggers in CTCP actions.
+     *
+     * @return void
+     */
+    public function onAction()
+    {
+        $this->processLart();
+    }
+}
diff --git a/plugins/Irc/extlib/phergie/Phergie/Plugin/Message.php b/plugins/Irc/extlib/phergie/Phergie/Plugin/Message.php
new file mode 100644 (file)
index 0000000..4dfbb30
--- /dev/null
@@ -0,0 +1,112 @@
+<?php
+/**
+ * Phergie
+ *
+ * PHP version 5
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.
+ * It is also available through the world-wide-web at this URL:
+ * http://phergie.org/license
+ *
+ * @category  Phergie
+ * @package   Phergie_Plugin_Message
+ * @author    Phergie Development Team <team@phergie.org>
+ * @copyright 2008-2010 Phergie Development Team (http://phergie.org)
+ * @license   http://phergie.org/license New BSD License
+ * @link      http://pear.phergie.org/package/Phergie_Plugin_Message
+ */
+
+/**
+ * Generalized plugin providing utility methods for
+ * prefix and bot named based message extraction.
+ *
+ * @category Phergie
+ * @package  Phergie_Plugin_Message
+ * @author   Phergie Development Team <team@phergie.org>
+ * @license  http://phergie.org/license New BSD License
+ * @link     http://pear.phergie.org/package/Phergie_Plugin_Message
+ */
+class Phergie_Plugin_Message extends Phergie_Plugin_Abstract
+{
+
+    /**
+     * Check whether a message is specifically targeted at the bot.
+     * This is the case when the message starts with the bot's name
+     * followed by [,:>] or when it is a private message.
+     *
+     * @return boolean true when the message is specifically targeted at the bot,
+     *                 false otherwise.
+     */
+    public function isTargetedMessage()
+    {
+        $event = $this->getEvent();
+
+        $self = preg_quote($this->connection->getNick());
+
+        $targetPattern = <<<REGEX
+        {^
+        \s*{$self}\s*[:>,].* # expect the bots name, followed by a [:>,]
+        $}ix
+REGEX;
+
+        return !$event->isInChannel()
+            || preg_match($targetPattern, $event->getText()) > 0;
+    }
+
+    /**
+     * Allow for prefix and bot name aware extraction of a message
+     *
+     * @return string|bool $message The message, which is possibly targeted at the
+     *                              bot or false if a prefix requirement failed
+     */
+    public function getMessage()
+    {
+        $event = $this->getEvent();
+
+        $prefix = preg_quote($this->getConfig('command.prefix'));
+        $self = preg_quote($this->connection->getNick());
+        $message = $event->getText();
+
+        // $prefixPattern matches : Phergie, do command <parameters>
+        // where $prefix = 'do'   : do command <parameters>
+        //                        : Phergie, command <parameters>
+        $prefixPattern = <<<REGEX
+        {^
+        (?:
+               \s*{$self}\s*[:>,]\s* # start with bot name
+                       (?:{$prefix})?        # which is optionally followed by the prefix
+        |
+               \s*{$prefix}          # or start with the prefix
+        )
+        \s*(.*)                   # always end with the message
+        $}ix
+REGEX;
+
+        // $noPrefixPattern matches : Phergie, command <parameters>
+        //                          : command <parameters>
+        $noPrefixPattern = <<<REGEX
+        {^
+        \s*(?:{$self}\s*[:>,]\s*)? # optionally start with the bot name
+        (.*?)                      # always end with the message
+        $}ix
+REGEX;
+
+        $pattern = $noPrefixPattern;
+
+        // If a prefix is set, force it as a requirement
+        if ($prefix && $event->isInChannel()) {
+            $pattern = $prefixPattern;
+        }
+
+        $match = null;
+
+        if (!preg_match($pattern, $message, $match)) {
+            return false;
+        }
+
+        return $match[1];
+    }
+}
\ No newline at end of file
diff --git a/plugins/Irc/extlib/phergie/Phergie/Plugin/NickServ.php b/plugins/Irc/extlib/phergie/Phergie/Plugin/NickServ.php
new file mode 100644 (file)
index 0000000..e97abec
--- /dev/null
@@ -0,0 +1,178 @@
+<?php\r
+/**\r
+ * Phergie\r
+ *\r
+ * PHP version 5\r
+ *\r
+ * LICENSE\r
+ *\r
+ * This source file is subject to the new BSD license that is bundled\r
+ * with this package in the file LICENSE.\r
+ * It is also available through the world-wide-web at this URL:\r
+ * http://phergie.org/license\r
+ *\r
+ * @category  Phergie\r
+ * @package   Phergie_Plugin_NickServ\r
+ * @author    Phergie Development Team <team@phergie.org>\r
+ * @copyright 2008-2010 Phergie Development Team (http://phergie.org)\r
+ * @license   http://phergie.org/license New BSD License\r
+ * @link      http://pear.phergie.org/package/Phergie_Plugin_NickServ\r
+ */\r
+\r
+/**\r
+ * Intercepts and responds to messages from the NickServ agent requesting that\r
+ * the bot authenticate its identify.\r
+ *\r
+ * The password configuration setting should contain the password registered\r
+ * with NickServ for the nick used by the bot.\r
+ *\r
+ * @category Phergie\r
+ * @package  Phergie_Plugin_NickServ\r
+ * @author   Phergie Development Team <team@phergie.org>\r
+ * @license  http://phergie.org/license New BSD License\r
+ * @link     http://pear.phergie.org/package/Phergie_Plugin_NickServ\r
+ * @uses     Phergie_Plugin_Command pear.phergie.org\r
+ */\r
+class Phergie_Plugin_NickServ extends Phergie_Plugin_Abstract\r
+{\r
+    /**\r
+     * Nick of the NickServ bot\r
+     *\r
+     * @var string\r
+     */\r
+    protected $botNick;\r
+\r
+    /**\r
+    * Identify message\r
+    */\r
+    protected $identifyMessage;\r
+\r
+    /**\r
+     * Checks for dependencies and required configuration settings.\r
+     *\r
+     * @return void\r
+     */\r
+    public function onLoad()\r
+    {\r
+        $this->getPluginHandler()->getPlugin('Command');\r
+\r
+        // Get the name of the NickServ bot, defaults to NickServ\r
+        $this->botNick = $this->getConfig('nickserv.botnick', 'NickServ');\r
+\r
+        // Get the identify message\r
+        $this->identifyMessage = $this->getConfig(\r
+            'nickserv.identify_message',\r
+            '/This nickname is registered./'\r
+        );\r
+    }\r
+\r
+    /**\r
+     * Checks for a notice from NickServ and responds accordingly if it is an\r
+     * authentication request or a notice that a ghost connection has been\r
+     * killed.\r
+     *\r
+     * @return void\r
+     */\r
+    public function onNotice()\r
+    {\r
+        $event = $this->event;\r
+        if (strtolower($event->getNick()) == strtolower($this->botNick)) {\r
+            $message = $event->getArgument(1);\r
+            $nick = $this->connection->getNick();\r
+            if (preg_match($this->identifyMessage, $message)) {\r
+                $password = $this->config['nickserv.password'];\r
+                if (!empty($password)) {\r
+                    $this->doPrivmsg($this->botNick, 'IDENTIFY ' . $password);\r
+                }\r
+                unset($password);\r
+            } elseif (preg_match('/^.*' . $nick . '.* has been killed/', $message)) {\r
+                $this->doNick($nick);\r
+            }\r
+        }\r
+    }\r
+\r
+    /**\r
+     * Checks to see if the original nick has quit; if so, take the name back.\r
+     *\r
+     * @return void\r
+     */\r
+    public function onQuit()\r
+    {\r
+        $eventNick = $this->event->getNick();\r
+        $nick = $this->connection->getNick();\r
+        if ($eventNick == $nick) {\r
+            $this->doNick($nick);\r
+        }\r
+    }\r
+\r
+    /**\r
+     * Changes the in-memory configuration setting for the bot nick if it is\r
+     * successfully changed.\r
+     *\r
+     * @return void\r
+     */\r
+    public function onNick()\r
+    {\r
+        $event = $this->event;\r
+        $connection = $this->connection;\r
+        if ($event->getNick() == $connection->getNick()) {\r
+            $connection->setNick($event->getArgument(0));\r
+        }\r
+    }\r
+\r
+    /**\r
+     * Provides a command to terminate ghost connections.\r
+     *\r
+     * @return void\r
+     */\r
+    public function onCommandGhostbust()\r
+    {\r
+        $event = $this->event;\r
+        $user = $event->getNick();\r
+        $conn = $this->connection;\r
+        $nick = $conn->getNick();\r
+\r
+        if ($nick != $this->config['connections'][$conn->getHost()]['nick']) {\r
+            $password = $this->config['nickserv.password'];\r
+            if (!empty($password)) {\r
+                $this->doPrivmsg(\r
+                    $this->event->getSource(),\r
+                    $user . ': Attempting to ghost ' . $nick .'.'\r
+                );\r
+                $this->doPrivmsg(\r
+                    $this->botNick,\r
+                    'GHOST ' . $nick . ' ' . $password,\r
+                    true\r
+                );\r
+            }\r
+        }\r
+    }\r
+\r
+    /**\r
+     * Automatically send the GHOST command if the bot's nick is in use.\r
+     *\r
+     * @return void\r
+     */\r
+    public function onResponse()\r
+    {\r
+        if ($this->event->getCode() == Phergie_Event_Response::ERR_NICKNAMEINUSE) {\r
+            $password = $this->config['nickserv.password'];\r
+            if (!empty($password)) {\r
+                $this->doPrivmsg(\r
+                    $this->botNick,\r
+                    'GHOST ' . $this->connection->getNick() . ' ' . $password\r
+                );\r
+            }\r
+        }\r
+    }\r
+\r
+    /**\r
+     * Handle the server sending a KILL request.\r
+     *\r
+     * @return void\r
+     */\r
+    public function onKill()\r
+    {\r
+        $this->doQuit($this->event->getArgument(1));\r
+    }\r
+}\r
diff --git a/plugins/Irc/extlib/phergie/Phergie/Plugin/Part.php b/plugins/Irc/extlib/phergie/Phergie/Plugin/Part.php
new file mode 100755 (executable)
index 0000000..c07cdd9
--- /dev/null
@@ -0,0 +1,55 @@
+<?php
+/**
+ * Phergie 
+ *
+ * PHP version 5
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.
+ * It is also available through the world-wide-web at this URL:
+ * http://phergie.org/license
+ *
+ * @category  Phergie 
+ * @package   Phergie_Plugin_Part
+ * @author    Phergie Development Team <team@phergie.org>
+ * @copyright 2008-2010 Phergie Development Team (http://phergie.org)
+ * @license   http://phergie.org/license New BSD License
+ * @link      http://pear.phergie.org/package/Phergie_Plugin_Part
+ */
+
+/**
+ * Parts a specified channel on command from a user.
+ *
+ * @category Phergie 
+ * @package  Phergie_Plugin_Part
+ * @author   Phergie Development Team <team@phergie.org>
+ * @license  http://phergie.org/license New BSD License
+ * @link     http://pear.phergie.org/package/Phergie_Plugin_Part
+ * @uses     Phergie_Plugin_Command pear.phergie.org
+ */
+class Phergie_Plugin_Part extends Phergie_Plugin_Abstract
+{
+    /**
+     * Checks for dependencies.
+     *
+     * @return void
+     */
+    public function onLoad()
+    {
+        $this->getPluginHandler()->getPlugin('Command');
+    }
+
+    /**
+     * Parts a channel.
+     *
+     * @param string $channels Comma-delimited list of channels to leave
+     *
+     * @return void
+     */
+    public function onCommandPart($channels)
+    {
+        $this->doPart($channels);
+    }
+}
diff --git a/plugins/Irc/extlib/phergie/Phergie/Plugin/Php.php b/plugins/Irc/extlib/phergie/Phergie/Plugin/Php.php
new file mode 100644 (file)
index 0000000..e10d101
--- /dev/null
@@ -0,0 +1,78 @@
+<?php
+/**
+ * Phergie 
+ *
+ * PHP version 5
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.
+ * It is also available through the world-wide-web at this URL:
+ * http://phergie.org/license
+ *
+ * @category  Phergie 
+ * @package   Phergie_Plugin_Php
+ * @author    Phergie Development Team <team@phergie.org>
+ * @copyright 2008-2010 Phergie Development Team (http://phergie.org)
+ * @license   http://phergie.org/license New BSD License
+ * @link      http://pear.phergie.org/package/Phergie_Plugin_Php
+ */
+
+/**
+ * Returns information on PHP functions as requested. 
+ *
+ * @category Phergie 
+ * @package  Phergie_Plugin_Php
+ * @author   Phergie Development Team <team@phergie.org>
+ * @license  http://phergie.org/license New BSD License
+ * @link     http://pear.phergie.org/package/Phergie_Plugin_Php
+ * @uses     extension pdo 
+ * @uses     extension pdo_sqlite 
+ * @uses     Phergie_Plugin_Command pear.phergie.org
+ */
+class Phergie_Plugin_Php extends Phergie_Plugin_Abstract
+{
+    /**
+     * Data source to use
+     *
+     * @var Phergie_Plugin_Php_Source
+     */
+    protected $source;
+
+    /**
+     * Check for dependencies.
+     *
+     * @return void
+     */
+    public function onLoad()
+    {
+        // @todo find a way to move this to Phergie_Plugin_Php_Source_Local
+        if (!extension_loaded('PDO') || !extension_loaded('pdo_sqlite')) {
+            $this->fail('PDO and pdo_sqlite extensions must be installed');
+        }
+
+        $this->getPluginHandler()->getPlugin('Command');
+
+        $this->source = new Phergie_Plugin_Php_Source_Local;
+    }
+
+    /**
+     * Searches the data source for the requested function.
+     * 
+     * @param string $functionName Name of the function to search for
+     *
+     * @return void
+     */
+    public function onCommandPhp($functionName)
+    {
+        $nick = $this->event->getNick();
+        if ($function = $this->source->findFunction($functionName)) {
+            $msg = $nick . ': ' . $function['description'];
+            $this->doPrivmsg($this->event->getSource(), $msg);
+        } else {
+            $msg = 'Search for function ' . $functionName . ' returned no results.';
+            $this->doNotice($nick, $msg);
+        }
+    }
+}
diff --git a/plugins/Irc/extlib/phergie/Phergie/Plugin/Php/Source.php b/plugins/Irc/extlib/phergie/Phergie/Plugin/Php/Source.php
new file mode 100644 (file)
index 0000000..c6cf326
--- /dev/null
@@ -0,0 +1,46 @@
+<?php
+/**
+ * Phergie 
+ *
+ * PHP version 5
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.
+ * It is also available through the world-wide-web at this URL:
+ * http://phergie.org/license
+ *
+ * @category  Phergie 
+ * @package   Phergie_Plugin_Php
+ * @author    Phergie Development Team <team@phergie.org>
+ * @copyright 2008-2010 Phergie Development Team (http://phergie.org)
+ * @license   http://phergie.org/license New BSD License
+ * @link      http://pear.phergie.org/package/Phergie_Plugin_Php
+ */
+
+/**
+ * Data source interface for the Php plugin.
+ *
+ * @category Phergie 
+ * @package  Phergie_Plugin_Php
+ * @author   Phergie Development Team <team@phergie.org>
+ * @license  http://phergie.org/license New BSD License
+ * @link     http://pear.phergie.org/package/Phergie_Plugin_Php
+ * @uses     extension pdo 
+ * @uses     extension pdo_sqlite 
+ * @uses     Phergie_Plugin_Command pear.phergie.org
+ */
+interface Phergie_Plugin_Php_Source
+{
+    /**
+     * Searches for a description of the function.
+     * 
+     * @param string $function Search pattern to match against the function 
+     *        name, wildcards supported using %
+     *
+     * @return array|null Associative array containing the function name and 
+     *         description or NULL if no results are found
+     */
+    public function findFunction($function);
+}
diff --git a/plugins/Irc/extlib/phergie/Phergie/Plugin/Php/Source/Local.php b/plugins/Irc/extlib/phergie/Phergie/Plugin/Php/Source/Local.php
new file mode 100644 (file)
index 0000000..9292bea
--- /dev/null
@@ -0,0 +1,208 @@
+<?php
+/**
+ * Phergie 
+ *
+ * PHP version 5
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.
+ * It is also available through the world-wide-web at this URL:
+ * http://phergie.org/license
+ *
+ * @category  Phergie 
+ * @package   Phergie_Plugin_Php
+ * @author    Phergie Development Team <team@phergie.org>
+ * @copyright 2008-2010 Phergie Development Team (http://phergie.org)
+ * @license   http://phergie.org/license New BSD License
+ * @link      http://pear.phergie.org/package/Phergie_Plugin_Php
+ */
+
+/**
+ * Data source for {@see Phergie_Plugin_Php}. This source reads function 
+ * descriptions from a file and stores them in a SQLite database. When a 
+ * function description is requested, the function is retrieved from the 
+ * local database.
+ *
+ * @category Phergie 
+ * @package  Phergie_Plugin_Php
+ * @author   Phergie Development Team <team@phergie.org>
+ * @license  http://phergie.org/license New BSD License
+ * @link     http://pear.phergie.org/package/Phergie_Plugin_Php
+ * @uses     extension pdo 
+ * @uses     extension pdo_sqlite 
+ * @uses     Phergie_Plugin_Command pear.phergie.org
+ */
+class Phergie_Plugin_Php_Source_Local implements Phergie_Plugin_Php_Source
+{
+    /**
+     * Local database for storage
+     *
+     * @var PDO 
+     */
+    protected $database;
+
+    /**
+     * Source of the PHP function summary
+     *
+     * @var string
+     */
+    protected $url = 'http://cvs.php.net/viewvc.cgi/phpdoc/funcsummary.txt?revision=HEAD';
+
+    /**
+     * Constructor to initialize the data source.
+     *
+     * @return void
+     */
+    public function __construct()
+    {
+        $path = dirname(__FILE__);
+
+        try {
+            $this->database = new PDO('sqlite:' . $path . '/functions.db');
+            $this->buildDatabase();
+            // @todo Modify this to be rethrown as an appropriate 
+            //       Phergie_Plugin_Exception and handled in Phergie_Plugin_Php
+        } catch (PDOException $e) {
+            echo 'PDO failure: '.$e->getMessage();
+        } 
+    }
+
+    /**
+     * Searches for a description of the function.
+     * 
+     * @param string $function Search pattern to match against the function 
+     *        name, wildcards supported using %
+     *
+     * @return array|null Associative array containing the function name and 
+     *         description or NULL if no results are found
+     */
+    public function findFunction($function)
+    {
+        // Remove possible parentheses
+        $split = preg_split('{\(|\)}', $function);
+        $function = (count($split)) ? array_shift($split) : $function;
+
+        // Prepare the database statement
+        $stmt = $this->database->prepare('SELECT `name`, `description` FROM `functions` WHERE `name` LIKE :function');
+        $stmt->execute(array(':function' => $function));
+
+        // Check the results
+        if (count($stmt) > 0) {
+            $result = $stmt->fetch(PDO::FETCH_ASSOC);
+            /**
+             * @todo add class and function URLS
+             * class methods: http://php.net/manual/en/classname.methodname.php
+             * functions: http://php.net/manual/en/function.functionname.php
+             * where '_' is replaced with '-'
+             */
+            return $result;
+        }
+
+        // No results found, return
+        return null;
+    }
+
+    /**
+     * Build the database and parses the function summary file into it.
+     *
+     * @param bool $rebuild TRUE to force a rebuild of the table used to 
+     *        house function information, FALSE otherwise, defaults to FALSE
+     *
+     * @return void
+     */
+    protected function buildDatabase($rebuild = false)
+    {
+        // Check to see if the functions table exists
+        $checkstmt = $this->database->query("SELECT COUNT(*) FROM `sqlite_master` WHERE `name` = 'functions'");
+        $checkstmt->execute();
+        $result = $checkstmt->fetch(PDO::FETCH_ASSOC);
+        unset( $checkstmt );
+        $table = $result['COUNT(*)'];
+        unset( $result );
+        // If the table doesn't exist, create it
+        if (!$table) {
+                $this->database->exec('CREATE TABLE `functions` (`name` VARCHAR(255), `description` TEXT)');
+                $this->database->exec('CREATE UNIQUE INDEX `functions_name` ON `functions` (`name`)');
+        }
+
+        // If we created a new table, fill it with data
+        if (!$table || $rebuild) {
+            // Get the contents of the source file
+            // @todo Handle possible error cases better here; the @ operator 
+            //       shouldn't be needed
+            $contents = @file($this->url, FILE_IGNORE_NEW_LINES + FILE_SKIP_EMPTY_LINES);
+
+            if (!$contents) {
+                return;
+            }
+
+            // Parse the contents
+            $valid = array();
+            $firstPart = '';
+            $lineNumber = 0;
+            foreach ($contents as $line) {
+                // Clean the current line
+                $line = trim($line);
+
+                // Skip comment lines
+                if (0 === strpos($line, '#')) {
+                    // reset the line if the current line is odd
+                    if (($lineNumber % 2) !== 0) {
+                        $lineNumber--;
+                    }
+                    continue;
+                }
+
+                /*
+                 * If the current line is even, it's the first part of the
+                 * complete function description ...
+                 */
+                if (($lineNumber % 2) === 0) {
+                    $firstPart = $line;
+                } else {
+                    // ... it's the last part of the complete function description
+                    $completeLine = $firstPart . ' ' . $line;
+                    $firstPart = '';
+                    if (preg_match('{^([^\s]*)[\s]?([^)]*)\(([^\)]*)\)[\sU]+([\sa-zA-Z0-9\.,\-_()]*)$}', $completeLine, $matches)) {
+                        $valid[] = $matches;
+                    }
+                }
+                // Up the line number before going to the next line
+                $lineNumber++;
+            }
+            // free up some memory
+            unset($contents);
+
+            // Process the valid matches
+            if (count($valid) > 0) {
+                // Clear the database
+                $this->database->exec('DELETE * FROM `functions`');
+
+                // Prepare the sql statement
+                $stmt = $this->database->prepare('INSERT INTO `functions` (`name`, `description`) VALUES (:name, :description)');
+                $this->database->beginTransaction();
+
+                // Insert the data
+                foreach ($valid as $function) {
+                    // Extract function values
+                    list( , $retval, $name, $params, $desc) = $function;
+                    if (empty($name)) {
+                        $name = $retval;
+                        $retval = '';
+                    }
+                    // Reconstruct the complete function line
+                    $line = trim($retval . ' ' . $name . '(' . $params . ') - ' . $desc);
+                    // Execute the statement
+                    $stmt->execute(array(':name' => $name, ':description' => $line));
+                }
+                
+                // Commit the changes to the database
+                $this->database->commit();
+            }
+            // free up some more memory
+            unset($valid);
+        }
+    }
+}
diff --git a/plugins/Irc/extlib/phergie/Phergie/Plugin/Ping.php b/plugins/Irc/extlib/phergie/Phergie/Plugin/Ping.php
new file mode 100755 (executable)
index 0000000..021670d
--- /dev/null
@@ -0,0 +1,162 @@
+<?php
+/**
+ * Phergie
+ *
+ * PHP version 5
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.
+ * It is also available through the world-wide-web at this URL:
+ * http://phergie.org/license
+ *
+ * @category  Phergie
+ * @package   Phergie_Plugin_Ping
+ * @author    Phergie Development Team <team@phergie.org>
+ * @copyright 2008-2010 Phergie Development Team (http://phergie.org)
+ * @license   http://phergie.org/license New BSD License
+ * @link      http://pear.phergie.org/package/Phergie_Plugin_Ping
+ */
+
+/**
+ * Uses a self CTCP PING to ensure that the client connection has not been
+ * dropped.
+ *
+ * @category Phergie
+ * @package  Phergie_Plugin_Ping
+ * @author   Phergie Development Team <team@phergie.org>
+ * @license  http://phergie.org/license New BSD License
+ * @link     http://pear.phergie.org/package/Phergie_Plugin_Ping
+ */
+class Phergie_Plugin_Ping extends Phergie_Plugin_Abstract
+{
+    /**
+     * Timestamp for the last instance in which an event was received
+     *
+     * @var int
+     */
+    protected $lastEvent;
+
+    /**
+     * Timestamp for the last instance in which a PING was sent
+     *
+     * @var int
+     */
+    protected $lastPing;
+
+    /**
+     * Initialize event timestamps upon connecting to the server.
+     *
+     * @return void
+     */
+    public function onConnect()
+    {
+        $this->lastEvent = time();
+        $this->lastPing = null;
+    }
+
+    /**
+     * Updates the timestamp since the last received event when a new event
+     * arrives.
+     *
+     * @return void
+     */
+    public function preEvent()
+    {
+        $this->lastEvent = time();
+    }
+
+    /**
+     * Clears the ping time if a reply is received.
+     *
+     * @return void
+     */
+    public function onPingResponse()
+    {
+        $this->lastPing = null;
+    }
+
+    /**
+     * Performs a self ping if the event threshold has been exceeded or
+     * issues a termination command if the ping threshold has been exceeded.
+     *
+     * @return void
+     */
+    public function onTick()
+    {
+        $time = time();
+        if (!empty($this->lastPing)) {
+            if ($time - $this->lastPing > $this->getConfig('ping.ping', 20)) {
+                $this->doQuit();
+            }
+        } elseif (
+            $time - $this->lastEvent > $this->getConfig('ping.event', 300)
+        ) {
+            $this->lastPing = $time;
+            $this->doPing($this->getConnection()->getNick(), $this->lastPing);
+        }
+    }
+
+    /**
+     * Gets the last ping time
+     * lastPing needs exposing for things such as unit testing
+     *
+     * @return int timestamp of last ping
+     */
+    public function getLastPing()
+    {
+        return $this->lastPing;
+    }
+
+    /**
+     * Set the last ping time
+     * lastPing needs to be exposed for unit testing
+     *
+     * @param int|null $ping timestamp of last ping
+     *
+     * @return self
+     */
+    public function setLastPing($ping = null)
+    {
+        if (null === $ping) {
+            $ping = time();
+        }
+        if (!is_int($ping)) {
+            throw new InvalidArgumentException('$ping must be an integer or null');
+        }
+        $this->lastPing = $ping;
+        return $this;
+    }
+
+    /**
+     * Gets the last event time
+     * lastEvent needs exposing for things such as unit testing
+     *
+     * @return int timestamp of last ping
+     */
+    public function getLastEvent()
+    {
+        return $this->lastEvent;
+    }
+
+    /**
+     * Set the last event time
+     * lastEvent needs to be exposed for unit testing
+     *
+     * @param int|null $event timestamp of last ping
+     *
+     * @return self
+     */
+    public function setLastEvent($event = null)
+    {
+        if (null === $event) {
+            $event = time();
+        }
+        if (!is_int($event)) {
+            throw new InvalidArgumentException('$ping must be an integer or null');
+        }
+        $this->lastEvent = $event;
+        return $this;
+    }
+}
diff --git a/plugins/Irc/extlib/phergie/Phergie/Plugin/Pong.php b/plugins/Irc/extlib/phergie/Phergie/Plugin/Pong.php
new file mode 100755 (executable)
index 0000000..54e19fc
--- /dev/null
@@ -0,0 +1,44 @@
+<?php
+/**
+ * Phergie 
+ *
+ * PHP version 5
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.
+ * It is also available through the world-wide-web at this URL:
+ * http://phergie.org/license
+ *
+ * @category  Phergie 
+ * @package   Phergie_Plugin_Pong
+ * @author    Phergie Development Team <team@phergie.org>
+ * @copyright 2008-2010 Phergie Development Team (http://phergie.org)
+ * @license   http://phergie.org/license New BSD License
+ * @link      http://pear.phergie.org/package/Phergie_Plugin_Pong
+ */
+
+/**
+ * Responds to PING requests from the server.
+ *
+ * @category Phergie 
+ * @package  Phergie_Plugin_Pong
+ * @author   Phergie Development Team <team@phergie.org>
+ * @license  http://phergie.org/license New BSD License
+ * @link     http://pear.phergie.org/package/Phergie_Plugin_Pong
+ * @link     http://irchelp.org/irchelp/rfc/chapter4.html#c4_6_2
+ * @link     http://irchelp.org/irchelp/rfc/chapter4.html#c4_6_3
+ */
+class Phergie_Plugin_Pong extends Phergie_Plugin_Abstract
+{
+    /**
+     * Sends a PONG response for each PING request received by the server. 
+     *
+     * @return void
+     */
+    public function onPing()
+    {
+        $this->doPong($this->getEvent()->getArgument(0));
+    }
+}
diff --git a/plugins/Irc/extlib/phergie/Phergie/Plugin/Prioritize.php b/plugins/Irc/extlib/phergie/Phergie/Plugin/Prioritize.php
new file mode 100755 (executable)
index 0000000..2312567
--- /dev/null
@@ -0,0 +1,99 @@
+<?php
+/**
+ * Phergie 
+ *
+ * PHP version 5
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.
+ * It is also available through the world-wide-web at this URL:
+ * http://phergie.org/license
+ *
+ * @category  Phergie 
+ * @package   Phergie_Plugin_Prioritize
+ * @author    Phergie Development Team <team@phergie.org>
+ * @copyright 2008-2010 Phergie Development Team (http://phergie.org)
+ * @license   http://phergie.org/license New BSD License
+ * @link      http://pear.phergie.org/package/Phergie_Plugin_Prioritize
+ */
+
+/**
+ * Prioritizes events such that they are executed in order from least to most 
+ * destructive.
+ *
+ * @category Phergie 
+ * @package  Phergie_Plugin_Prioritize
+ * @author   Phergie Development Team <team@phergie.org>
+ * @license  http://phergie.org/license New BSD License
+ * @link     http://pear.phergie.org/package/Phergie_Plugin_Prioritize
+ */
+class Phergie_Plugin_Prioritize extends Phergie_Plugin_Abstract
+{
+    /** 
+     * Event types ordered by priority of execution
+     *
+     * @var array
+     */
+    protected $priority = array(
+        'raw',
+        'pass',
+        'user',
+        'ping',
+        'pong',
+        'notice',
+        'join',
+        'list',
+        'names',
+        'version',
+        'stats',
+        'links',
+        'time',
+        'trace',
+        'admin',
+        'info',
+        'who',
+        'whois',
+        'whowas',
+        'mode',
+        'privmsg',
+        'action',
+        'nick',
+        'topic',
+        'invite',
+        'kill',
+        'part',
+        'quit'
+    );  
+
+    /**
+     * Prioritizes events from least to most destructive. 
+     *
+     * @return void 
+     */
+    public function preDispatch()
+    {
+        $events = $this->getEventHandler();
+
+        // Categorize events by type
+        $categorized = array();
+        foreach ($events as $event) {
+            $type = $event->getType();
+            if (!isset($categorized[$type])) {
+                $categorized[$type] = array();
+            }
+            $categorized[$type][] = $event;
+        }
+
+        // Order events by type from least to most destructive
+        $types = array_intersect($this->priority, array_keys($categorized));
+        $prioritized = array();
+        foreach ($types as $type) {
+            $prioritized = array_merge($prioritized, $categorized[$type]);
+        }
+
+        // Replace the original events array with the prioritized one
+        $events->replaceEvents($prioritized);
+    }
+}
diff --git a/plugins/Irc/extlib/phergie/Phergie/Plugin/Puppet.php b/plugins/Irc/extlib/phergie/Phergie/Plugin/Puppet.php
new file mode 100644 (file)
index 0000000..bede0be
--- /dev/null
@@ -0,0 +1,89 @@
+<?php
+/**
+ * Phergie 
+ *
+ * PHP version 5
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.
+ * It is also available through the world-wide-web at this URL:
+ * http://phergie.org/license
+ *
+ * @category  Phergie 
+ * @package   Phergie_Plugin_Puppet
+ * @author    Phergie Development Team <team@phergie.org>
+ * @copyright 2008-2010 Phergie Development Team (http://phergie.org)
+ * @license   http://phergie.org/license New BSD License
+ * @link      http://pear.phergie.org/package/Phergie_Plugin_Puppet
+ */
+
+/**
+ * Allows a user to effectively speak and act as the bot.
+ *
+ * @category Phergie 
+ * @package  Phergie_Plugin_Puppet
+ * @author   Phergie Development Team <team@phergie.org>
+ * @license  http://phergie.org/license New BSD License
+ * @link     http://pear.phergie.org/package/Phergie_Plugin_Puppet
+ * @uses     Phergie_Plugin_Command pear.phergie.org
+ */
+class Phergie_Plugin_Puppet extends Phergie_Plugin_Abstract
+{
+    /**
+     * Checks for dependencies.
+     *
+     * @return void
+     */
+    public function onLoad()
+    {
+        $this->getPluginHandler()->getPlugin('Command');
+    }
+
+    /**
+     * Handles a request for the bot to repeat a given message in a specified
+     * channel.
+     *
+     * <code>say #chan message</code>
+     *
+     * @param string $channel Name of the channel
+     * @param string $message Message to repeat
+     *
+     * @return void
+     */
+    public function onCommandSay($channel, $message)
+    {
+        $this->doPrivmsg($channel, $message);
+    }
+
+    /**
+     * Handles a request for the bot to repeat a given action in a specified
+     * channel.
+     *
+     * <code>act #chan action</code>
+     *
+     * @param string $channel Name of the channel
+     * @param string $action  Action to perform
+     *
+     * @return void
+     */
+    public function onCommandAct($channel, $action)
+    {
+        $this->doAction($channel, $action);
+    }
+
+    /**
+     * Handles a request for the bot to send the server a raw message
+     *
+     * <code>raw message</code>
+     *
+     * @param string $message Message to send
+     *
+     * @return void
+     */
+    public function onCommandRaw($message)
+    {
+        $this->doRaw($message);
+    }
+}
diff --git a/plugins/Irc/extlib/phergie/Phergie/Plugin/Quit.php b/plugins/Irc/extlib/phergie/Phergie/Plugin/Quit.php
new file mode 100755 (executable)
index 0000000..eca22a9
--- /dev/null
@@ -0,0 +1,54 @@
+<?php
+/**
+ * Phergie
+ *
+ * PHP version 5
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.
+ * It is also available through the world-wide-web at this URL:
+ * http://phergie.org/license
+ *
+ * @category  Phergie
+ * @package   Phergie_Plugin_Quit
+ * @author    Phergie Development Team <team@phergie.org>
+ * @copyright 2008-2010 Phergie Development Team (http://phergie.org)
+ * @license   http://phergie.org/license New BSD License
+ * @link      http://pear.phergie.org/package/Phergie_Plugin_Quit
+ */
+
+/**
+ * Terminates the current connection upon command.
+ *
+ * @category Phergie
+ * @package  Phergie_Plugin_Quit
+ * @author   Phergie Development Team <team@phergie.org>
+ * @license  http://phergie.org/license New BSD License
+ * @link     http://pear.phergie.org/package/Phergie_Plugin_Quit
+ * @uses     Phergie_Plugin_Command pear.phergie.org
+ */
+class Phergie_Plugin_Quit extends Phergie_Plugin_Abstract
+{
+    /**
+     * Checks for dependencies.
+     *
+     * @return void
+     */
+    public function onLoad()
+    {
+        $this->getPluginHandler()->getPlugin('Command');
+    }
+
+    /**
+     * Issues a quit command when a message is received requesting that the
+     * bot terminate the current connection.
+     *
+     * @return void
+     */
+    public function onCommandQuit()
+    {
+        $this->doQuit('Requested by ' . $this->getEvent()->getNick());
+    }
+}
diff --git a/plugins/Irc/extlib/phergie/Phergie/Plugin/Reload.php b/plugins/Irc/extlib/phergie/Phergie/Plugin/Reload.php
new file mode 100755 (executable)
index 0000000..90e3adc
--- /dev/null
@@ -0,0 +1,103 @@
+<?php
+/**
+ * Phergie
+ *
+ * PHP version 5
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.
+ * It is also available through the world-wide-web at this URL:
+ * http://phergie.org/license
+ *
+ * @category  Phergie
+ * @package   Phergie_Plugin_Reload
+ * @author    Phergie Development Team <team@phergie.org>
+ * @copyright 2008-2010 Phergie Development Team (http://phergie.org)
+ * @license   http://phergie.org/license New BSD License
+ * @link      http://pear.phergie.org/package/Phergie_Plugin_Reload
+ */
+
+/**
+ * Facilitates reloading of individual plugins for development purposes.
+ * Note that, because existing class definitions cannot be removed from
+ * memory, increased memory usage is an expected result of using this plugin.
+ *
+ * @category Phergie
+ * @package  Phergie_Plugin_Reload
+ * @author   Phergie Development Team <team@phergie.org>
+ * @license  http://phergie.org/license New BSD License
+ * @link     http://pear.phergie.org/package/Phergie_Plugin_Reload
+ * @uses     Phergie_Plugin_Command pear.phergie.org
+ */
+class Phergie_Plugin_Reload extends Phergie_Plugin_Abstract
+{
+    /**
+     * Checks for dependencies.
+     *
+     * @return void
+     */
+    public function onLoad()
+    {
+        $this->getPluginHandler()->getPlugin('Command');
+    }
+
+    /**
+     * Reloads a specified plugin.
+     *
+     * @param string $plugin Short name of the plugin to reload
+     *
+     * @return void
+     */
+    public function onCommandReload($plugin)
+    {
+        $plugin = ucfirst($plugin);
+
+        if (!$this->plugins->hasPlugin($plugin)) {
+            echo 'DEBUG(Reload): ' . ucfirst($plugin) . ' is not loaded yet, loading', PHP_EOL;
+            $this->plugins->getPlugin($plugin);
+            $this->plugins->command->populateMethodCache();
+            return;
+        }
+
+        try {
+            $info = $this->plugins->getPluginInfo($plugin);
+        } catch (Phergie_Plugin_Exception $e) {
+            $source = $this->event->getSource();
+            $nick = $this->event->getNick();
+            $this->doNotice($source, $nick . ': ' . $e->getMessage());
+            return;
+        }
+
+        $class = $info['class'];
+        $contents = file_get_contents($info['file']);
+        $newClass = $class . '_' . sha1($contents);
+
+        if (class_exists($newClass, false)) {
+            echo 'DEBUG(Reload): Class ', $class, ' has not changed since last reload', PHP_EOL;
+            return;
+        }
+
+        $contents = preg_replace(
+            array('/^<\?(?:php)?/', '/class\s+' . $class . '/i'),
+            array('', 'class ' . $newClass),
+            $contents
+        );
+        eval($contents);
+
+        $instance = new $newClass;
+        $instance->setName($plugin);
+        $instance->setEvent($this->event);
+        $this->plugins
+            ->removePlugin($plugin)
+            ->addPlugin($instance);
+
+        $this->plugins->command->populateMethodCache();
+        if ($this->plugins->hasPlugin('Help')) {
+            $this->plugins->help->populateRegistry();
+        }
+
+        echo 'DEBUG(Reload): Reloaded ', $class, ' to ', $newClass, PHP_EOL;
+    }
+}
diff --git a/plugins/Irc/extlib/phergie/Phergie/Plugin/Remind.php b/plugins/Irc/extlib/phergie/Phergie/Plugin/Remind.php
new file mode 100644 (file)
index 0000000..42d674c
--- /dev/null
@@ -0,0 +1,378 @@
+<?php
+/**
+ * Phergie
+ *
+ * PHP version 5
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.
+ * It is also available through the world-wide-web at this URL:
+ * http://phergie.org/license
+ *
+ * @category  Phergie
+ * @package   Phergie_Plugin_Remind
+ * @author    Phergie Development Team <team@phergie.org>
+ * @copyright 2008-2010 Phergie Development Team (http://phergie.org)
+ * @license   http://phergie.org/license New BSD License
+ * @link      http://pear.phergie.org/package/Phergie_Plugin_Remind
+ */
+
+/**
+ * Parses and logs messages that should be relayed to other users the next time
+ * the recipient is active on the same channel.
+ *
+ * @category Phergie
+ * @package  Phergie_Plugin_Remind
+ * @author   Phergie Development Team <team@phergie.org>
+ * @license  http://phergie.org/license New BSD License
+ * @link     http://pear.phergie.org/package/Phergie_Plugin_Remind
+ * @uses     Phergie_Plugin_Command pear.phergie.org
+ * @uses     Phergie_Plugin_Time pear.phergie.org
+ * @uses     extension PDO
+ * @uses     extension pdo_sqlite
+ */
+class Phergie_Plugin_Remind extends Phergie_Plugin_Abstract
+{
+    /**
+     * Number of reminders to show in public.
+     */
+    protected $publicReminders = 3;
+
+    /**
+     * PDO resource for a SQLite database containing the reminders.
+     *
+     * @var resource
+     */
+    protected $db;
+
+    /**
+     * Flag that indicates whether or not to use an in-memory reminder list.
+     *
+     * @var bool
+     */
+    protected $keepListInMemory = true;
+
+    /**
+     * In-memory store for pending reminders.
+     */
+    protected $msgStorage = array();
+
+    /**
+     * Check for dependencies.
+     *
+     * @return void
+     */
+    public function onLoad()
+    {
+        $plugins = $this->getPluginHandler();
+        $plugins->getPlugin('Command');
+        $plugins->getPlugin('Time');
+
+        if (!extension_loaded('PDO') || !extension_loaded('pdo_sqlite')) {
+            $this->fail('PDO and pdo_sqlite extensions must be installed');
+        }
+
+        $dir = dirname(__FILE__) . '/' . $this->getName();
+        $path = $dir . '/reminder.db';
+        if (!file_exists($dir)) {
+            mkdir($dir);
+        }
+
+        if (isset($this->config['remind.use_memory'])) {
+            $this->keepListInMemory = (bool) $this->config['remind.use_memory'];
+        }
+
+        if (isset($this->config['remind.public_reminders'])) {
+            $this->publicReminders = (int) $this->config['remind.public_reminders'];
+            $this->publicReminders = max($this->publicReminders, 0);
+        }
+
+        try {
+            $this->db = new PDO('sqlite:' . $path);
+            $this->createTables();
+        } catch (PDO_Exception $e) {
+            throw new Phergie_Plugin_Exception($e->getMessage());
+        }
+    }
+
+    /**
+     * Intercepts a message and processes any contained recognized commands.
+     *
+     * @return void
+     */
+    public function onPrivmsg()
+    {
+        $source = $this->getEvent()->getSource();
+        $nick = $this->getEvent()->getNick();
+
+        $this->deliverReminders($source, $nick);
+    }
+
+    /**
+     * Handle reminder requests
+     *
+     * @param string $recipient recipient of the message
+     * @param string $message   message to tell the recipient
+     *
+     * @return void
+     * @see handleRemind()
+     */
+    public function onCommandTell($recipient, $message)
+    {
+        $this->handleRemind($recipient, $message);
+    }
+
+    /**
+     * Handle reminder requests
+     *
+     * @param string $recipient recipient of the message
+     * @param string $message   message to tell the recipient
+     *
+     * @return void
+     * @see handleRemind()
+     */
+    public function onCommandAsk($recipient, $message)
+    {
+        $this->handleRemind($recipient, $message);
+    }
+
+    /**
+     * Handle reminder requests
+     *
+     * @param string $recipient recipient of the message
+     * @param string $message   message to tell the recipient
+     *
+     * @return void
+     * @see handleRemind()
+     */
+    public function onCommandRemind($recipient, $message)
+    {
+        $this->handleRemind($recipient, $message);
+    }
+
+    /**
+     * Handles the tell/remind command (stores the message)
+     *
+     * @param string $recipient name of the recipient
+     * @param string $message   message to store
+     *
+     * @return void
+     */
+    protected function handleRemind($recipient, $message)
+    {
+        $source = $this->getEvent()->getSource();
+        $nick = $this->getEvent()->getNick();
+
+        if (!$this->getEvent()->isInChannel()) {
+            $this->doPrivmsg($source, 'Reminders must be requested in-channel.');
+            return;
+        }
+
+        $q = $this->db->prepare(
+            'INSERT INTO remind
+                (
+                    time,
+                    channel,
+                    recipient,
+                    sender,
+                    message
+                )
+            VALUES
+                (
+                    :time,
+                    :channel,
+                    :recipient,
+                    :sender,
+                    :message
+               )'
+        );
+        try {
+            $q->execute(
+                array(
+                    'time' => date(DATE_RFC822),
+                    'channel' => $source,
+                    'recipient' => strtolower($recipient),
+                    'sender' => strtolower($nick),
+                    'message' => $message
+                )
+            );
+        } catch (PDOException $e) {
+        }
+
+        if ($rowid = $this->db->lastInsertId()) {
+            $this->doPrivmsg($source, 'ok, ' . $nick . ', message stored');
+        } else {
+            $this->doPrivmsg(
+                $source,
+                $nick . ': bad things happened. Message not saved.'
+            );
+            return;
+        }
+
+        if ($this->keepListInMemory) {
+            $this->msgStorage[$source][strtolower($recipient)] = $rowid;
+        }
+    }
+
+    /**
+     * Determines if the user has pending reminders, and if so, delivers them.
+     *
+     * @param string $channel channel to check
+     * @param string $nick    nick to check
+     *
+     * @return void
+     */
+    protected function deliverReminders($channel, $nick)
+    {
+        if ($channel[0] != '#') {
+            // private message, not a channel, so don't check
+            return;
+        }
+
+        // short circuit if there's no message in memory (if allowed)
+        if ($this->keepListInMemory
+            && !isset($this->msgStorage[$channel][strtolower($nick)])
+        ) {
+            return;
+        }
+
+        // fetch and deliver messages
+        $reminders = $this->fetchMessages($channel, $nick);
+        if (count($reminders) > $this->publicReminders) {
+            $msgs = array_slice($reminders, 0, $this->publicReminders);
+            $privmsgs = array_slice($reminders, $this->publicReminders);
+        } else {
+            $msgs = $reminders;
+            $privmsgs = false;
+        }
+
+        foreach ($msgs as $msg) {
+            $ts = $this->plugins->time->getCountdown($msg['time']);
+            $formatted = sprintf(
+                '%s: (from %s, %s ago) %s',
+                $nick, $msg['sender'], $ts, $msg['message']
+            );
+            $this->doPrivmsg($channel, $formatted);
+            $this->deleteMessage($msg['rowid'], $channel, $nick);
+        }
+
+        if ($privmsgs) {
+            foreach ($privmsgs as $msg) {
+                $ts = $this->plugins->time->getCountdown($msg['time']);
+                $formatted = sprintf(
+                    'from %s, %s ago: %s',
+                    $msg['sender'], $ts, $msg['message']
+                );
+                $this->doPrivmsg($nick, $formatted);
+                $this->deleteMessage($msg['rowid'], $channel, $nick);
+            }
+            $formatted = sprintf(
+                '%s: (%d more messages sent in private.)',
+                $nick, count($privmsgs)
+            );
+            $this->doPrivmsg($channel, $formatted);
+        }
+    }
+
+    /**
+     * Get pending messages (for a specific channel/recipient)
+     *
+     * @param string $channel   channel on which to check for pending messages
+     * @param string $recipient user for which to check pending messages
+     *
+     * @return array of records
+     */
+    protected function fetchMessages($channel = null, $recipient = null)
+    {
+        if ($channel) {
+            $qClause = 'WHERE channel = :channel AND recipient LIKE :recipient';
+            $params = compact('channel', 'recipient');
+        } else {
+            $qClause = '';
+            $params = array();
+        }
+        $q = $this->db->prepare(
+            'SELECT rowid, channel, sender, recipient, time, message
+            FROM remind ' . $qClause
+        );
+        $q->execute($params);
+        return $q->fetchAll();
+    }
+
+    /**
+     * Deletes a delivered message
+     *
+     * @param int    $rowid   ID of the message to delete
+     * @param string $channel message's channel
+     * @param string $nick    message's recipient
+     *
+     * @return void
+     */
+    protected function deleteMessage($rowid, $channel, $nick)
+    {
+        $nick = strtolower($nick);
+        $q = $this->db->prepare('DELETE FROM remind WHERE rowid = :rowid');
+        $q->execute(array('rowid' => $rowid));
+
+        if ($this->keepListInMemory) {
+            if (isset($this->msgStorage[$channel][$nick])
+                && $this->msgStorage[$channel][$nick] == $rowid
+            ) {
+                unset($this->msgStorage[$channel][$nick]);
+            }
+        }
+    }
+
+    /**
+     * Determines if a table exists
+     *
+     * @param string $name Table name
+     *
+     * @return bool
+     */
+    protected function haveTable($name)
+    {
+        $sql = 'SELECT COUNT(*) FROM sqlite_master WHERE name = '
+            . $this->db->quote($name);
+        return (bool) $this->db->query($sql)->fetchColumn();
+    }
+
+    /**
+     * Creates the database table(s) (if they don't exist)
+     *
+     * @return void
+     */
+    protected function createTables()
+    {
+        if (!$this->haveTable('remind')) {
+            $this->db->exec(
+                'CREATE TABLE
+                    remind
+                    (
+                        time INTEGER,
+                        channel TEXT,
+                        recipient TEXT,
+                        sender TEXT,
+                        message TEXT
+                    )'
+            );
+        }
+    }
+
+    /**
+     * Populates the in-memory cache of pending reminders
+     *
+     * @return void
+     */
+    protected function populateMemory()
+    {
+        if (!$this->keepListInMemory) {
+            return;
+        }
+        foreach ($this->fetchMessages() as $msg) {
+            $this->msgStorage[$msg['channel']][$msg['recipient']] = $msg['rowid'];
+        }
+    }
+}
diff --git a/plugins/Irc/extlib/phergie/Phergie/Plugin/Serve.php b/plugins/Irc/extlib/phergie/Phergie/Plugin/Serve.php
new file mode 100755 (executable)
index 0000000..cdb8f7f
--- /dev/null
@@ -0,0 +1,160 @@
+<?php
+/**
+ * Phergie
+ *
+ * PHP version 5
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.
+ * It is also available through the world-wide-web at this URL:
+ * http://phergie.org/license
+ *
+ * @category  Phergie
+ * @package   Phergie_Plugin_Serve
+ * @author    Phergie Development Team <team@phergie.org>
+ * @copyright 2008-2010 Phergie Development Team (http://phergie.org)
+ * @license   http://phergie.org/license New BSD License
+ * @link      http://pear.phergie.org/package/Phergie_Plugin_Serve
+ */
+
+/**
+ * Processes requests to serve a user something from a database.
+ *
+ * @category Phergie
+ * @package  Phergie_Plugin_Serve
+ * @author   Phergie Development Team <team@phergie.org>
+ * @license  http://phergie.org/license New BSD License
+ * @link     http://pear.phergie.org/package/Phergie_Plugin_Serve
+ * @uses     extension pdo
+ * @uses     extension pdo_sqlite
+ */
+class Phergie_Plugin_Serve extends Phergie_Plugin_Abstract
+{
+    /**
+     * Checks for dependencies.
+     *
+     * @return void
+     */
+    public function onLoad()
+    {
+        if (!extension_loaded('PDO') || !extension_loaded('pdo_sqlite')) {
+            $this->fail('PDO and pdo_sqlite extensions must be installed');
+        }
+    }
+
+    /**
+     * Retrieves a random item from the database table.
+     *
+     * @param string $database Path to the SQLite database file
+     * @param string $table    Name of the database table
+     * @param array  $request  Parsed request
+     *
+     * @return object Retrieved item
+     */
+    protected function getItem($database, $table, array $request)
+    {
+        $db = new PDO('sqlite:' . $database);
+        if (!empty($request['suggestion'])) {
+            $query = 'SELECT * FROM ' . $table . ' WHERE name LIKE ? ORDER BY RANDOM() LIMIT 1';
+            $stmt = $db->prepare($query);
+            $stmt->execute(array('%' . $request['suggestion'] . '%'));
+            $item = $stmt->fetchObject();
+            if (!$item) {
+                $item = new stdClass;
+                $item->name = $request['suggestion'];
+                $item->link = null;
+            }
+        } else {
+            $query = 'SELECT * FROM ' . $table . ' ORDER BY RANDOM() LIMIT 1';
+            $stmt = $db->query($query);
+            $item = $stmt->fetchObject();
+        }
+        return $item;
+    }
+
+    /**
+     * Processes a request to serve a user something.
+     *
+     * @param string $database Path to the SQLite database file
+     * @param string $table    Name of the database table
+     * @param string $format   Format of the response where %target%,
+     *        %item%, %article%', and %link will be replaced with their
+     *        respective data
+     * @param string $request  Request string including the target and an
+     *        optional suggestion of the item to fetch
+     * @param boolean $censor  TRUE to integrate with the Censor plugin,
+     *        defaults to FALSE
+     *
+     * @return boolean TRUE if the request was processed successfully, FALSE
+     *         otherwise
+     */
+    public function serve($database, $table, $format, $request, $censor = false)
+    {
+        // Parse the request
+        $result = preg_match(
+            '/(?P<target>[^\s]+)(\s+an?\s+)?(?P<suggestion>.*)?/',
+            $request,
+            $match
+        );
+
+        if (!$result) {
+            return false;
+        }
+
+        // Resolve the target
+        $target = $match['target'];
+        if ($target == 'me') {
+            $target = $this->event->getNick();
+        }
+
+        // Process the request
+        $item = $this->getItem($database, $table, $match);
+
+        // Reprocess the request for censorship if required
+        if ($this->plugins->hasPlugin('Censor')) {
+            $plugin = $this->plugins->getPlugin('Censor');
+            $attempts = 0;
+            while ($censor && $attempts < 3) {
+                $clean = $plugin->cleanString($item->name);
+                if ($item->name != $clean) {
+                    $attempts++;
+                    $item = $this->getItem($database, $table, $match);
+                } else {
+                    $censor = false;
+                }
+            }
+            if ($censor && $attempts == 3) {
+                $this->doAction($this->event->getSource(), 'shrugs.');
+            }
+        }
+
+        // Derive the proper article for the item
+        if (preg_match('/^[aeiou]/i', $item->name)) {
+            $article = 'an';
+        } else {
+            $article = 'a';
+        }
+
+        // Format the message
+        $replacements = array(
+            'target' => $target,
+            'item' => $item->name,
+            'link' => $item->link,
+            'article' => $article
+        );
+
+        $msg = $format;
+        foreach ($replacements as $placeholder => $value) {
+            $msg = str_replace(
+                '%' . $placeholder . '%',
+                $value,
+                $msg
+            );
+        }
+
+        // Send the message
+        $this->doAction($this->event->getSource(), $msg);
+    }
+}
diff --git a/plugins/Irc/extlib/phergie/Phergie/Plugin/SpellCheck.php b/plugins/Irc/extlib/phergie/Phergie/Plugin/SpellCheck.php
new file mode 100644 (file)
index 0000000..3324082
--- /dev/null
@@ -0,0 +1,120 @@
+<?php
+/**
+ * Phergie 
+ *
+ * PHP version 5
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.
+ * It is also available through the world-wide-web at this URL:
+ * http://phergie.org/license
+ *
+ * @category  Phergie 
+ * @package   Phergie_Plugin_TerryChay
+ * @author    Phergie Development Team <team@phergie.org>
+ * @copyright 2008-2010 Phergie Development Team (http://phergie.org)
+ * @license   http://phergie.org/license New BSD License
+ * @link      http://pear.phergie.org/package/Phergie_Plugin_TerryChay
+ */
+
+/**
+ * Handles requests for checking spelling of specified words and returning
+ * either confirmation of correctly spelled words or potential correct
+ * spellings for misspelled words.
+ *
+ * @category Phergie 
+ * @package  Phergie_Plugin_SpellCheck
+ * @author   Phergie Development Team <team@phergie.org>
+ * @license  http://phergie.org/license New BSD License
+ * @link     http://pear.phergie.org/package/Phergie_Plugin_TerryChay
+ * @uses     Phergie_Plugin_Command pear.phergie.org
+ * @uses     extension pspell
+ */
+class Phergie_Plugin_SpellCheck extends Phergie_Plugin_Abstract
+{
+
+    /**
+     * Spell check dictionary handler
+     *
+     * @var resource
+     */
+    protected $pspell;
+
+    /**
+     * Limit on the number of potential correct spellings returned
+     *
+     * @var int
+     */
+    protected $limit;
+
+    /**
+     * Check for dependencies.
+     *
+     * @return void
+     */
+    public function onLoad()
+    {
+        if (!extension_loaded('pspell')) {
+            $this->fail('pspell php extension is required');
+        }
+
+        if (!$this->getConfig('spellcheck.lang')) {
+            $this->fail('Setting spellcheck.lang must be filled-in');
+        }
+
+        $this->plugins->getPlugin('Command');
+       
+        set_error_handler(array($this, 'loadDictionaryError'));
+        $this->pspell = pspell_new($this->getConfig('spellcheck.lang'));
+        restore_error_handler();
+
+        $this->limit = $this->getConfig('spellcheck.limit', 5);
+    }
+
+    /**
+     * Intercepts and handles requests for spell checks.
+     *
+     * @param string $word the string to perform checks against
+     *
+     * @return void
+     */
+    public function onCommandSpell($word)
+    {
+        $source = $this->event->getSource();
+        $target = $this->event->getNick();
+
+        $message  = $target . ': The word "' . $word;
+        $message .= '" seems to be spelt correctly.';
+        if (!pspell_check($this->pspell, $word)) {
+            $suggestions = pspell_suggest($this->pspell, $word);
+           
+            $message  = $target; 
+            $message .= ': I could not find any suggestions for "' . $word . '".';
+            if (!empty($suggestions)) {
+                $suggestions = array_splice($suggestions, 0, $this->limit);
+                $message     = $target . ': Suggestions for "';
+                $message    .= $word . '": ' . implode(', ', $suggestions) . '.';
+            }
+        }
+         
+        $this->doPrivmsg($source, $message);
+    }
+
+    /**
+     * Handle any errors from loading dictionary
+     *
+     * @param integer $errno   Error code
+     * @param string  $errstr  Error message
+     * @param string  $errfile File that errored
+     * @param integer $errline Line where the error happened
+     *
+     * @return void
+     */
+    protected function loadDictionaryError($errno, $errstr, $errfile, $errline)
+    {
+        $this->fail($errstr);
+    }
+
+}
diff --git a/plugins/Irc/extlib/phergie/Phergie/Plugin/Statusnet.php b/plugins/Irc/extlib/phergie/Phergie/Plugin/Statusnet.php
new file mode 100644 (file)
index 0000000..dc2680a
--- /dev/null
@@ -0,0 +1,143 @@
+<?php\r
+/**\r
+ * StatusNet - the distributed open-source microblogging tool\r
+ *\r
+ * This program is free software: you can redistribute it and/or modify\r
+ * it under the terms of the GNU Affero General Public License as published by\r
+ * the Free Software Foundation, either version 3 of the License, or\r
+ * (at your option) any later version.\r
+ *\r
+ * This program is distributed in the hope that it will be useful,\r
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of\r
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\r
+ * GNU Affero General Public License for more details.\r
+ *\r
+ * You should have received a copy of the GNU Affero General Public License\r
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.\r
+ *\r
+ * Talks to the Statusnet IM architecture to enqueue incoming message messages\r
+ * and notify result of nickname registration checks\r
+ *\r
+ * @category  Phergie\r
+ * @package   Phergie_Plugin_Statusnet\r
+ * @author    Luke Fitzgerald <lw.fitzgerald@googlemail.com>\r
+ * @copyright 2010 StatusNet, Inc.\r
+ * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0\r
+ * @link      http://status.net/\r
+ */\r
+\r
+class Phergie_Plugin_Statusnet extends Phergie_Plugin_Abstract {\r
+    /**\r
+    * Message callback details\r
+    *\r
+    * @var array\r
+    */\r
+    protected $messageCallback;\r
+\r
+    /**\r
+    * Registration check callback details\r
+    *\r
+    * @var array\r
+    */\r
+    protected $regCallback;\r
+\r
+    /**\r
+    * Connection established callback details\r
+    *\r
+    * @var array\r
+    */\r
+    protected $connectedCallback;\r
+\r
+    /**\r
+    * Load callback from config\r
+    */\r
+    public function onLoad() {\r
+        $messageCallback = $this->config['statusnet.messagecallback'];\r
+        if (is_callable($messageCallback)) {\r
+            $this->messageCallback = $messageCallback;\r
+        } else {\r
+            $this->messageCallback = NULL;\r
+        }\r
+\r
+        $regCallback = $this->config['statusnet.regcallback'];\r
+        if (is_callable($regCallback)) {\r
+            $this->regCallback = $regCallback;\r
+        } else {\r
+            $this->regCallback = NULL;\r
+        }\r
+\r
+        $connectedCallback = $this->config['statusnet.connectedcallback'];\r
+        if (is_callable($connectedCallback)) {\r
+            $this->connectedCallback = $connectedCallback;\r
+        } else {\r
+            $this->connectedCallback = NULL;\r
+        }\r
+\r
+        $this->unregRegexp = $this->getConfig('statusnet.unregregexp', '/\x02(.*?)\x02 (?:isn\'t|is not) registered/i');\r
+        $this->regRegexp = $this->getConfig('statusnet.regregexp', '/(?:\A|\x02)(\w+?)\x02? (?:\(account|is \w+?\z)/i');\r
+    }\r
+\r
+    /**\r
+     * Passes incoming messages to StatusNet\r
+     *\r
+     * @return void\r
+     */\r
+    public function onPrivmsg() {\r
+        if ($this->messageCallback !== NULL) {\r
+            $event = $this->getEvent();\r
+            $source = $event->getSource();\r
+            $sender = $event->getNick();\r
+            $message = trim($event->getText());\r
+\r
+            if (strpos($source, '#') === 0) {\r
+                $botNick = $this->getConnection()->getNick();\r
+                $nickPos = strpos($message, $botNick);\r
+                $nickLen = strlen($botNick);\r
+                $colonPos = strpos($message, ':', $nickLen);\r
+                $commandStr = trim(substr($message, $colonPos+1));\r
+                if ($nickPos === 0 && $colonPos == $nickLen && !empty($commandStr)) {\r
+                    call_user_func($this->messageCallback, array('source' => $source, 'sender' => $sender, 'message' => $commandStr));\r
+                }\r
+            } else {\r
+                call_user_func($this->messageCallback, array('source' => $source, 'sender' => $sender, 'message' => $message));\r
+            }\r
+        }\r
+    }\r
+\r
+    /**\r
+     * Catches the response from NickServ\r
+     *\r
+     * @return void\r
+     */\r
+    public function onNotice() {\r
+        if ($this->regCallback !== NULL) {\r
+            $event = $this->getEvent();\r
+            if ($event->getNick() == 'NickServ') {\r
+                $message = $event->getArgument(1);\r
+                if (preg_match($this->unregRegexp, $message, $groups)) {\r
+                    $screenname = $groups[1];\r
+                    call_user_func($this->regCallback, array('screenname' => $screenname, 'registered' => false));\r
+                } elseif (preg_match($this->regRegexp, $message, $groups)) {\r
+                    $screenname = $groups[1];\r
+                    call_user_func($this->regCallback, array('screenname' => $screenname, 'registered' => true));\r
+                }\r
+            }\r
+        }\r
+    }\r
+\r
+    /**\r
+     * Intercepts the end of the "message of the day" response and tells\r
+     * StatusNet we're connected\r
+     *\r
+     * @return void\r
+     */\r
+    public function onResponse() {\r
+        switch ($this->getEvent()->getCode()) {\r
+        case Phergie_Event_Response::RPL_ENDOFMOTD:\r
+        case Phergie_Event_Response::ERR_NOMOTD:\r
+            if ($this->connectedCallback !== NULL) {\r
+                call_user_func($this->connectedCallback);\r
+            }\r
+        }\r
+    }\r
+}\r
diff --git a/plugins/Irc/extlib/phergie/Phergie/Plugin/Tea.php b/plugins/Irc/extlib/phergie/Phergie/Plugin/Tea.php
new file mode 100644 (file)
index 0000000..c645356
--- /dev/null
@@ -0,0 +1,69 @@
+<?php
+/**
+ * Phergie
+ *
+ * PHP version 5
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.
+ * It is also available through the world-wide-web at this URL:
+ * http://phergie.org/license
+ *
+ * @category  Phergie
+ * @package   Phergie_Plugin_Tea
+ * @author    Phergie Development Team <team@phergie.org>
+ * @copyright 2008-2010 Phergie Development Team (http://phergie.org)
+ * @license   http://phergie.org/license New BSD License
+ * @link      http://pear.phergie.org/package/Phergie_Plugin_Tea
+ */
+
+/**
+ * Processes requests to serve users tea.
+ *
+ * @category Phergie
+ * @package  Phergie_Plugin_Tea
+ * @author   Phergie Development Team <team@phergie.org>
+ * @license  http://phergie.org/license New BSD License
+ * @link     http://pear.phergie.org/package/Phergie_Plugin_Tea
+ * @uses     Phergie_Plugin_Command pear.phergie.org
+ * @uses     Phergie_Plugin_Serve pear.phergie.org
+ */
+class Phergie_Plugin_Tea extends Phergie_Plugin_Abstract
+{
+    /**
+     * Checks for dependencies.
+     *
+     * @return void
+     */
+    public function onLoad()
+    {
+        $plugins = $this->plugins;
+        $plugins->getPlugin('Command');
+        $plugins->getPlugin('Serve');
+    }
+
+    /**
+     * Processes requests to serve a user tea.
+     *
+     * @param string $request Request including the target and an optional
+     *        suggestion of what tea to serve
+     *
+     * @return void
+     */
+    public function onCommandTea($request)
+    {
+        $format = $this->getConfig(
+            'tea.format',
+            'serves %target% a cup of %item% tea.'
+        );
+
+        $this->plugins->getPlugin('Serve')->serve(
+            dirname(__FILE__) . '/Tea/tea.db',
+            'tea',
+            $format,
+            $request
+        );
+    }
+}
diff --git a/plugins/Irc/extlib/phergie/Phergie/Plugin/Tea/db.php b/plugins/Irc/extlib/phergie/Phergie/Plugin/Tea/db.php
new file mode 100644 (file)
index 0000000..21fe195
--- /dev/null
@@ -0,0 +1,49 @@
+<?php
+
+if (!defined('__DIR__')) {
+    define('__DIR__', dirname(__FILE__));
+}
+
+// Create database schema
+echo 'Creating database', PHP_EOL;
+$file = __DIR__ . '/tea.db';
+if (file_exists($file)) {
+    unlink($file);
+}
+$db = new PDO('sqlite:' . $file);
+$db->exec('CREATE TABLE tea (name VARCHAR(255), link VARCHAR(255))');
+$db->exec('CREATE UNIQUE INDEX tea_name ON tea (name)');
+$insert = $db->prepare('INSERT INTO tea (name, link) VALUES (:name, :link)');
+
+// Get raw teacuppa.com data set
+echo 'Downloading teacuppa.com data set', PHP_EOL;
+$file = __DIR__ . '/tea-list.html';
+if (!file_exists($file)) {
+    copy('http://www.teacuppa.com/tea-list.asp', $file);
+}
+$contents = file_get_contents($file);
+
+// Extract data from data set
+echo 'Processing teacuppa.com data', PHP_EOL;
+$contents = tidy_repair_string($contents);
+libxml_use_internal_errors(true);
+$doc = new DOMDocument;
+$doc->loadHTML($contents);
+libxml_clear_errors();
+$xpath = new DOMXPath($doc);
+$teas = $xpath->query('//p[@class="page_title"]/following-sibling::table//a');
+$db->beginTransaction();
+foreach ($teas as $tea) {
+    $name = preg_replace(
+        array('/\s*\v+\s*/', '/\s+tea\s*$/i'),
+        array(' ', ''),
+        $tea->textContent
+    );
+    $link = 'http://teacuppa.com/' . $tea->getAttribute('href');
+    $insert->execute(array($name, $link));
+}
+$db->commit();
+
+// Clean up
+echo 'Cleaning up', PHP_EOL;
+unlink($file);
diff --git a/plugins/Irc/extlib/phergie/Phergie/Plugin/Temperature.php b/plugins/Irc/extlib/phergie/Phergie/Plugin/Temperature.php
new file mode 100644 (file)
index 0000000..541fd85
--- /dev/null
@@ -0,0 +1,81 @@
+<?php
+/**
+ * Phergie
+ *
+ * PHP version 5
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.
+ * It is also available through the world-wide-web at this URL:
+ * http://phergie.org/license
+ *
+ * @category  Phergie
+ * @package   Phergie_Plugin_Temperature
+ * @author    Phergie Development Team <team@phergie.org>
+ * @copyright 2008-2010 Phergie Development Team (http://phergie.org)
+ * @license   http://phergie.org/license New BSD License
+ * @link      http://pear.phergie.org/package/Phergie_Plugin_Temperature
+ */
+
+/**
+ * Performs temperature calculations for other plugins.
+ *
+ * @category Phergie
+ * @package  Phergie_Plugin_Temperature
+ * @author   Phergie Development Team <team@phergie.org>
+ * @license  http://phergie.org/license New BSD License
+ * @link     http://pear.phergie.org/package/Phergie_Plugin_Temperature
+ */
+class Phergie_Plugin_Temperature extends Phergie_Plugin_Abstract
+{
+    /**
+     * Converts a temperature in Celsius to Fahrenheit.
+     *
+     * @param int $temp Temperature in Celsius
+     *
+     * @return int Temperature converted to Fahrenheit
+     */
+    public function convertCelsiusToFahrenheit($temp)
+    {
+        return round(((((int) $temp * 9) / 5) + 32));
+    }
+
+    /**
+     * Converts a temperature in Fahrenheit to Celsius.
+     *
+     * @param int $temp Temperature in Fahrenheit
+     *
+     * @return int Temperature converted to Celsius
+     */
+    public function convertFahrenheitToCelsius($temp)
+    {
+        return round(((((int) $temp - 32) * 5) / 9));
+    }
+
+    /**
+     * Calculates the heat index (i.e. "feels like" temperature) based on
+     * temperature and relative humidity.
+     *
+     * @param int $temperature Temperature in degrees Fahrenheit
+     * @param int $humidity Relative humidity (ex: 68)
+     * @return int Heat index in degrees Fahrenheit
+     */
+    public function getHeatIndex($temperature, $humidity)
+    {
+        $temperature2 = $temperature * $temperature;
+        $humidity2 = $humidity * $humidity;
+        return round(
+            -42.379 +
+            (2.04901523 * $temperature) +
+            (10.14333127 * $humidity) -
+            (0.22475541 * $temperature * $humidity) -
+            (0.00683783 * $temperature2) -
+            (0.05481717 * $humidity2) +
+            (0.00122874 * $temperature2 * $humidity) +
+            (0.00085282 * $temperature * $humidity2) -
+            (0.00000199 * $temperature2 * $humidity2)
+        );
+    }
+}
diff --git a/plugins/Irc/extlib/phergie/Phergie/Plugin/TerryChay.php b/plugins/Irc/extlib/phergie/Phergie/Plugin/TerryChay.php
new file mode 100644 (file)
index 0000000..611dfc9
--- /dev/null
@@ -0,0 +1,109 @@
+<?php
+/**
+ * Phergie 
+ *
+ * PHP version 5
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.
+ * It is also available through the world-wide-web at this URL:
+ * http://phergie.org/license
+ *
+ * @category  Phergie 
+ * @package   Phergie_Plugin_TerryChay
+ * @author    Phergie Development Team <team@phergie.org>
+ * @copyright 2008-2010 Phergie Development Team (http://phergie.org)
+ * @license   http://phergie.org/license New BSD License
+ * @link      http://pear.phergie.org/package/Phergie_Plugin_TerryChay
+ */
+
+/**
+ * Parses incoming messages for the words "Terry Chay" or tychay and responds
+ * with a random Terry fact retrieved from the Chayism web service. 
+ *
+ * @category Phergie 
+ * @package  Phergie_Plugin_TerryChay
+ * @author   Phergie Development Team <team@phergie.org>
+ * @license  http://phergie.org/license New BSD License
+ * @link     http://pear.phergie.org/package/Phergie_Plugin_TerryChay
+ * @uses     Phergie_Plugin_Http pear.phergie.org
+ */
+class Phergie_Plugin_TerryChay extends Phergie_Plugin_Abstract
+{
+    /**
+     * URL to the web service
+     *
+     * @const string
+     */
+    const URL = 'http://phpdoc.info/chayism/';
+
+    /**
+     * HTTP plugin
+     *
+     * @var Phergie_Plugin_Http
+     */
+    protected $http;
+
+    /**
+     * Checks for dependencies.
+     *
+     * @return void
+     */
+    public function onLoad()
+    {
+        $this->http = $this->getPluginHandler()->getPlugin('Http');
+    }
+
+    /**
+     * Fetches a chayism.
+     *
+     * @return string|bool Fetched chayism or FALSE if the operation failed 
+     */
+    public function getChayism()
+    {
+        return $this->http->get(self::URL)->getContent();
+    }
+
+    /**
+     * Parses incoming messages for "Terry Chay" and related variations and 
+     * responds with a chayism.
+     *
+     * @return void
+     */
+    public function onPrivmsg()
+    {
+        $event = $this->getEvent();
+        $source = $event->getSource();
+        $message = $event->getText();
+        $pattern 
+            = '{^(' . preg_quote($this->getConfig('command.prefix')) . 
+            '\s*)?.*(terry\s+chay|tychay)}ix';
+
+        if (preg_match($pattern, $message)
+            && $fact = $this->getChayism()
+        ) {
+            $this->doPrivmsg($source, 'Fact: ' . $fact);
+        }
+    }
+
+    /**
+     * Parses incoming CTCP request for "Terry Chay" and related variations 
+     * and responds with a chayism.
+     *
+     * @return void
+     */
+    public function onCtcp()
+    {
+        $event = $this->getEvent();
+        $source = $event->getSource();
+        $ctcp = $event->getArgument(1);
+
+        if (preg_match('({terry[\s_+-]*chay}|tychay)ix', $ctcp)
+            && $fact = $this->getChayism()
+        ) {
+            $this->doCtcpReply($source, 'TERRYCHAY', $fact);
+        }
+    }
+}
diff --git a/plugins/Irc/extlib/phergie/Phergie/Plugin/TheFuckingWeather.php b/plugins/Irc/extlib/phergie/Phergie/Plugin/TheFuckingWeather.php
new file mode 100644 (file)
index 0000000..8559426
--- /dev/null
@@ -0,0 +1,144 @@
+<?php
+/**
+ * Phergie
+ *
+ * PHP version 5
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.
+ * It is also available through the world-wide-web at this URL:
+ * http://phergie.org/license
+ *
+ * @category  Phergie
+ * @package   Phergie_Plugin_TheFuckingWeather
+ * @author    Phergie Development Team <team@phergie.org>
+ * @copyright 2008-2010 Phergie Development Team (http://phergie.org)
+ * @license   http://phergie.org/license New BSD License
+ * @link      http://pear.phergie.org/package/Phergie_Plugin_TheFuckingWeather
+ */
+
+/**
+ * Detects and responds to requests for current weather conditions in a
+ * particular location using data from a web service.
+ *
+ * @category Phergie
+ * @package  Phergie_Plugin_TheFuckingWeather
+ * @author   Phergie Development Team <team@phergie.org>
+ * @license  http://phergie.org/license New BSD License
+ * @link     http://pear.phergie.org/package/Phergie_Plugin_TheFuckingWeather
+ * @link     http://thefuckingweather.com
+ * @uses     Phergie_Plugin_Command pear.phergie.org
+ * @uses     Phergie_Plugin_Http pear.phergie.org
+ */
+
+class Phergie_Plugin_TheFuckingWeather extends Phergie_Plugin_Abstract
+{
+    /**
+     * HTTP plugin
+     *
+     * @var Phergie_Plugin_Http
+     */
+    protected $http = null;
+
+    /**
+     * Base API URL
+     *
+     * @var string
+     */
+    protected $url = 'http://www.thefuckingweather.com/?zipcode=';
+
+    /**
+     * Checks for dependencies.
+     *
+     * @return void
+     */
+    public function onLoad()
+    {
+        $pluginHandler = $this->getPluginHandler();
+        $pluginHandler->getPlugin('Command');
+        $this->http = $pluginHandler->getPlugin('Http');
+    }
+
+    /**
+     * Returns the weather from the specified location.
+     *
+     * @param string $location Location term
+     *
+     * @return void
+     * @todo Implement use of URL shortening here
+     */
+    public function onCommandThefuckingweather($location)
+    {
+        $source = $this->getEvent()->getSource();
+        $target = $this->getEvent()->getNick();
+        $out = $this->getWeather($location);
+        if (!$out) {
+            $this->doNotice($source, $out);
+        } else {
+            $this->doPrivmsg($source, $target . ': ' . $out);
+        }
+    }
+
+    /**
+    * Alias for TheFuckingWeather command.
+    *
+    * @param string $location Location term
+    *
+    * @return void
+    */
+    public function onCommandTfw($location)
+    {
+        $this->onCommandThefuckingweather($location);
+    }
+
+    /**
+     * Get the necessary content and returns the search result.
+     *
+     * @param string $location Location term
+     *
+     * @return string|bool Search result or FALSE if none is found
+     * @todo Try to optimize pregs
+     */
+    protected function getWeather($location)
+    {
+        $url = $this->url . urlencode($location);
+        $response = $this->http->get($url);
+        $content = $response->getContent();
+
+        preg_match_all(
+            '#<div><span class="small">(.*?)<\/span><\/div>#im',
+            $content, $matches
+        );
+        $location = $matches[1][0];
+
+        if (!empty($location)) {
+            preg_match_all(
+                '#<div class="large" >(.*?)<br \/>#im',
+                $content, $matches
+            );
+            $temp_numb = (int) $matches[1][0];
+            $temp_numb .= ' F / ' . round(($temp_numb - 32) / 1.8, 0) . ' C?!';
+
+            preg_match_all(
+                '#<br \/>(.*?)<\/div><div  id="remark"><br \/>#im',
+                $content, $matches
+            );
+            $temp_desc = $matches[1][0];
+
+            preg_match_all(
+                '#<div  id="remark"><br \/>\n<span>(.*?)<\/span><\/div>#im',
+                $content, $matches
+            );
+            $remark = $matches[1][0];
+
+            $result = "{$location}: {$temp_numb} {$temp_desc} ({$remark})";
+            $result = preg_replace('/</', ' <', $result);
+            $result = strip_tags($result);
+            return html_entity_decode($result);
+        } else {
+            return 'No fucking clue where that is.';
+        }
+    }
+}
diff --git a/plugins/Irc/extlib/phergie/Phergie/Plugin/Time.php b/plugins/Irc/extlib/phergie/Phergie/Plugin/Time.php
new file mode 100644 (file)
index 0000000..0d90bd8
--- /dev/null
@@ -0,0 +1,72 @@
+<?php
+/**
+ * Phergie 
+ *
+ * PHP version 5
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.
+ * It is also available through the world-wide-web at this URL:
+ * http://phergie.org/license
+ *
+ * @category  Phergie 
+ * @package   Phergie_Plugin_Time
+ * @author    Phergie Development Team <team@phergie.org>
+ * @copyright 2008-2010 Phergie Development Team (http://phergie.org)
+ * @license   http://phergie.org/license New BSD License
+ * @link      http://pear.phergie.org/package/Phergie_Plugin_Time
+ */
+
+/**
+ * Helper plugin to assist other plugins with time manipulation, display.
+ *
+ * Any shared time-related code should go into this class.
+ *
+ * @category Phergie
+ * @package  Phergie_Plugin_Time
+ * @author   Phergie Development Team <team@phergie.org>
+ * @license  http://phergie.org/license New BSD License
+ * @link     http://pear.phergie.org/package/Phergie_Plugin_Time
+ */
+class Phergie_Plugin_Time extends Phergie_Plugin_Abstract 
+{
+    /**
+     * Returns the time interval between the current time and a given 
+     * timestamp. 
+     *
+     * @param string $timestamp Timestamp compatible with strtotime()
+     *
+     * @return string
+     */
+    public function getCountdown($timestamp)
+    {
+        $time = time() - strtotime($timestamp); 
+        $return = array();
+
+        $days = floor($time / 86400);
+        if ($days > 0) {
+            $return[] = $days . 'd';
+            $time %= 86400;
+        }
+
+        $hours = floor($time / 3600);
+        if ($hours > 0) {
+            $return[] = $hours . 'h';
+            $time %= 3600;
+        }
+
+        $minutes = floor($time / 60);
+        if ($minutes > 0) {
+            $return[] = $minutes . 'm';
+            $time %= 60;
+        }
+
+        if ($time > 0 || count($return) <= 0) {
+            $return[] = ($time > 0 ? $time : '0') . 's';
+        }
+
+        return implode(' ', $return);
+    }
+}
diff --git a/plugins/Irc/extlib/phergie/Phergie/Plugin/Tld.php b/plugins/Irc/extlib/phergie/Phergie/Plugin/Tld.php
new file mode 100644 (file)
index 0000000..d7d64a4
--- /dev/null
@@ -0,0 +1,148 @@
+<?php
+
+/**
+ * Phergie
+ *
+ * PHP version 5
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.
+ * It is also available through the world-wide-web at this URL:
+ * http://phergie.org/license
+ *
+ * @category  Phergie
+ * @package   Phergie_Plugin_Url
+ * @author    Phergie Development Team <team@phergie.org>
+ * @copyright 2008-2010 Phergie Development Team (http://phergie.org)
+ * @license   http://phergie.org/license New BSD License
+ * @link      http://pear.phergie.org/package/Phergie_Plugin_Url
+ */
+
+/**
+ * Responds to a request for a TLD (formatted as .tld where tld is the TLD to
+ * be looked up) with its corresponding description.
+ *
+ * @category Phergie
+ * @package  Phergie_Plugin_Tld
+ * @author   Phergie Development Team <team@phergie.org>
+ * @license  http://phergie.org/license New BSD License
+ * @link     http://pear.phergie.org/package/Phergie_Plugin_Tld
+ * @uses     extension PDO
+ * @uses     extension pdo_sqlite
+ */
+class Phergie_Plugin_Tld extends Phergie_Plugin_Abstract
+{
+    /**
+     * Connection to the database
+     *
+     * @var PDO
+     */
+    protected $db;
+
+    /**
+     * Prepared statement for selecting a single TLD
+     *
+     * @var PDOStatement
+     */
+    protected $select;
+
+    /**
+     * Prepared statement for selecting all TLDs
+     *
+     * @var PDOStatement
+     */
+    protected $selectAll;
+
+    /**
+     * Checks for dependencies and sets up the database and hard-coded values.
+     *
+     * @return void
+     */
+    public function onLoad()
+    {
+        if (!extension_loaded('PDO') || !extension_loaded('pdo_sqlite')) {
+            $this->fail('PDO and pdo_sqlite extensions must be installed');
+        }
+
+        $dbFile = dirname(__FILE__) . '/Tld/tld.db';
+        try {
+            $this->db = new PDO('sqlite:' . $dbFile);
+
+            $this->select = $this->db->prepare('
+                SELECT type, description
+                FROM tld
+                WHERE LOWER(tld) = LOWER(:tld)
+            ');
+
+            $this->selectAll = $this->db->prepare('
+                SELECT tld, type, description
+                FROM btld
+            ');
+        } catch (PDOException $e) {
+            $this->getPluginHandler()->removePlugin($this);
+        }
+    }
+
+    /**
+     * takes a tld in the format '.tld' and returns its related data
+     *
+     * @param string $tld tld to process
+     *
+     * @return null
+     */
+    public function onCommandTld($tld)
+    {
+        $tld = ltrim($tld, '.');
+        $description = $this->getTld($tld);
+        $this->doPrivmsg(
+            $this->event->getSource(),
+            "{$this->getEvent()->getNick()}: .{$tld} -> "
+            . ($description ? $description : 'Unknown TLD')
+        );
+    }
+
+    /**
+     * Retrieves the definition for a given TLD if it exists
+     *
+     * @param string $tld TLD to search for
+     *
+     * @return mixed Definition of the given TLD as a string or false if unknown
+     */
+    public function getTld($tld)
+    {
+        $tld = trim(strtolower($tld));
+        if ($this->select->execute(array('tld' => $tld))) {
+            $tlds = $this->select->fetch();
+            if (is_array($tlds)) {
+                return '(' . $tlds['type'] . ') ' . $tlds['description'];
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Retrieves a list of all the TLDs and their definitions
+     *
+     * @return mixed Array of all the TLDs and their definitions or FALSE on
+      *        failure
+     */
+    public function getTlds()
+    {
+        if ($this->selectAll->execute()) {
+            $tlds = $this->selectAll->fetchAll();
+            if (is_array($tlds)) {
+                $tldinfo = array();
+                foreach ($tlds as $key => $tld) {
+                    if (!empty($tld['tld'])) {
+                        $tldinfo[$tld['tld']] = "({$tld['type']}) "
+                        . $tld['description'];
+                    }
+                }
+                return $tldinfo;
+            }
+        }
+        return false;
+    }
+}
diff --git a/plugins/Irc/extlib/phergie/Phergie/Plugin/Tld/db.php b/plugins/Irc/extlib/phergie/Phergie/Plugin/Tld/db.php
new file mode 100644 (file)
index 0000000..28f963a
--- /dev/null
@@ -0,0 +1,68 @@
+<?php
+
+$dbFile = 'tld.db';
+
+if (file_exists($dbFile)) {
+    exit;
+}
+
+$db = new PDO('sqlite:' . dirname(__FILE__) . '/' . $dbFile);
+
+$query = '
+    CREATE TABLE tld (
+        tld VARCHAR(20),
+        type VARCHAR(20),
+        description VARCHAR(255)
+    )
+';
+$db->exec($query);
+
+$insert = $db->prepare('
+    INSERT INTO tld (tld, type, description)
+    VALUES (:tld, :type, :description)
+');
+
+$contents = file_get_contents(
+    'http://www.iana.org/domains/root/db/'
+);
+
+libxml_use_internal_errors(true);
+$doc = new DOMDocument;
+$doc->loadHTML($contents);
+libxml_clear_errors();
+
+$descriptions = array(
+    'com' => 'Commercial',
+    'info' => 'Information',
+    'net' => 'Network',
+    'org' => 'Organization',
+    'edu' => 'Educational',
+    'name' => 'Individuals, by name'
+);
+
+$xpath = new DOMXPath($doc);
+$rows = $xpath->query('//tr[contains(@class, "iana-group")]');
+foreach (range(0, $rows->length - 1) as $index) {
+    $row = $rows->item($index);
+    $tld = strtolower(ltrim($row->childNodes->item(0)->textContent, '.'));
+    $type = $row->childNodes->item(1)->nodeValue;
+    if (isset($descriptions[$tld])) {
+        $description = $descriptions[$tld];
+    } else {
+        $description = $row->childNodes->item(2)->textContent;
+        $regex = '{(^(?:Reserved|Restricted)\s*(?:exclusively\s*)?'
+         . '(?:for|to)\s*(?:members of\s*)?(?:the|support)?'
+         . '\s*|\s*as advised.*$)}i';
+        $description = preg_replace($regex, '', $description);
+        $description = ucfirst(trim($description));
+    }
+    $data = array_map(
+        'html_entity_decode',
+        array(
+            'tld' => $tld,
+            'type' => $type,
+            'description' => $description
+        )
+    );
+    $insert->execute($data);
+}
diff --git a/plugins/Irc/extlib/phergie/Phergie/Plugin/Twitter.php b/plugins/Irc/extlib/phergie/Phergie/Plugin/Twitter.php
new file mode 100644 (file)
index 0000000..4a77d1e
--- /dev/null
@@ -0,0 +1,206 @@
+<?php
+/**
+ * Phergie
+ *
+ * PHP version 5
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.
+ * It is also available through the world-wide-web at this URL:
+ * http://phergie.org/license
+ *
+ * @category  Phergie
+ * @package   Phergie_Plugin_Twitter
+ * @author    Phergie Development Team <team@phergie.org>
+ * @copyright 2008-2010 Phergie Development Team (http://phergie.org)
+ * @license   http://phergie.org/license New BSD License
+ * @link      http://pear.phergie.org/package/Phergie_Plugin_Twitter
+ */
+
+/**
+ * These requires are for library code, so they don't fit Autoload's normal
+ * conventions.
+ *
+ * @link http://github.com/scoates/simpletweet
+ */
+require dirname(__FILE__) . '/Twitter/twitter.class.php';
+require dirname(__FILE__) . '/Twitter/laconica.class.php';
+
+/**
+ * Twitter plugin; Allows tweet (if configured) and twitter commands
+ *
+ * Usage:
+ *   tweet text to tweet
+ *    (sends a message to twitter and Phergie will give you the link)
+ *   twitter username
+ *    (fetches and displays the last tweet by @username)
+ *   twitter username 3
+ *    (fetches and displays the third last tweet by @username)
+ *   twitter 1234567
+ *    (fetches and displays tweet number 1234567)
+ *   http://twitter.com/username/statuses/1234567
+ *    (same as `twitter 1234567`)
+ *
+ * @category Phergie
+ * @package  Phergie_Plugin_Twitter
+ * @author   Phergie Development Team <team@phergie.org>
+ * @license  http://phergie.org/license New BSD License
+ * @link     http://pear.phergie.org/package/Phergie_Plugin_Twitter
+ * @uses     Phergie_Plugin_Time pear.phergie.org
+ */
+class Phergie_Plugin_Twitter extends Phergie_Plugin_Abstract
+{
+    /**
+     * Twitter object (from Simpletweet)
+     */
+    protected $twitter;
+
+    /**
+     * Twitter user
+     */
+    protected $twitteruser = null;
+
+    /**
+     * Password
+     */
+    protected $twitterpassword = null;
+
+    /**
+     * Register with the URL plugin, if possible
+     *
+     * @return void
+     */
+    public function onConnect()
+    {
+        if ($url = $this->getPluginHandler()->getPlugin('Url')) {
+            $url->registerRenderer($this);
+        }
+    }
+
+    /**
+     * Initialize (set up configuration vars)
+     *
+     * @return void
+     */
+    public function onLoad()
+    {
+        if (!isset($this->config['twitter.class'])
+            || !$twitterClass = $this->config['twitter.class']
+        ) {
+            $twitterClass = 'Twitter';
+        }
+
+        $this->twitteruser = $this->config['twitter.user'];
+        $this->twitterpassword = $this->config['twitter.password'];
+        $url = $this->config['twitter.url'];
+
+        $this->twitter = new $twitterClass(
+            $this->twitteruser,
+            $this->twitterpassword,
+            $url
+        );
+
+    }
+
+    /**
+     * Fetches the associated tweet and relays it to the channel
+     *
+     * @param string $tweeter if numeric the tweet number/id, otherwise the
+     *  twitter user name (optionally prefixed with @)
+     * @param int    $num     optional tweet number for this user (number of
+     *  tweets ago)
+     *
+     * @return void
+     */
+    public function onCommandTwitter($tweeter = null, $num = 1)
+    {
+        $source = $this->getEvent()->getSource();
+        if (is_numeric($tweeter)) {
+            $tweet = $this->twitter->getTweetByNum($tweeter);
+        } else if (is_null($tweeter) && $this->twitteruser) {
+            $tweet = $this->twitter->getLastTweet($this->twitteruser, 1);
+        } else {
+            $tweet = $this->twitter->getLastTweet(ltrim($tweeter, '@'), $num);
+        }
+        if ($tweet) {
+            $this->doPrivmsg($source, $this->formatTweet($tweet));
+        }
+    }
+
+    /**
+     * Sends a tweet to Twitter as the configured user
+     *
+     * @param string $txt the text to tweet
+     *
+     * @return void
+     */
+    public function onCommandTweet($txt)
+    {
+        $nick = $this->getEvent()->getNick();
+        if (!$this->twitteruser) {
+            return;
+        }
+        $source = $this->getEvent()->getSource();
+        if ($tweet = $this->twitter->sendTweet($txt)) {
+            $this->doPrivmsg(
+                $source, 'Tweeted: '
+                . $this->twitter->getUrlOutputStatus($tweet)
+            );
+        } else {
+            $this->doNotice($nick, 'Tweet failed');
+        }
+    }
+
+    /**
+     * Formats a Tweet into a message suitable for output
+     *
+     * @param object $tweet      JSON-decoded tweet object from Twitter
+     * @param bool   $includeUrl whether or not to include the URL in the
+     *  formatted output
+     *
+     * @return string
+     */
+    protected function formatTweet(StdClass $tweet, $includeUrl = true)
+    {
+        $ts = $this->plugins->time->getCountDown($tweet->created_at);
+        $out =  '<@' . $tweet->user->screen_name .'> '. $tweet->text
+            . ' - ' . $ts . ' ago';
+        if ($includeUrl) {
+            $out .= ' (' . $this->twitter->getUrlOutputStatus($tweet) . ')';
+        }
+        return $out;
+    }
+
+    /**
+     * Renders a URL
+     *
+     * @param array $parsed parse_url() output for the URL to render
+     *
+     * @return bool
+     */
+    public function renderUrl(array $parsed)
+    {
+        if ($parsed['host'] != 'twitter.com'
+            && $parsed['host'] != 'www.twitter.com'
+        ) {
+            // unable to render non-twitter URLs
+            return false;
+        }
+
+        $source = $this->getEvent()->getSource();
+
+        if (preg_match('#^/(.*?)/status(es)?/([0-9]+)$#', $parsed['path'], $matches)
+        ) {
+            $tweet = $this->twitter->getTweetByNum($matches[3]);
+            if ($tweet) {
+                $this->doPrivmsg($source, $this->formatTweet($tweet, false));
+            }
+            return true;
+        }
+
+        // if we get this far, we haven't satisfied the URL, so bail:
+        return false;
+    }
+}
diff --git a/plugins/Irc/extlib/phergie/Phergie/Plugin/Twitter/laconica.class.php b/plugins/Irc/extlib/phergie/Phergie/Plugin/Twitter/laconica.class.php
new file mode 100644 (file)
index 0000000..e411991
--- /dev/null
@@ -0,0 +1,41 @@
+<?php
+/**
+ * Sean's Simple Twitter Library - Laconica extension
+ *
+ * Copyright 2008, Sean Coates
+ * Usage of the works is permitted provided that this instrument is retained
+ * with the works, so that any entity that uses the works is notified of this
+ * instrument.
+ * DISCLAIMER: THE WORKS ARE WITHOUT WARRANTY.
+ * ( Fair License - http://www.opensource.org/licenses/fair.php )
+ * Short license: do whatever you like with this.
+ * 
+ */
+class Twitter_Laconica extends Twitter {
+
+    /**
+     * Constructor; sets up configuration.
+     * 
+     * @param string $user Laconica user name; null for limited read-only access
+     * @param string $pass Laconica password; null for limited read-only access
+     * @param string $baseUrl Base URL of Laconica install. Defaults to identi.ca
+     */
+    public function __construct($user=null, $pass=null, $baseUrl = 'http://identi.ca/') {
+        $this->baseUrl = $baseUrl;
+        parent::__construct($user, $pass);
+    }
+    
+    /**
+     * Returns the base API URL
+     */
+    protected function getUrlApi() {
+        return $this->baseUrlFull . 'api/';
+    }
+    
+    /**
+     * Output URL: status
+     */
+    public function getUrlOutputStatus(StdClass $tweet) {
+        return $this->baseUrl . 'notice/' . urlencode($tweet->id);
+    }
+}
diff --git a/plugins/Irc/extlib/phergie/Phergie/Plugin/Twitter/twitter.class.php b/plugins/Irc/extlib/phergie/Phergie/Plugin/Twitter/twitter.class.php
new file mode 100644 (file)
index 0000000..31173a6
--- /dev/null
@@ -0,0 +1,287 @@
+<?php
+/**
+ * Sean's Simple Twitter Library
+ *
+ * Probably a little more or a little less than you need.
+ *
+ * Copyright 2008, Sean Coates
+ * Usage of the works is permitted provided that this instrument is retained
+ * with the works, so that any entity that uses the works is notified of this
+ * instrument.
+ * DISCLAIMER: THE WORKS ARE WITHOUT WARRANTY.
+ * ( Fair License - http://www.opensource.org/licenses/fair.php )
+ * Short license: do whatever you like with this.
+ *
+ * komode: le=unix language=php codepage=utf8 tab=4 notabs indent=4
+ */
+class Twitter {
+
+    /**
+     * Base URL for Twitter API
+     *
+     * Do not specify user/password in URL
+     */
+    protected $baseUrl = 'http://twitter.com/';
+    
+    /**
+     * Full base URL (includes user/pass)
+     *
+     * (created in Init)
+     */
+    protected $baseUrlFull = null;
+    
+    /**
+     * Twitter API user
+     */
+    protected $user;
+    
+    /**
+     * Twitter API password
+     */
+    protected $pass;
+    
+    /**
+     * Constructor; sets up configuration.
+     * 
+     * @param string $user Twitter user name; null for limited read-only access
+     * @param string $pass Twitter password; null for limited read-only access
+     */
+    public function __construct($user=null, $pass=null) {
+        $this->baseUrlFull = $this->baseUrl;
+        if (null !== $user) {
+            // user is defined, so use it in the URL
+            $this->user = $user;
+            $this->pass = $pass;
+            $parsed = parse_url($this->baseUrl);
+            $this->baseUrlFull = $parsed['scheme'] . '://' . $this->user . ':' .
+                $this->pass . '@' . $parsed['host'];
+            // port (optional)
+            if (isset($parsed['port']) && is_numeric($parsed['port'])) {
+                $this->baseUrlFull .= ':' . $parsed['port'];
+            }
+            // append path (default: /)
+            if (isset($parsed['path'])) {
+                $this->baseUrlFull .= $parsed['path'];
+            } else {
+                $this->baseUrlFull .= '/';
+            }
+        }
+    }
+
+    /**
+     * Fetches a tweet by its number/id
+     *
+     * @param int $num the tweet id/number
+     * @return string (null on failure)
+     */
+    public function getTweetByNum($num) {
+        if (!is_numeric($num)) {
+            return;
+        }
+        $tweet = json_decode(file_get_contents($this->getUrlStatus($num)));
+        return $tweet;
+    }
+
+    /**
+     * Reads [last] tweet from user
+     *
+     * @param string $tweeter the tweeter username
+     * @param int $num this many tweets ago (1 = current tweet)
+     * @return string (false on failure)
+     */
+    public function getLastTweet($tweeter, $num = 1)
+    {
+        $source = json_decode(file_get_contents($this->getUrlUserTimeline($tweeter)));
+        if ($num > count($source)) {
+            return false;
+        }
+        $tweet = $source[$num - 1];
+        if (!isset($tweet->user->screen_name) || !$tweet->user->screen_name) {
+            return false;
+        }
+        return $tweet;
+    }
+    
+    /**
+     * fetches mentions for a user
+     */
+    public function getMentions($sinceId=null, $count=20) {
+        return json_decode(file_get_contents($this->getUrlMentions($sinceId, $count)));
+    }
+    
+    /**
+     * Fetches followers for a user
+     */
+    public function getFollowers($cursor=-1) {
+        return json_decode(file_get_contents($this->getUrlFollowers($cursor)));
+    }
+    
+    /**
+     * Follow a userid
+     */
+    public function follow($userId) {
+        $params = array(
+            'http' => array(
+                'method' => 'POST',
+                'content' => array(),
+                'header' => 'Content-type: application/x-www-form-urlencoded',
+            )
+        );
+        $ctx = stream_context_create($params);
+        $fp = fopen($this->getUrlFollow($userId), 'rb', false, $ctx);
+        if (!$fp) {
+            return false;
+        }
+        $response = stream_get_contents($fp);
+        if ($response === false) {
+            return false;
+        }
+        $response = json_decode($response);
+        return $response;
+    }
+    
+    /**
+     * fetches DMs for a user
+     */
+    public function getDMs($sinceId=null, $count=20, $page=1) {
+        return json_decode(file_get_contents($this->getUrlDMs($sinceId, $count, $page)));
+    }
+    
+    /**
+     * Send DM
+     */
+    public function sendDM($screenName, $text) {
+        $data = http_build_query(array('screen_name'=>$screenName, 'text'=>$text));
+        $params = array(
+            'http' => array(
+                'method' => 'POST',
+                'content' => $data,
+                'header' => 'Content-type: application/x-www-form-urlencoded',
+            )
+        );
+        $ctx = stream_context_create($params);
+        $fp = fopen($this->getUrlSendDM(), 'rb', false, $ctx);
+        if (!$fp) {
+            return false;
+        }
+        $response = stream_get_contents($fp);
+        if ($response === false) {
+            return false;
+        }
+        $response = json_decode($response);
+        return $response;
+    }
+
+    /**
+     * Sends a tweet
+     *
+     * @param string $txt the tweet text to send
+     * @return string URL of tweet (or false on failure)
+     */
+    public function sendTweet($txt, $limit=true) {
+        if ($limit) {
+            $txt = substr($txt, 0, 140); // twitter message size limit
+        }
+        $data = 'status=' . urlencode($txt);
+        $params = array(
+            'http' => array(
+                'method' => 'POST',
+                'content' => $data,
+                'header' => 'Content-type: application/x-www-form-urlencoded',
+            )
+        );
+        $ctx = stream_context_create($params);
+        $fp = fopen($this->getUrlTweetPost(), 'rb', false, $ctx);
+        if (!$fp) {
+            return false;
+        }
+        $response = stream_get_contents($fp);
+        if ($response === false) {
+            return false;
+        }
+        $response = json_decode($response);
+        return $response;
+    }
+    
+    /**
+     * Returns the base API URL
+     */
+    protected function getUrlApi() {
+        return $this->baseUrlFull;
+    }
+    
+    /**
+     * Returns the status URL
+     *
+     * @param int $num the tweet number
+     */
+    protected function getUrlStatus($num) {
+        return $this->getUrlApi() . 'statuses/show/'. urlencode($num) .'.json';
+    }
+    
+    /**
+     * Returns the user timeline URL
+     */
+    protected function getUrlUserTimeline($user) {
+        return $this->getUrlApi() . 'statuses/user_timeline/'. urlencode($user) .'.json';
+    }
+    
+    /**
+     * Returns the tweet posting URL
+     */
+    protected function getUrlTweetPost() {
+        return $this->getUrlApi() . 'statuses/update.json';
+    }
+    
+    /**
+     * Output URL: status
+     */
+    public function getUrlOutputStatus(StdClass $tweet) {
+        return $this->baseUrl . urlencode($tweet->user->screen_name) . '/statuses/' . urlencode($tweet->id);
+    }
+    
+    /**
+     * Return mentions URL
+     */
+    public function getUrlMentions($sinceId=null, $count=20) {
+        $url = $this->baseUrlFull . 'statuses/mentions.json?count=' . urlencode($count);
+        if ($sinceId !== null) {
+            $url .= '&since_id=' . urlencode($sinceId);
+        }
+        return $url;
+    }
+    
+    /**
+     * Returns the followers URL
+     */
+    public function getUrlFollowers($cursor=-1) {
+        return $this->baseUrlFull . 'statuses/followers.json?cursor=' . ((int)$cursor);
+    }
+    
+    /**
+     * Returns the follow-user URL
+     */
+    public function getUrlFollow($userid) {
+        return $this->baseUrlFull . 'friendships/create/' . ((int) $userid) . '.json';
+    }
+    
+    /**
+     * Returns the get DMs URL
+     */
+    public function getUrlDMs($sinceId=null, $count=20, $page=1) {
+        $url = $this->baseUrlFull . 'direct_messages.json?';
+        if ($sinceId !== null) {
+            $url .= 'since_id=' . urlencode($sinceId);
+        }
+        $url .= "&page={$page}";
+        $url .= "&count={$count}";
+        return $url;
+    }
+
+    /**
+     * Returns the send DM URL
+     */
+    public function getURLSendDM() {
+        return $this->baseUrlFull . 'direct_messages/new.json';
+    }
+}
diff --git a/plugins/Irc/extlib/phergie/Phergie/Plugin/Url.php b/plugins/Irc/extlib/phergie/Phergie/Plugin/Url.php
new file mode 100644 (file)
index 0000000..bac115b
--- /dev/null
@@ -0,0 +1,638 @@
+<?php
+/**
+ * Phergie
+ *
+ * PHP version 5
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.
+ * It is also available through the world-wide-web at this URL:
+ * http://phergie.org/license
+ *
+ * @category  Phergie
+ * @package   Phergie_Plugin_Url
+ * @author    Phergie Development Team <team@phergie.org>
+ * @copyright 2008-2010 Phergie Development Team (http://phergie.org)
+ * @license   http://phergie.org/license New BSD License
+ * @link      http://pear.phergie.org/package/Phergie_Plugin_Url
+ */
+
+/**
+ * Monitors incoming messages for instances of URLs and responds with messages
+ * containing relevant information about detected URLs.
+ *
+ * Has an utility method accessible via
+ * $this->getPlugin('Url')->getTitle('http://foo..').
+ *
+ * @category Phergie
+ * @package  Phergie_Plugin_Url
+ * @author   Phergie Development Team <team@phergie.org>
+ * @license  http://phergie.org/license New BSD License
+ * @link     http://pear.phergie.org/package/Phergie_Plugin_Url
+ * @uses     Phergie_Plugin_Encoding pear.phergie.org
+ * @uses     Phergie_Plugin_Http pear.phergie.org
+ * @uses     Phergie_Plugin_Tld pear.phergie.org
+ */
+class Phergie_Plugin_Url extends Phergie_Plugin_Abstract
+{
+    /**
+     * Links output format
+     *
+     * Can use the variables %nick%, %title% and %link% in it to display
+     * page titles and links
+     *
+     * @var string
+     */
+    protected $baseFormat = '%message%';
+    protected $messageFormat = '[ %link% ] %title%';
+
+    /**
+     * Flag indicating whether a single response should be sent for a single
+     * message containing multiple links
+     *
+     * @var bool
+     */
+    protected $mergeLinks = true;
+
+    /**
+     * Max length of the fetched URL title
+     *
+     * @var int
+     */
+    protected $titleLength = 40;
+
+    /**
+     * Url cache to prevent spamming, especially with multiple bots on the
+     * same channel
+     *
+     * @var array
+     */
+    protected $urlCache = array();
+    protected $shortCache = array();
+
+    /**
+     * Time in seconds to store the cached entries
+     *
+     * Setting it to 0 or below disables the cache expiration
+     *
+     * @var int
+     */
+    protected $expire = 1800;
+
+    /**
+     * Number of entries to keep in the cache at one time per channel
+     *
+     * Setting it to 0 or below disables the cache limit
+     *
+     * @var int
+     */
+    protected $limit = 10;
+
+    /**
+     * Flag that determines if the plugin will fall back to using an HTTP
+     * stream when a URL using SSL is detected and OpenSSL support isn't
+     * available in the PHP installation in use
+     *
+     * @var bool
+     */
+    protected $sslFallback = true;
+
+    /**
+     * Flag that is set to true by the custom error handler if an HTTP error
+     * code has been received
+     *
+     * @var boolean
+     */
+    protected $errorStatus = false;
+    protected $errorMessage = null;
+
+    /**
+     * Flag indicating whether or not to display error messages as the title
+     * if a link posted encounters an error
+     *
+     * @var boolean
+     */
+    protected $showErrors = true;
+
+    /**
+     * Flag indicating whether to detect schemeless URLS (i.e. "example.com")
+     *
+     * @var boolean
+     */
+    protected $detectSchemeless = false;
+
+    /**
+     * Shortener object
+     */
+    protected $shortener;
+
+    /**
+     * Array of renderers
+     */
+    protected $renderers = array();
+
+    /**
+     * Checks for dependencies.
+     *
+     * @return void
+     */
+    public function onLoad()
+    {
+        $plugins = $this->plugins;
+        $plugins->getPlugin('Encoding');
+        $plugins->getPlugin('Http');
+        $plugins->getPlugin('Tld');
+
+        // make the shortener configurable
+        $shortener = $this->getConfig('url.shortener', 'Trim');
+        $shortener = "Phergie_Plugin_Url_Shorten_{$shortener}";
+        $this->shortener = new $shortener($this->plugins->getPlugin('Http'));
+
+        if (!$this->shortener instanceof Phergie_Plugin_Url_Shorten_Abstract) {
+            $this->fail("Declared shortener class {$shortener} is not of proper ancestry");
+        }
+
+        // load config (a bit ugly, but focusing on porting):
+        foreach (
+            array(
+                'detect_schemeless' => 'detectSchemeless',
+                'base_format' => 'baseFormat',
+                'message_format' => 'messageFormat',
+                'merge_links' => 'mergeLinks',
+                'title_length' => 'titleLength',
+                'show_errors' => 'showErrors',
+                'expire' => 'expire',
+            ) as $config => $local) {
+            if (isset($this->config["url.{$config}"])) {
+                $this->$local = $this->config["uri.{$config}"];
+            }
+        }
+    }
+
+    /**
+     * Checks an incoming message for the presence of a URL and, if one is
+     * found, responds with its title if it is an HTML document and the
+     * shortened equivalent of its original URL if it meets length requirements.
+     *
+     * @todo Update this to pull configuration settings from $this->config
+     *       rather than caching them as class properties
+     * @return void
+     */
+    public function onPrivmsg()
+    {
+        $this->handleMsg();
+    }
+
+    /**
+     * Checks an incoming message for the presence of a URL and, if one is
+     * found, responds with its title if it is an HTML document and the
+     * shortened equivalent of its original URL if it meets length requirements.
+     *
+     * @todo Update this to pull configuration settings from $this->config
+     *       rather than caching them as class properties
+     * @return void
+     */
+    public function onAction()
+    {
+        $this->handleMsg();
+    }
+
+    /**
+     * Handles message events and responds with url titles.
+     *
+     * @return void
+     */
+    protected function handleMsg()
+    {
+        $source = $this->getEvent()->getSource();
+        $user = $this->getEvent()->getNick();
+
+        $responses = array();
+        $urls = $this->findUrls($this->getEvent()->getArgument(1));
+
+        foreach ($urls as $parsed) {
+            $url = $parsed['glued'];
+
+            // allow out-of-class renderers to handle this URL
+            foreach ($this->renderers as $renderer) {
+                if ($renderer->renderUrl($parsed) === true) {
+                    // renderers should return true if they've fully
+                    // rendered the passed URL (they're responsible
+                    // for their own output)
+                    $this->debug('Handled by renderer: ' . get_class($renderer));
+                    continue 2;
+                }
+            }
+
+            // Convert url
+            $shortenedUrl = $this->shortener->shorten($url);
+            if (!$shortenedUrl) {
+                $this->debug('Invalid Url: Unable to shorten. (' . $url . ')');
+                $shortenedUrl = $url;
+            }
+
+            // Prevent spamfest
+            if ($this->checkUrlCache($url, $shortenedUrl)) {
+                $this->debug('Invalid Url: URL is in the cache. (' . $url . ')');
+                continue;
+            }
+
+            $title = $this->getTitle($url);
+            if (!empty($title)) {
+                $responses[] = str_replace(
+                    array(
+                        '%title%',
+                        '%link%',
+                        '%nick%'
+                    ), array(
+                        $title,
+                        $shortenedUrl,
+                        $user
+                    ), $this->messageFormat
+                );
+            }
+
+            // Update cache
+            $this->updateUrlCache($url, $shortenedUrl);
+            unset($title, $shortenedUrl, $title);
+        }
+
+        // Check to see if there were any URL responses, format them and handle if they
+        // get merged into one message or not
+        if (count($responses) > 0) {
+            if ($this->mergeLinks) {
+                $message = str_replace(
+                    array(
+                        '%message%',
+                        '%nick%'
+                    ), array(
+                        implode('; ', $responses),
+                        $user
+                    ), $this->baseFormat
+                );
+                $this->doPrivmsg($source, $message);
+            } else {
+                foreach ($responses as $response) {
+                    $message = str_replace(
+                        array(
+                            '%message%',
+                            '%nick%'
+                        ), array(
+                            implode('; ', $responses),
+                            $user
+                        ), $this->baseFormat
+                    );
+                    $this->doPrivmsg($source, $message);
+                }
+            }
+        }
+    }
+
+    /**
+     * Detect URLs in a given string.
+     *
+     * @param string $message the string to detect urls in
+     *
+     * @return array the array of urls found
+     */
+    public function findUrls($message)
+    {
+        $pattern = '#'.($this->detectSchemeless ? '' : 'https?://').'(?:([0-9]{1,3}(?:\.[0-9]{1,3}){3})(?![^/]) | ('
+            .($this->detectSchemeless ? '(?<!http:/|https:/)[@/\\\]' : '').')?(?:(?:[a-z0-9_-]+\.?)+\.[a-z0-9]{1,6}))[^\s]*#xis';
+        $urls = array();
+
+        // URL Match
+        if (preg_match_all($pattern, $message, $matches, PREG_SET_ORDER)) {
+            foreach ($matches as $m) {
+                $url = trim(rtrim($m[0], ', ].?!;'));
+
+                // Check to see if the URL was from an email address, is a directory, etc
+                if (!empty($m[2])) {
+                    $this->debug('Invalid Url: URL is either an email or a directory path. (' . $url . ')');
+                    continue;
+                }
+
+                // Parse the given URL
+                if (!$parsed = $this->parseUrl($url)) {
+                    $this->debug('Invalid Url: Could not parse the URL. (' . $url . ')');
+                    continue;
+                }
+
+                // Check to see if the given IP/Host is valid
+                if (!empty($m[1]) and !$this->checkValidIP($m[1])) {
+                    $this->debug('Invalid Url: ' . $m[1] . ' is not a valid IP address. (' . $url . ')');
+                    continue;
+                }
+
+                // Process TLD if it's not an IP
+                if (empty($m[1])) {
+                    // Get the TLD from the host
+                    $pos = strrpos($parsed['host'], '.');
+                    $parsed['tld'] = ($pos !== false ? substr($parsed['host'], ($pos+1)) : '');
+
+                    // Check to see if the URL has a valid TLD
+                    if ($this->plugins->tld->getTld($parsed['tld']) === false) {
+                        $this->debug('Invalid Url: ' . $parsed['tld'] . ' is not a supported TLD. (' . $url . ')');
+                        continue;
+                    }
+                }
+
+                // Check to see if the URL is to a secured site or not and handle it accordingly
+                if ($parsed['scheme'] == 'https' && !extension_loaded('openssl')) {
+                    if (!$this->sslFallback) {
+                        $this->debug('Invalid Url: HTTPS is an invalid scheme, OpenSSL isn\'t available. (' . $url . ')');
+                        continue;
+                    } else {
+                        $parsed['scheme'] = 'http';
+                    }
+                }
+
+                if (!in_array($parsed['scheme'], array('http', 'https'))) {
+                    $this->debug('Invalid Url: ' . $parsed['scheme'] . ' is not a supported scheme. (' . $url . ')');
+                    continue;
+                }
+
+                $urls[] = $parsed + array('glued' => $this->glueURL($parsed));
+            }
+        }
+
+        return $urls;
+    }
+
+    /**
+     * Checks a given URL (+shortened) against the cache to verify if they were
+     * previously posted on the channel.
+     *
+     * @param string $url          The URL to check against
+     * @param string $shortenedUrl The shortened URL to check against
+     *
+     * @return bool
+     */
+    protected function checkUrlCache($url, $shortenedUrl)
+    {
+        $source = $this->getEvent()->getSource();
+
+        /**
+         * Transform the URL (+shortened) into a HEX CRC32 checksum to prevent potential problems
+         * and minimize the size of the cache for less cache bloat.
+         */
+        $url = $this->getUrlChecksum($url);
+        $shortenedUrl = $this->getUrlChecksum($shortenedUrl);
+
+        $cache = array(
+            'url' => isset($this->urlCache[$source][$url]) ? $this->urlCache[$source][$url] : null,
+            'shortened' => isset($this->shortCache[$source][$shortenedUrl]) ? $this->shortCache[$source][$shortenedUrl] : null
+        );
+
+        $expire = $this->expire;
+        $this->debug("Cache expire: {$expire}");
+        /**
+         * If cache expiration is enabled, check to see if the given url has expired in the cache
+         * If expire is disabled, simply check to see if the url is listed
+         */
+        if (($expire > 0 && (($cache['url'] + $expire) > time() || ($cache['shortened'] + $expire) > time()))
+            || ($expire <= 0 && (isset($cache['url']) || isset($cache['shortened'])))
+        ) {
+            unset($cache, $url, $shortenedUrl, $expire);
+            return true;
+        }
+        unset($cache, $url, $shortenedUrl, $expire);
+        return false;
+    }
+
+    /**
+     * Updates the cache and adds the given URL (+shortened) to the cache. It
+     * also handles cleaning the cache of old entries as well.
+     *
+     * @param string $url          The URL to add to the cache
+     * @param string $shortenedUrl The shortened to add to the cache
+     *
+     * @return bool
+     */
+    protected function updateUrlCache($url, $shortenedUrl)
+    {
+        $source = $this->getEvent()->getSource();
+
+        /**
+         * Transform the URL (+shortened) into a HEX CRC32 checksum to prevent potential problems
+         * and minimize the size of the cache for less cache bloat.
+         */
+        $url = $this->getUrlChecksum($url);
+        $shortenedUrl = $this->getUrlChecksum($shortenedUrl);
+        $time = time();
+
+        // Handle the URL cache and remove old entries that surpass the limit if enabled
+        $this->urlCache[$source][$url] = $time;
+        if ($this->limit > 0 && count($this->urlCache[$source]) > $this->limit) {
+            asort($this->urlCache[$source], SORT_NUMERIC);
+            array_shift($this->urlCache[$source]);
+        }
+
+        // Handle the shortened cache and remove old entries that surpass the limit if enabled
+        $this->shortCache[$source][$shortenedUrl] = $time;
+        if ($this->limit > 0 && count($this->shortCache[$source]) > $this->limit) {
+            asort($this->shortCache[$source], SORT_NUMERIC);
+            array_shift($this->shortCache[$source]);
+        }
+        unset($url, $shortenedUrl, $time);
+    }
+
+    /**
+     * Transliterates a UTF-8 string into corresponding ASCII characters and
+     * truncates and appends an ellipsis to the string if it exceeds a given
+     * length.
+     *
+     * @param string $str  String to decode
+     * @param int    $trim Maximum string length, optional
+     *
+     * @return string
+     */
+    protected function decode($str, $trim = null)
+    {
+        $out = $this->plugins->encoding->transliterate($str);
+        if ($trim > 0) {
+            $out = substr($out, 0, $trim) . (strlen($out) > $trim ? '...' : '');
+        }
+        return $out;
+    }
+
+    /**
+     * Takes a url, parses and cleans the URL without of all the junk
+     * and then return the hex checksum of the url.
+     *
+     * @param string $url url to checksum
+     *
+     * @return string the hex checksum of the cleaned url
+     */
+    protected function getUrlChecksum($url)
+    {
+        $checksum = strtolower(urldecode($this->glueUrl($url, true)));
+        $checksum = preg_replace('#\s#', '', $this->plugins->encoding->transliterate($checksum));
+        return dechex(crc32($checksum));
+    }
+
+    /**
+     * Parses a given URI and procceses the output to remove redundant
+     * or missing values.
+     *
+     * @param string $url the url to parse
+     *
+     * @return array the url components
+     */
+    protected function parseUrl($url)
+    {
+        if (is_array($url)) return $url;
+
+        $url = trim(ltrim($url, ' /@\\'));
+        if (!preg_match('&^(?:([a-z][-+.a-z0-9]*):)&xis', $url, $matches)) {
+            $url = 'http://' . $url;
+        }
+        $parsed = parse_url($url);
+
+        if (!isset($parsed['scheme'])) {
+            $parsed['scheme'] = 'http';
+        }
+        $parsed['scheme'] = strtolower($parsed['scheme']);
+
+        if (isset($parsed['path']) && !isset($parsed['host'])) {
+            $host = $parsed['path'];
+            $path = '';
+            if (strpos($parsed['path'], '/') !== false) {
+                list($host, $path) = array_pad(explode('/', $parsed['path'], 2), 2, null);
+            }
+            $parsed['host'] = $host;
+            $parsed['path'] = $path;
+        }
+
+        return $parsed;
+    }
+
+    /**
+     * Parses a given URI and then glues it back together in the proper format.
+     * If base is set, then it chops off the scheme, user and pass and fragment
+     * information to return a more unique base URI.
+     *
+     * @param string $uri  uri to rebuild
+     * @param string $base set to true to only return the base components
+     *
+     * @return string the rebuilt uri
+     */
+    protected function glueUrl($uri, $base = false)
+    {
+        $parsed = $uri;
+        if (!is_array($parsed)) {
+            $parsed = $this->parseUrl($parsed);
+        }
+
+        if (is_array($parsed)) {
+            $uri = '';
+            if (!$base) {
+                $uri .= (!empty($parsed['scheme']) ? $parsed['scheme'] . ':' .
+                        ((strtolower($parsed['scheme']) == 'mailto') ? '' : '//') : '');
+                $uri .= (!empty($parsed['user']) ? $parsed['user'] .
+                        (!empty($parsed['pass']) ? ':' . $parsed['pass'] : '') . '@' : '');
+            }
+            if ($base && !empty($parsed['host'])) {
+                $parsed['host'] = trim($parsed['host']);
+                if (substr($parsed['host'], 0, 4) == 'www.') {
+                    $parsed['host'] = substr($parsed['host'], 4);
+                }
+            }
+            $uri .= (!empty($parsed['host']) ? $parsed['host'] : '');
+            if (!empty($parsed['port'])
+                && (($parsed['scheme'] == 'http' && $parsed['port'] == 80)
+                || ($parsed['scheme'] == 'https' && $parsed['port'] == 443))
+            ) {
+                unset($parsed['port']);
+            }
+            $uri .= (!empty($parsed['port']) ? ':' . $parsed['port'] : '');
+            if (!empty($parsed['path']) && (!$base || $base && $parsed['path'] != '/')) {
+                $uri .= (substr($parsed['path'], 0, 1) == '/') ? $parsed['path'] : ('/' . $parsed['path']);
+            }
+            $uri .= (!empty($parsed['query']) ? '?' . $parsed['query'] : '');
+            if (!$base) {
+                $uri .= (!empty($parsed['fragment']) ? '#' . $parsed['fragment'] : '');
+            }
+        }
+        return $uri;
+    }
+
+    /**
+     * Checks the given string to see if its a valid IP4 address
+     *
+     * @param string $ip the ip to validate
+     *
+     * @return bool
+     */
+    protected function checkValidIP($ip)
+    {
+        return long2ip(ip2long($ip)) === $ip;
+    }
+
+    /**
+     * Returns the title of the given page
+     *
+     * @param string $url url to the page
+     *
+     * @return string title
+     */
+    public function getTitle($url)
+    {
+        $http = $this->plugins->getPlugin('Http');
+        $options = array(
+            'timeout' => 3.5,
+            'user_agent' => 'Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.1.12) Gecko/20080201 Firefox/2.0.0.12'
+        );
+
+        $response = $http->head($url, array(), $options);
+        $header = $response->getHeaders('Content-Type');
+
+        if (!preg_match('#^(text/x?html|application/xhtml+xml)(?:;.*)?$#', $header)) {
+            $title = $header;
+        } else {
+            $response = $http->get($url, array(), $options);
+            $content = $response->getContent();
+            if (preg_match('#<title[^>]*>(.*?)</title>#is', $content, $match)) {
+                $title = preg_replace('/[\s\v]+/', ' ', trim($match[1]));
+            }
+        }
+        $encoding = $this->plugins->getPlugin('Encoding');
+        $title = $encoding->decodeEntities($title);
+
+        if (empty($title)) {
+            if ($response->isError()) {
+                $title = $response->getCodeAsString();
+            } else {
+                $title = 'No Title';
+            }
+        }
+
+        return $title;
+    }
+
+    /**
+     * Output a debug message
+     *
+     * @param string $msg the message to output
+     *
+     * @return void
+     */
+    protected function debug($msg)
+    {
+        echo "(DEBUG:Url) $msg\n";
+    }
+
+    /**
+     * Add a renderer to the stack
+     *
+     * @param object $obj the renderer to add
+     *
+     * @return void
+     */
+    public function registerRenderer($obj)
+    {
+        $this->renderers[spl_object_hash($obj)] = $obj;
+    }
+}
diff --git a/plugins/Irc/extlib/phergie/Phergie/Plugin/Url/Shorten/Abstract.php b/plugins/Irc/extlib/phergie/Phergie/Plugin/Url/Shorten/Abstract.php
new file mode 100644 (file)
index 0000000..607d165
--- /dev/null
@@ -0,0 +1,105 @@
+<?php
+/**
+ * Phergie 
+ *
+ * PHP version 5
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.
+ * It is also available through the world-wide-web at this URL:
+ * http://phergie.org/license
+ *
+ * @category  Phergie 
+ * @package   Phergie_Plugin_Php
+ * @author    Phergie Development Team <team@phergie.org>
+ * @copyright 2008-2010 Phergie Development Team (http://phergie.org)
+ * @license   http://phergie.org/license New BSD License
+ * @link      http://pear.phergie.org/package/Phergie_Plugin_Php
+ */
+
+/**
+ * URL shortener abstract class
+ *
+ * @category Phergie 
+ * @package  Phergie_Plugin_Url
+ * @author   Phergie Development Team <team@phergie.org>
+ * @license  http://phergie.org/license New BSD License
+ * @link     http://pear.phergie.org/package/Phergie_Plugin_Url
+ * @uses     Phergie_Plugin_Http pear.phergie.org
+ */
+abstract class Phergie_Plugin_Url_Shorten_Abstract
+{
+    protected $http;
+
+    /**
+     * Constructor
+     *
+     * @param Phergie_Plugin_Http $http instance of the http plugin
+     */
+    public function __construct(Phergie_Plugin_Http $http)
+    {
+        $this->http = $http;
+    }
+
+    /**
+     * Returns an array of request parameters given a url to shorten. The
+     * following keys are valid request parameters:
+     *
+     *  * 'uri': the URI for the request (required)
+     *  * 'query': an array of key-value pairs sent in a GET request
+     *  * 'post': an array of key-value pairs sent in a POST request
+     *  * 'callback': to be called after the request is finished. Should accept
+     *    a Phergie_Plugin_Http_Response object and return either the shortened
+     *    url or false if an error has occured.
+     *
+     * If the 'post' key is present a POST request shall be made; otherwise
+     * a GET request will be made. The 'post' key can be an empty array and
+     * a post request will still be made.
+     *
+     * If no callback is provided the contents of the response will be returned.
+     *
+     * @param string $url the url to shorten
+     *
+     * @return array the request parameters
+     */
+    protected abstract function getRequestParams($url);
+
+    /**
+     * Shortens a given url.
+     *
+     * @param string $url the url to shorten
+     *
+     * @return string the shortened url or false on a failure
+     */
+    public function shorten($url)
+    {
+        $defaults = array('get' => array(), 'post' => array(), 'callback' => null);
+        $options = array('timeout' => 2);
+        $params = $this->getRequestParams($url) + $defaults;
+
+        // Should some kind of notice be thrown? Maybe just if getRequestParams does not return an array?
+        if (!is_array($params) || empty($params['uri'])) {
+            return $url;
+        }
+
+        if (!empty($params['post'])) {
+            $response = $this->http->post($params['uri'], $params['get'], $params['post'], $options);
+        } else {
+            $response = $this->http->get($params['uri'], $params['get'], $options);
+        }
+
+        if (is_callable($params['callback'])) {
+            return call_user_func($params['callback'], $response);
+        }
+
+        $code = $response->getCode();
+        $content = trim($response->getContent);
+        if ($code < 200 || $code >= 300 || empty($content)) {
+            return false;
+        }
+
+        return $response->getContent();
+    }
+}
diff --git a/plugins/Irc/extlib/phergie/Phergie/Plugin/Url/Shorten/Trim.php b/plugins/Irc/extlib/phergie/Phergie/Plugin/Url/Shorten/Trim.php
new file mode 100644 (file)
index 0000000..af7e8a5
--- /dev/null
@@ -0,0 +1,64 @@
+<?php
+/**
+ * Phergie 
+ *
+ * PHP version 5
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.
+ * It is also available through the world-wide-web at this URL:
+ * http://phergie.org/license
+ *
+ * @category  Phergie 
+ * @package   Phergie_Plugin_Php
+ * @author    Phergie Development Team <team@phergie.org>
+ * @copyright 2008-2010 Phergie Development Team (http://phergie.org)
+ * @license   http://phergie.org/license New BSD License
+ * @link      http://pear.phergie.org/package/Phergie_Plugin_Php
+ */
+
+/**
+ * Shortens urls via the tr.im service
+ *
+ * @category Phergie 
+ * @package  Phergie_Plugin_Url
+ * @author   Phergie Development Team <team@phergie.org>
+ * @license  http://phergie.org/license New BSD License
+ * @link     http://pear.phergie.org/package/Phergie_Plugin_Url
+ */
+class Phergie_Plugin_Url_Shorten_Trim extends Phergie_Plugin_Url_Shorten_Abstract
+{
+    /**
+     * Returns an array of request parameters given a url to shorten. The
+     * following keys are valid request parameters:
+     *
+     * @param string $url the url to shorten
+     *
+     * @return array the request parameters
+     */
+    protected function getRequestParams($url)
+    {
+        return array(
+            'uri' => 'http://api.tr.im/v1/trim_simple?url=' . rawurlencode($url),
+            'callback' => array($this, 'onComplete')
+        );
+    }
+
+    /**
+     * Callback for when the URL has been shortened. Checks for error messages.
+     *
+     * @param Phergie_Plugin_Http_Response $response the response object
+     *
+     * @return string|bool the shortened url or false on failure
+     */
+    protected function onComplete($response)
+    {
+        if (strpos($response->getContent(), 'Error: ') === 0) {
+            return false;
+        }
+
+        return $response->getContent();
+    }
+}
diff --git a/plugins/Irc/extlib/phergie/Phergie/Plugin/UserInfo.php b/plugins/Irc/extlib/phergie/Phergie/Plugin/UserInfo.php
new file mode 100644 (file)
index 0000000..9437073
--- /dev/null
@@ -0,0 +1,413 @@
+<?php
+/**
+ * Phergie
+ *
+ * PHP version 5
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.
+ * It is also available through the world-wide-web at this URL:
+ * http://phergie.org/license
+ *
+ * @category  Phergie
+ * @package   Phergie_Plugin_UserInfo
+ * @author    Phergie Development Team <team@phergie.org>
+ * @copyright 2008-2010 Phergie Development Team (http://phergie.org)
+ * @license   http://phergie.org/license New BSD License
+ * @link      http://pear.phergie.org/package/Phergie_Plugin_UserInfo
+ */
+
+/**
+ * Provides an API for querying information on users.
+ *
+ * @category Phergie
+ * @package  Phergie_Plugin_UserInfo
+ * @author   Phergie Development Team <team@phergie.org>
+ * @license  http://phergie.org/license New BSD License
+ * @link     http://pear.phergie.org/package/Phergie_Plugin_UserInfo
+ */
+class Phergie_Plugin_UserInfo extends Phergie_Plugin_Abstract
+{
+    const REGULAR = 1;
+    const VOICE   = 2;
+    const HALFOP  = 4;
+    const OP      = 8;
+    const ADMIN   = 16;
+    const OWNER   = 32;
+
+    /**
+     * An array containing all the user information for a given channel
+     *
+     * @var array
+     */
+    protected $store = array();
+
+    /**
+     * Tracks mode changes
+     *
+     * @return void
+     */
+    public function onMode()
+    {
+        $args = $this->event->getArguments();
+
+        if (count($args) != 3) {
+            return;
+        }
+
+        list($chan, $modes, $nicks) = $args;
+
+        if (!preg_match('/(?:\+|-)[hovaq+-]+/i', $modes)) {
+            return;
+        }
+
+        $chan = trim(strtolower($chan));
+        $modes = str_split(trim(strtolower($modes)), 1);
+        $nicks = explode(' ', trim(strtolower($nicks)));
+        $operation = array_shift($modes); // + or -
+
+        while ($char = array_shift($modes)) {
+            $nick = array_shift($nicks);
+            $mode = null;
+
+            switch ($char) {
+            case 'q':
+                $mode = self::OWNER;
+                break;
+            case 'a':
+                $mode = self::ADMIN;
+                break;
+            case 'o':
+                $mode = self::OP;
+                break;
+            case 'h':
+                $mode = self::HALFOP;
+                break;
+            case 'v':
+                $mode = self::VOICE;
+                break;
+            }
+
+            if (!empty($mode)) {
+                if ($operation == '+') {
+                    $this->store[$chan][$nick] |= $mode;
+                } else if ($operation == '-') {
+                    $this->store[$chan][$nick] ^= $mode;
+                }
+            }
+        }
+    }
+
+    /**
+     * Tracks users joining a channel
+     *
+     * @return void
+     */
+    public function onJoin()
+    {
+        $chan = trim(strtolower($this->event->getArgument(0)));
+        $nick = trim(strtolower($this->event->getNick()));
+
+        $this->store[$chan][$nick] = self::REGULAR;
+    }
+
+    /**
+     * Tracks users leaving a channel
+     *
+     * @return void
+     */
+    public function onPart()
+    {
+        $chan = trim(strtolower($this->event->getArgument(0)));
+        $nick = trim(strtolower($this->event->getNick()));
+
+        if (isset($this->store[$chan][$nick])) {
+            unset($this->store[$chan][$nick]);
+        }
+    }
+
+    /**
+     * Tracks users quitting a server
+     *
+     * @return void
+     */
+    public function onQuit()
+    {
+        $nick = trim(strtolower($this->event->getNick()));
+
+        foreach ($this->store as $chan => $store) {
+            if (isset($store[$nick])) {
+                unset($this->store[$chan][$nick]);
+            }
+        }
+    }
+
+    /**
+     * Tracks users changing nicks
+     *
+     * @return void
+     */
+    public function onNick()
+    {
+        $nick = trim(strtolower($this->event->getNick()));
+        $newNick = trim(strtolower($this->event->getArgument(0)));
+
+        foreach ($this->store as $chan => $store) {
+            if (isset($store[$nick])) {
+                $this->store[$chan][$newNick] = $store[$nick];
+                unset($this->store[$chan][$nick]);
+            }
+        }
+    }
+
+    /**
+     * Populates the internal user listing for a channel when the bot joins it.
+     *
+     * @return void
+     */
+    public function onResponse()
+    {
+        if ($this->event->getCode() != Phergie_Event_Response::RPL_NAMREPLY) {
+            return;
+        }
+
+        $desc = preg_split('/[@*=]\s*/', $this->event->getDescription(), 2);
+        list($chan, $users) = array_pad(explode(' :', trim($desc[1])), 2, null);
+        $users = explode(' ', trim($users));
+
+        $chan = trim(strtolower($chan));
+
+        foreach ($users as $user) {
+            if (empty($user)) {
+                continue;
+            }
+
+            $user = trim(strtolower($user));
+            $flag = self::REGULAR;
+
+            if ($user[0] == '~') {
+                $flag |= self::OWNER;
+            } else if ($user[0] == '&') {
+                $flag |= self::ADMIN;
+            } else if ($user[0] == '@') {
+                $flag |= self::OP;
+            } else if ($user[0] == '%') {
+                $flag |= self::HALFOP;
+            } else if ($user[0] == '+') {
+                $flag |= self::VOICE;
+            }
+
+            if ($flag != self::REGULAR) {
+                $user = substr($user, 1);
+            }
+
+            $this->store[$chan][$user] = $flag;
+        }
+    }
+
+    /**
+     * Debugging function
+     *
+     * @return void
+     */
+    public function onPrivmsg()
+    {
+        if ($this->getConfig('debug', false) == false) {
+            return;
+        }
+
+        list($target, $msg) = array_pad($this->event->getArguments(), 2, null);
+
+        if (preg_match('#^ishere (\S+)$#', $msg, $m)) {
+            $this->doPrivmsg($target, $this->isIn($m[1], $target) ? 'true' : 'false');
+        } elseif (preg_match('#^isowner (\S+)$#', $msg, $m)) {
+            $this->doPrivmsg($target, $this->isOwner($m[1], $target) ? 'true' : 'false');
+        } elseif (preg_match('#^isadmin (\S+)$#', $msg, $m)) {
+            $this->doPrivmsg($target, $this->isAdmin($m[1], $target) ? 'true' : 'false');
+        } elseif (preg_match('#^isop (\S+)$#', $msg, $m)) {
+            $this->doPrivmsg($target, $this->isOp($m[1], $target) ? 'true' : 'false');
+        } elseif (preg_match('#^ishop (\S+)$#', $msg, $m)) {
+            $this->doPrivmsg($target, $this->isHalfop($m[1], $target) ? 'true' : 'false');
+        } elseif (preg_match('#^isvoice (\S+)$#', $msg, $m)) {
+            $this->doPrivmsg($target, $this->isVoice($m[1], $target) ? 'true' : 'false');
+        } elseif (preg_match('#^channels (\S+)$#', $msg, $m)) {
+            $channels = $this->getChannels($m[1]);
+            $this->doPrivmsg($target, $channels ? join(', ', $channels) : 'unable to find nick');
+        } elseif (preg_match('#^users (\S+)$#', $msg, $m)) {
+            $nicks = $this->getUsers($m[1]);
+            $this->doPrivmsg($target, $nicks ? join(', ', $nicks) : 'unable to find channel');
+        } elseif (preg_match('#^random (\S+)$#', $msg, $m)) {
+            $nick = $this->getrandomuser($m[1]);
+            $this->doPrivmsg($target, $nick ? $nick : 'unable to  find channel');
+        }
+    }
+
+    /**
+     * Checks whether or not a given user has a mode
+     *
+     * @param int    $mode A numeric mode (identified by the class constants)
+     * @param string $nick The nick to check
+     * @param string $chan The channel to check in
+     *
+     * @return bool
+     */
+    public function is($mode, $nick, $chan)
+    {
+        $chan = trim(strtolower($chan));
+        $nick = trim(strtolower($nick));
+
+        if (!isset($this->store[$chan][$nick])) {
+            return false;
+        }
+
+        return ($this->store[$chan][$nick] & $mode) != 0;
+    }
+
+    /**
+     * Checks whether or not a given user has owner (~) status
+     *
+     * @param string $nick The nick to check
+     * @param string $chan The channel to check in
+     *
+     * @return bool
+     */
+    public function isOwner($nick, $chan)
+    {
+        return $this->is(self::OWNER, $nick, $chan);
+    }
+
+    /**
+     * Checks whether or not a given user has admin (&) status
+     *
+     * @param string $nick The nick to check
+     * @param string $chan The channel to check in
+     *
+     * @return bool
+     */
+    public function isAdmin($nick, $chan)
+    {
+        return $this->is(self::ADMIN, $nick, $chan);
+    }
+
+    /**
+     * Checks whether or not a given user has operator (@) status
+     *
+     * @param string $nick The nick to check
+     * @param string $chan The channel to check in
+     *
+     * @return bool
+     */
+    public function isOp($nick, $chan)
+    {
+        return $this->is(self::OP, $nick, $chan);
+    }
+
+    /**
+     * Checks whether or not a given user has halfop (%) status
+     *
+     * @param string $nick The nick to check
+     * @param string $chan The channel to check in
+     *
+     * @return bool
+     */
+    public function isHalfop($nick, $chan)
+    {
+        return $this->is(self::HALFOP, $nick, $chan);
+    }
+
+    /**
+     * Checks whether or not a given user has voice (+) status
+     *
+     * @param string $nick The nick to check
+     * @param string $chan The channel to check in
+     *
+     * @return bool
+     */
+    public function isVoice($nick, $chan)
+    {
+        return $this->is(self::VOICE, $nick, $chan);
+    }
+
+    /**
+     * Checks whether or not a given user is in a channel
+     *
+     * @param string $nick The nick to check
+     * @param string $chan The channel to check in
+     *
+     * @return bool
+     */
+    public function isIn($nick, $chan)
+    {
+        return $this->is(self::REGULAR, $nick, $chan);
+    }
+
+    /**
+     * Returns the entire user list for a channel or false if the bot is not
+     * in the channel.
+     *
+     * @param string $chan The channel name
+     *
+     * @return array|bool
+     */
+    public function getUsers($chan)
+    {
+        $chan = trim(strtolower($chan));
+        if (isset($this->store[$chan])) {
+            return array_keys($this->store[$chan]);
+        }
+        return false;
+    }
+
+    /**
+     * Returns the nick of a random user present in a given channel or false
+     * if the bot is not present in the channel.
+     *
+     * @param string $chan The channel name
+     *
+     * @return array|bool
+     */
+    public function getRandomUser($chan)
+    {
+        $chan = trim(strtolower($chan));
+
+        if (isset($this->store[$chan])) {
+            $ignore = array('chanserv', 'q', 'l', 's');
+
+            do {
+                $nick = array_rand($this->store[$chan], 1);
+            } while (in_array($nick, $ignore));
+
+            return $nick;
+        }
+
+        return false;
+    }
+
+    /**
+     * Returns a list of channels in which a given user is present.
+     *
+     * @param string $nick Nick of the user (optional, defaults to the bot's
+     *               nick)
+     *
+     * @return array|bool
+     */
+    public function getChannels($nick = null)
+    {
+        if (empty($nick)) {
+            $nick = $this->connection->getNick();
+        }
+
+        $nick = trim(strtolower($nick));
+        $channels = array();
+
+        foreach ($this->store as $chan => $store) {
+            if (isset($store[$nick])) {
+                $channels[] = $chan;
+            }
+        }
+
+        return $channels;
+    }
+}
diff --git a/plugins/Irc/extlib/phergie/Phergie/Plugin/Weather.php b/plugins/Irc/extlib/phergie/Phergie/Plugin/Weather.php
new file mode 100644 (file)
index 0000000..7d4f85a
--- /dev/null
@@ -0,0 +1,149 @@
+<?php
+/**
+ * Phergie
+ *
+ * PHP version 5
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.
+ * It is also available through the world-wide-web at this URL:
+ * http://phergie.org/license
+ *
+ * @category  Phergie
+ * @package   Phergie_Plugin_Weather
+ * @author    Phergie Development Team <team@phergie.org>
+ * @copyright 2008-2010 Phergie Development Team (http://phergie.org)
+ * @license   http://phergie.org/license New BSD License
+ * @link      http://pear.phergie.org/package/Phergie_Plugin_Weather
+ */
+
+/**
+ * Detects and responds to requests for current weather conditions in a
+ * particular location using data from a web service. Requires registering
+ * with weather.com to obtain authentication credentials, which must be
+ * stored in the configuration settings weather.partner_id and
+ * weather.license_key for the plugin to function.
+ *
+ * @category Phergie
+ * @package  Phergie_Plugin_Weather
+ * @author   Phergie Development Team <team@phergie.org>
+ * @license  http://phergie.org/license New BSD License
+ * @link     http://pear.phergie.org/package/Phergie_Plugin_Weather
+ * @link     http://www.weather.com/services/xmloap.html
+ * @uses     Phergie_Plugin_Command pear.phergie.org
+ * @uses     Phergie_Plugin_Http pear.phergie.org
+ * @uses     Phergie_Plugin_Temperature pear.phergie.org
+ * @uses     extension SimpleXML
+ */
+class Phergie_Plugin_Weather extends Phergie_Plugin_Abstract
+{
+    /**
+     * Checks for dependencies.
+     *
+     * @return void
+     */
+    public function onLoad()
+    {
+        $plugins = $this->getPluginHandler();
+        $plugins->getPlugin('Command');
+        $plugins->getPlugin('Http');
+        $plugins->getPlugin('Temperature');
+
+        if (empty($this->config['weather.partner_id'])
+            || empty($this->config['weather.license_key'])) {
+            $this->fail('weather.partner_id and weather.license_key must be specified');
+        }
+    }
+
+    /**
+     * Returns a weather report for a specified location.
+     *
+     * @param string $location Zip code or city/state/country specification
+     *
+     * @return void
+     */
+    public function onCommandWeather($location)
+    {
+        $response = $this->plugins->http->get(
+            'http://xoap.weather.com/search/search',
+            array('where' => $location)
+        );
+
+        if ($response->isError()) {
+            $this->doNotice(
+                $this->event->getNick(),
+                'ERROR: ' . $response->getCode() . ' ' . $response->getMessage()
+            );
+            return;
+        }
+
+        $nick = $this->event->getNick();
+
+        $xml = $response->getContent();
+        if (count($xml->loc) == 0) {
+            $this->doNotice($nick, 'No results for that location.');
+            return;
+        }
+
+        $where = (string) $xml->loc[0]['id'];
+        $response = $this->plugins->http->get(
+            'http://xoap.weather.com/weather/local/' . $where,
+            array(
+                'cc' => '*',
+                'link' => 'xoap',
+                'prod' => 'xoap',
+                'par' => $this->config['weather.partner_id'],
+                'key' => $this->config['weather.license_key'],
+            )
+        );
+
+        if ($response->isError()) {
+            $this->doNotice(
+                $this->event->getNick(),
+                'ERROR: ' . $response->getCode() . ' ' . $response->getMessage()
+            );
+            return;
+        }
+
+        $temperature = $this->plugins->getPlugin('Temperature');
+        $xml = $response->getContent();
+        $weather = 'Weather for ' . (string) $xml->loc->dnam . ' - ';
+        switch ($xml->head->ut) {
+            case 'F':
+                $tempF = $xml->cc->tmp;
+                $tempC = $temperature->convertFahrenheitToCelsius($tempF);
+                break;
+            case 'C':
+                $tempC = $xml->cc->tmp;
+                $tempF = $temperature->convertCelsiusToFahrenheit($tempC);
+                break;
+            default:
+                $this->doNotice(
+                    $this->event->getNick(),
+                    'ERROR: No scale information given.');
+                break;
+        }
+        $r = $xml->cc->hmid;
+        $hiF = $temperature->getHeatIndex($tempF, $r);
+        $hiC = $temperature->convertFahrenheitToCelsius($hiF);
+        $weather .= 'Temperature: ' . $tempF . 'F/' . $tempC . 'C';
+        $weather .= ', Humidity: ' . (string) $xml->cc->hmid . '%';
+        if ($hiF > $tempF || $hiC > $tempC) {
+            $weather .= ', Heat Index: ' . $hiF . 'F/' . $hiC . 'C';
+        }
+        $weather .=
+            ', Conditions: ' . (string) $xml->cc->t .
+            ', Updated: ' . (string) $xml->cc->lsup .
+            ' [ http://weather.com/weather/today/' .
+            str_replace(
+                array('(', ')', ',', ' '),
+                array('', '', '', '+'),
+                (string) $xml->loc->dnam
+            ) .
+            ' ]';
+
+        $this->doPrivmsg($this->event->getSource(), $nick . ': ' . $weather);
+    }
+}
diff --git a/plugins/Irc/extlib/phergie/Phergie/Plugin/Wine.php b/plugins/Irc/extlib/phergie/Phergie/Plugin/Wine.php
new file mode 100644 (file)
index 0000000..9aa0845
--- /dev/null
@@ -0,0 +1,69 @@
+<?php
+/**
+ * Phergie
+ *
+ * PHP version 5
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.
+ * It is also available through the world-wide-web at this URL:
+ * http://phergie.org/license
+ *
+ * @category  Phergie
+ * @package   Phergie_Plugin_Wine
+ * @author    Phergie Development Team <team@phergie.org>
+ * @copyright 2008-2010 Phergie Development Team (http://phergie.org)
+ * @license   http://phergie.org/license New BSD License
+ * @link      http://pear.phergie.org/package/Phergie_Plugin_Wine
+ */
+
+/**
+ * Processes requests to serve users wine.
+ *
+ * @category Phergie
+ * @package  Phergie_Plugin_Wine
+ * @author   Phergie Development Team <team@phergie.org>
+ * @license  http://phergie.org/license New BSD License
+ * @link     http://pear.phergie.org/package/Phergie_Plugin_Wine
+ * @uses     Phergie_Plugin_Command pear.phergie.org
+ * @uses     Phergie_Plugin_Serve pear.phergie.org
+ */
+class Phergie_Plugin_Wine extends Phergie_Plugin_Abstract
+{
+    /**
+     * Checks for dependencies.
+     *
+     * @return void
+     */
+    public function onLoad()
+    {
+        $plugins = $this->plugins;
+        $plugins->getPlugin('Command');
+        $plugins->getPlugin('Serve');
+    }
+
+    /**
+     * Processes requests to serve a user a wine.
+     *
+     * @param string $request Request including the target and an optional
+     *        suggestion of what wine to serve
+     *
+     * @return void
+     */
+    public function onCommandWine($request)
+    {
+        $format = $this->getConfig(
+            'wine.format',
+            'serves %target% a glass of %item%.'
+        );
+
+        $this->plugins->getPlugin('Serve')->serve(
+            dirname(__FILE__) . '/Wine/wine.db',
+            'wine',
+            $format,
+            $request
+        );
+    }
+}
diff --git a/plugins/Irc/extlib/phergie/Phergie/Plugin/Wine/db.php b/plugins/Irc/extlib/phergie/Phergie/Plugin/Wine/db.php
new file mode 100644 (file)
index 0000000..ce01c2d
--- /dev/null
@@ -0,0 +1,53 @@
+<?php
+
+// Create database schema
+echo 'Creating database', PHP_EOL;
+$file = __DIR__ . '/wine.db';
+if (file_exists($file)) {
+    unlink($file);
+}
+$db = new PDO('sqlite:' . $file);
+$db->exec('CREATE TABLE wine (name VARCHAR(255), link VARCHAR(255))');
+$db->exec('CREATE UNIQUE INDEX wine_name ON wine (name)');
+$insert = $db->prepare('INSERT INTO wine (name, link) VALUES (:name, :link)');
+
+// Get and decompress lcboapi.com data set
+$outer = __DIR__ . '/current.zip';
+if (!file_exists($outer)) {
+    echo 'Downloading lcboapi.com data set', PHP_EOL;
+    copy('http://lcboapi.com/download/current.zip', $outer);
+}
+
+echo 'Decompressing lcboapi.com data set', PHP_EOL;
+$zip = new ZipArchive;
+$zip->open($outer);
+$stat = $zip->statIndex(0);
+$inner = __DIR__ . '/' . $stat['name'];
+$zip->extractTo(__DIR__);
+$zip->close();
+$zip = new ZipArchive;
+$zip->open($inner);
+$stat = $zip->statIndex(0);
+$file = __DIR__ . '/' . $stat['name'];
+$zip->extractTo(__DIR__);
+$zip->close();
+
+// Aggregate data set into the database
+$lcbo = new PDO('sqlite:' . $file);
+$result = $lcbo->query('SELECT product_no, name FROM products WHERE primary_category = "Wine"');
+$wines = $result->fetchAll();
+echo 'Processing lcboapi.com data - ', number_format(count($wines), 0), ' records', PHP_EOL;
+$db->beginTransaction();
+foreach ($wines as $wine) {
+    $name = $wine['name'];
+    $link = 'http://lcboapi.com/products/' . $wine['product_no'];
+    $insert->execute(array($name, $link));
+}
+$db->commit();
+
+// Clean up
+echo 'Cleaning up', PHP_EOL;
+unset($lcbo);
+unlink($outer);
+unlink($inner);
+unlink($file);
diff --git a/plugins/Irc/extlib/phergie/Phergie/Plugin/Youtube.php b/plugins/Irc/extlib/phergie/Phergie/Plugin/Youtube.php
new file mode 100644 (file)
index 0000000..e06ea56
--- /dev/null
@@ -0,0 +1,168 @@
+<?php
+/**
+ * Phergie
+ *
+ * PHP version 5
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.
+ * It is also available through the world-wide-web at this URL:
+ * http://phergie.org/license
+ *
+ * @category  Phergie
+ * @package   Phergie_Plugin_Youtube
+ * @author    Phergie Development Team <team@phergie.org>
+ * @copyright 2008-2010 Phergie Development Team (http://phergie.org)
+ * @license   http://phergie.org/license New BSD License
+ * @link      http://pear.phergie.org/package/Phergie_Plugin_Youtube
+ */
+
+/**
+ * Provides commands used to access several services offered by Google
+ * including search, translation, weather, maps, and currency and general
+ * value unit conversion.
+ *
+ * @category Phergie
+ * @package  Phergie_Plugin_Youtube
+ * @author   Phergie Development Team <team@phergie.org>
+ * @license  http://phergie.org/license New BSD License
+ * @link     http://pear.phergie.org/package/Phergie_Plugin_Youtube
+ * @uses     Phergie_Plugin_Command pear.phergie.org
+ * @uses     Phergie_Plugin_Http pear.phergie.org
+ */
+class Phergie_Plugin_Youtube extends Phergie_Plugin_Abstract
+{
+    /**
+     * Checks for dependencies.
+     *
+     * @return void
+     */
+    public function onLoad()
+    {
+        $plugins = $this->getPluginHandler();
+        $plugins->getPlugin('Command');
+        $plugins->getPlugin('Http');
+        if ($url = $plugins->getPlugin('Url')) {
+            $url->registerRenderer($this);
+        }
+    }
+
+    /**
+     * Queries the YouTube video search web service, processes the first
+     * result, and sends a message back to the current event source.
+     *
+     * @param string $query Search term
+     *
+     * @return object YouTube result object
+     */
+    protected function queryYoutube($query)
+    {
+        $url = 'http://gdata.youtube.com/feeds/api/videos';
+        $params = array(
+            'max-results' => '1',
+            'alt' => 'json',
+            'q' => $query
+        );
+        $http = $this->plugins->getPlugin('Http');
+        $response = $http->get($url, $params);
+        $json = $response->getContent();
+
+        $entries = $json->feed->entry;
+        if (!$entries) {
+            $this->doNotice($this->event->getNick(), 'Query returned no results');
+            return;
+        }
+        $entry = reset($entries);
+
+        $nick = $this->event->getNick();
+        $link = $entry->link[0]->href;
+        $title = $entry->title->{'$t'};
+        $author = $entry->author[0]->name->{'$t'};
+        $seconds = $entry->{'media$group'}->{'yt$duration'}->seconds;
+        $published = $entry->published->{'$t'};
+        $views = $entry->{'yt$statistics'}->viewCount;
+        $rating = $entry->{'gd$rating'}->average;
+
+        $minutes = floor($seconds / 60);
+        $seconds = str_pad($seconds % 60, 2, '0', STR_PAD_LEFT);
+        $parsed_link = parse_url($link);
+        parse_str($parsed_link['query'], $parsed_query);
+        $link = 'http://youtu.be/' . $parsed_query['v'];
+        $published = date('n/j/y g:i A', strtotime($published));
+        $views = number_format($views, 0);
+        $rating = round($rating, 2);
+
+        $format = $this->getConfig('youtube.format');
+        if (!$format) {
+            $format = '%nick%:'
+                . ' [ %link% ]'
+                . ' "%title%" by %author%,'
+                . ' Length %minutes%:%seconds%,'
+                . ' Published %published%,'
+                . ' Views %views%,'
+                . ' Rating %rating%';
+        }
+
+        $replacements = array(
+            'nick' => $nick,
+            'link' => $link,
+            'title' => $title,
+            'author' => $author,
+            'minutes' => $minutes,
+            'seconds' => $seconds,
+            'published' => $published,
+            'views' => $views,
+            'rating' => $rating
+        );
+
+        $msg = $format;
+        foreach ($replacements as $from => $to) {
+            $msg = str_replace('%' . $from . '%', $to, $msg);
+        }
+        $this->doPrivmsg($this->event->getSource(), $msg);
+    }
+
+    /**
+     * Returns the first result of a YouTube search.
+     *
+     * @param string $query Search query
+     *
+     * @return void
+     */
+    public function onCommandYoutube($query)
+    {
+        $this->queryYoutube($query);
+    }
+
+    /**
+     * Renders YouTube URLs.
+     *
+     * @param array $parsed parse_url() output for the URL to render
+     *
+     * @return boolean TRUE if the URL was rendered successfully, FALSE
+     *         otherwise
+     */
+    public function renderUrl(array $parsed)
+    {
+        switch ($parsed['host']) {
+            case 'youtu.be':
+                $v = ltrim($parsed['path'], '/');
+                break;
+            case 'youtube.com':
+            case 'www.youtube.com':
+                parse_str($parsed['query'], $parsed_query);
+                if (!empty($parsed_query['v'])) {
+                    $v = $parsed_query['v'];
+                    break;
+                }
+            default:
+                return false;
+        }
+
+        $this->queryYoutube($v);
+
+        return true;
+    }
+}
diff --git a/plugins/Irc/extlib/phergie/Phergie/Process/Abstract.php b/plugins/Irc/extlib/phergie/Phergie/Process/Abstract.php
new file mode 100755 (executable)
index 0000000..0ec3569
--- /dev/null
@@ -0,0 +1,130 @@
+<?php
+/**
+ * Phergie
+ *
+ * PHP version 5
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.
+ * It is also available through the world-wide-web at this URL:
+ * http://phergie.org/license
+ *
+ * @category  Phergie
+ * @package   Phergie
+ * @author    Phergie Development Team <team@phergie.org>
+ * @copyright 2008-2010 Phergie Development Team (http://phergie.org)
+ * @license   http://phergie.org/license New BSD License
+ * @link      http://pear.phergie.org/package/Phergie
+ */
+
+/**
+ * Base class for obtaining and processing incoming events.
+ *
+ * @category Phergie
+ * @package  Phergie
+ * @author   Phergie Development Team <team@phergie.org>
+ * @license  http://phergie.org/license New BSD License
+ * @link     http://pear.phergie.org/package/Phergie
+ */
+abstract class Phergie_Process_Abstract
+{
+    /**
+     * Current driver instance
+     *
+     * @var Phergie_Driver_Abstract
+     */
+    protected $driver;
+
+    /**
+     * Current connection handler instance
+     *
+     * @var Phergie_Connection_Handler
+     */
+    protected $connections;
+
+    /**
+     * Current plugin handler instance
+     *
+     * @var Phergie_Plugin_Handler
+     */
+    protected $plugins;
+
+    /**
+     * Current event handler instance
+     *
+     * @var Phergie_Event_Handler
+     */
+    protected $events;
+
+    /**
+     * Current end-user interface instance
+     *
+     * @var Phergie_Ui_Abstract
+     */
+    protected $ui;
+
+    /**
+     * List of arguments for use within the instance
+     *
+     * @var array
+     */
+    protected $options = array();
+
+    /**
+     * Gets the required class refences from Phergie_Bot.
+     *
+     * @param Phergie_Bot $bot     Current bot instance in use
+     * @param array       $options Optional processor arguments
+     *
+     * @return void
+     */
+    public function __construct(Phergie_Bot $bot, array $options = array())
+    {
+        $this->driver = $bot->getDriver();
+        $this->plugins = $bot->getPluginHandler();
+        $this->connections = $bot->getConnectionHandler();
+        $this->events = $bot->getEventHandler();
+        $this->ui = $bot->getUi();
+        $this->options = $options;
+    }
+
+    /**
+     * Sends resulting outgoing events from ealier processing in handleEvents.
+     *
+     * @param Phergie_Connection $connection Active connection
+     *
+     * @return void
+     */
+    protected function processEvents(Phergie_Connection $connection)
+    {
+        $this->plugins->preDispatch();
+        if (count($this->events)) {
+            foreach ($this->events as $event) {
+                $this->ui->onCommand($event, $connection);
+
+                $method = 'do' . ucfirst(strtolower($event->getType()));
+                call_user_func_array(
+                    array($this->driver, $method),
+                    $event->getArguments()
+                );
+            }
+        }
+        $this->plugins->postDispatch();
+
+        if ($this->events->hasEventOfType(Phergie_Event_Request::TYPE_QUIT)) {
+            $this->ui->onQuit($connection);
+            $this->connections->removeConnection($connection);
+        }
+
+        $this->events->clearEvents();
+    }
+
+    /**
+     * Obtains and processes incoming events.
+     *
+     * @return void
+     */
+    public abstract function handleEvents();
+}
diff --git a/plugins/Irc/extlib/phergie/Phergie/Process/Async.php b/plugins/Irc/extlib/phergie/Phergie/Process/Async.php
new file mode 100644 (file)
index 0000000..78d5959
--- /dev/null
@@ -0,0 +1,157 @@
+<?php
+/**
+ * Phergie
+ *
+ * PHP version 5
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.
+ * It is also available through the world-wide-web at this URL:
+ * http://phergie.org/license
+ *
+ * @category  Phergie
+ * @package   Phergie
+ * @author    Phergie Development Team <team@phergie.org>
+ * @copyright 2008-2010 Phergie Development Team (http://phergie.org)
+ * @license   http://phergie.org/license New BSD License
+ * @link      http://pear.phergie.org/package/Phergie
+ */
+
+/**
+ * Connection data processor which polls to handle input in an
+ * asynchronous manner. Will also cause the application tick at
+ * the user-defined wait time.
+ *
+ * @category Phergie
+ * @package  Phergie
+ * @author   Phergie Development Team <team@phergie.org>
+ * @license  http://phergie.org/license New BSD License
+ * @link     http://pear.phergie.org/package/Phergie
+ */
+class Phergie_Process_Async extends Phergie_Process_Abstract
+{
+    /**
+     * Length of time to poll for stream activity (seconds)
+     *
+     * @var int
+     */
+    protected $sec = 0;
+
+    /**
+     * Length of time to poll for stream activity (microseconds)
+     *
+     * @var int
+     */
+    protected $usec = 200000;
+
+    /**
+     * Length of time to wait between ticks.
+     *
+     * @var int
+     */
+    protected $wait = 0;
+
+    /**
+     * Records when the application last performed a tick
+     *
+     * @var int
+     */
+    protected $lastTick = 0;
+
+    /**
+     * Overrides the parent class to set the poll time.
+     *
+     * @param Phergie_Bot $bot     Main bot class
+     * @param array       $options Processor arguments
+     *
+     * @return void
+     */
+    public function __construct(Phergie_Bot $bot, array $options)
+    {
+        if (!$bot->getDriver() instanceof Phergie_Driver_Streams) {
+            throw new Phergie_Process_Exception(
+                'The Async event processor requires the Streams driver'
+            );
+        }
+
+        foreach (array('sec', 'usec') as $var) {
+            if (isset($options[$var])) {
+                if (!is_int($options[$var])) {
+                     throw new Phergie_Process_Exception(
+                        'Processor option "' . $var . '" must be an integer'
+                     );
+                }
+                $this->$var = $options[$var];
+            }
+        }
+
+        if (!isset($this->sec) && !isset($this->usec)) {
+            throw new Phergie_Process_Exception(
+                'One of the processor options "sec" or "usec" must be specified'
+            );
+        }
+
+        parent::__construct($bot, $options);
+    }
+
+    /**
+     * Waits for stream activity and performs event processing on
+     * connections with data to read.
+     *
+     * @return void
+     */
+    protected function handleEventsAsync()
+    {
+        $hostmasks = $this->driver->getActiveReadSockets($this->sec, $this->usec);
+        if (!$hostmasks) {
+            return;
+        }
+        $connections = $this->connections->getConnections($hostmasks);
+        foreach ($connections as $connection) {
+            $this->driver->setConnection($connection);
+            $this->plugins->setConnection($connection);
+            $this->plugins->onTick();
+
+            if ($event = $this->driver->getEvent()) {
+                $this->ui->onEvent($event, $connection);
+                $this->plugins->setEvent($event);
+                $this->plugins->preEvent();
+                $this->plugins->{'on' . ucfirst($event->getType())}();
+            }
+
+            $this->processEvents($connection);
+        }
+    }
+
+    /**
+     * Perform application tick event on all plugins and connections.
+     *
+     * @return void
+     */
+    protected function doTick()
+    {
+        foreach ($this->connections as $connection) {
+            $this->plugins->setConnection($connection);
+            $this->plugins->onTick();
+            $this->processEvents($connection);
+        }
+    }
+
+    /**
+     * Obtains and processes incoming events, then sends resulting outgoing
+     * events.
+     *
+     * @return void
+     */
+    public function handleEvents()
+    {
+        $time = time();
+        if ($this->lastTick == 0 || ($this->lastTick + $this->wait <= $time)) {
+            $this->doTick();
+            $this->lastTick = $time;
+        }
+        $this->handleEventsAsync();
+    }
+}
diff --git a/plugins/Irc/extlib/phergie/Phergie/Process/Exception.php b/plugins/Irc/extlib/phergie/Phergie/Process/Exception.php
new file mode 100755 (executable)
index 0000000..f964443
--- /dev/null
@@ -0,0 +1,33 @@
+<?php
+/**
+ * Phergie 
+ *
+ * PHP version 5
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.
+ * It is also available through the world-wide-web at this URL:
+ * http://phergie.org/license
+ *
+ * @category  Phergie 
+ * @package   Phergie
+ * @author    Phergie Development Team <team@phergie.org>
+ * @copyright 2008-2010 Phergie Development Team (http://phergie.org)
+ * @license   http://phergie.org/license New BSD License
+ * @link      http://pear.phergie.org/package/Phergie
+ */
+
+/**
+ * Exception related to event processor operations.
+ *
+ * @category Phergie 
+ * @package  Phergie
+ * @author   Phergie Development Team <team@phergie.org>
+ * @license  http://phergie.org/license New BSD License
+ * @link     http://pear.phergie.org/package/Phergie
+ */
+class Phergie_Process_Exception extends Phergie_Exception
+{
+}
diff --git a/plugins/Irc/extlib/phergie/Phergie/Process/Standard.php b/plugins/Irc/extlib/phergie/Phergie/Process/Standard.php
new file mode 100644 (file)
index 0000000..24adbaf
--- /dev/null
@@ -0,0 +1,57 @@
+<?php
+/**
+ * Phergie
+ *
+ * PHP version 5
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.
+ * It is also available through the world-wide-web at this URL:
+ * http://phergie.org/license
+ *
+ * @category  Phergie
+ * @package   Phergie
+ * @author    Phergie Development Team <team@phergie.org>
+ * @copyright 2008-2010 Phergie Development Team (http://phergie.org)
+ * @license   http://phergie.org/license New BSD License
+ * @link      http://pear.phergie.org/package/Phergie
+ */
+
+/**
+ * Connection data processor which reads all connections looking
+ * for a response.
+ *
+ * @category Phergie
+ * @package  Phergie
+ * @author   Phergie Development Team <team@phergie.org>
+ * @license  http://phergie.org/license New BSD License
+ * @link     http://pear.phergie.org/package/Phergie
+ */
+class Phergie_Process_Standard extends Phergie_Process_Abstract
+{
+    /**
+     * Obtains and processes incoming events, then sends resulting outgoing
+     * events.
+     *
+     * @return void
+     */
+    public function handleEvents()
+    {
+        foreach ($this->connections as $connection) {
+            $this->driver->setConnection($connection);
+            $this->plugins->setConnection($connection);
+            $this->plugins->onTick();
+
+            if ($event = $this->driver->getEvent()) {
+                $this->ui->onEvent($event, $connection);
+                $this->plugins->setEvent($event);
+                $this->plugins->preEvent();
+                $this->plugins->{'on' . ucfirst($event->getType())}();
+            }
+
+            $this->processEvents($connection);
+        }
+    }
+}
diff --git a/plugins/Irc/extlib/phergie/Phergie/StatusnetBot.php b/plugins/Irc/extlib/phergie/Phergie/StatusnetBot.php
new file mode 100644 (file)
index 0000000..9c1b3a6
--- /dev/null
@@ -0,0 +1,98 @@
+<?php\r
+/**\r
+ * StatusNet - the distributed open-source microblogging tool\r
+ *\r
+ * This program is free software: you can redistribute it and/or modify\r
+ * it under the terms of the GNU Affero General Public License as published by\r
+ * the Free Software Foundation, either version 3 of the License, or\r
+ * (at your option) any later version.\r
+ *\r
+ * This program is distributed in the hope that it will be useful,\r
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of\r
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\r
+ * GNU Affero General Public License for more details.\r
+ *\r
+ * You should have received a copy of the GNU Affero General Public License\r
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.\r
+ *\r
+ * Extends the bot class (Phergie_Bot) to allow connection and access to\r
+ * sockets and to allow StatusNet to 'drive' the bot\r
+ *\r
+ * @category  Phergie\r
+ * @package   Phergie_StatusnetBot\r
+ * @author    Luke Fitzgerald <lw.fitzgerald@googlemail.com>\r
+ * @copyright 2010 StatusNet, Inc.\r
+ * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0\r
+ * @link      http://status.net/\r
+ */\r
+class Phergie_StatusnetBot extends Phergie_Bot {\r
+    /**\r
+    * Set up bot and connect to servers\r
+    *\r
+    * @return void\r
+    */\r
+    public function connect() {\r
+        $ui = $this->getUi();\r
+        $ui->setEnabled($this->getConfig('ui.enabled'));\r
+\r
+        $this->loadPlugins();\r
+        $this->loadConnections();\r
+    }\r
+\r
+    /**\r
+    * Transmit raw command to server using driver\r
+    *\r
+    * Handles construction of command strings and their transmission to the\r
+    * server.\r
+    *\r
+    * @param string       $command Command to send\r
+    * @param string|array $args    Optional string or array of sequential\r
+    *        arguments\r
+    *\r
+    * @return string Command string that was sent\r
+    * @throws Phergie_Driver_Exception\r
+    */\r
+    public function send($command, $args = '') {\r
+        return $this->getDriver()->send($command, $args);\r
+    }\r
+\r
+    /**\r
+    * Handle incoming data on the socket using the handleEvents\r
+    * method of the Processor\r
+    *\r
+    * @return void\r
+    */\r
+    public function handleEvents() {\r
+        $this->getProcessor()->handleEvents();\r
+    }\r
+\r
+    /**\r
+    * Close the current connection and reconnect to the server\r
+    *\r
+    * @return void\r
+    */\r
+    public function reconnect() {\r
+        $driver = $this->getDriver();\r
+        $sockets = $driver->getSockets();\r
+\r
+        // Close any existing connections\r
+        try {\r
+            $driver->forceQuit();\r
+        } catch (Phergie_Driver_Exception $e){}\r
+        try {\r
+            $driver->doConnect();\r
+        } catch (Phergie_Driver_Exception $e){\r
+            $driver->forceQuit();\r
+            throw $e;\r
+        }\r
+    }\r
+\r
+    /**\r
+    * Get the sockets used by the bot\r
+    *\r
+    * @return array Array of socket resources\r
+    */\r
+    public function getSockets() {\r
+        return $this->getDriver()->getSockets();\r
+    }\r
+}
\ No newline at end of file
diff --git a/plugins/Irc/extlib/phergie/Phergie/Tools/LogViewer/INSTALL b/plugins/Irc/extlib/phergie/Phergie/Tools/LogViewer/INSTALL
new file mode 100755 (executable)
index 0000000..dfc9857
--- /dev/null
@@ -0,0 +1,24 @@
+-----------------------------------------------------------------------------
+-- About LogViewer
+-----------------------------------------------------------------------------
+
+The purpose of this tool is to make an HTML display of the Logs made by the 
+Logging plugin for Phergie stored in SQLite database.
+
+-----------------------------------------------------------------------------
+-- Installation
+-----------------------------------------------------------------------------
+
+To install this, simply copy the contents of this directory into whatever
+directory you want this to run under.
+
+-----------------------------------------------------------------------------
+-- Configuration
+-----------------------------------------------------------------------------
+
+The only configuration needed at this point in time is to edit the $database
+variable in the top of the index.php to point to the appropriate Phergie
+log file.
+
+Stuff may get more complicated in the future ;)  But this is meant to be 
+simple for now.
\ No newline at end of file
diff --git a/plugins/Irc/extlib/phergie/Phergie/Tools/LogViewer/index.php b/plugins/Irc/extlib/phergie/Phergie/Tools/LogViewer/index.php
new file mode 100755 (executable)
index 0000000..fc27fa2
--- /dev/null
@@ -0,0 +1,368 @@
+<?php
+// Phergie Log Viewer ... Currently designed as a single PHP file in order to make it easy to
+//  'install' this.  Just drop the index.php (or whatever name you wish to rename it to) wherever
+//  you wish, and it will simply work.  Sure, it would be nice to structure some of this stuff into
+//  various include files/etc.  But right now this is simple enough of a quick log viewer, that it's
+//  just one file.
+
+
+/********** SETUP **********/
+
+// (Change any of these if/as needed for your setup) 
+ini_set('default_charset', 'UTF-8');
+date_default_timezone_set('UTC');
+$log = "/PATH/AND/FILENAME/TO/YOUR/LOGFILE/PLEASE.db";
+
+
+/********** PREPARATION **********/
+
+$db = new PDO('sqlite:' . $log);
+if (!is_object($db)) {
+    // Failure, can't access Phergie Log.  Bail with an error message, not pretty, but works:
+    echo "ERROR: Cannot access Phergie Log File, please check the configuration & access privileges";
+    exit();
+}
+
+
+/********** DETECTION **********/
+
+// Determine the mode of the application and call the appropriate handler function
+$mode = empty($_GET['m']) ? '' : $_GET['m'];
+switch ($mode) {
+    case 'channel':
+        show_days($db);
+        break;
+    case 'day':
+        show_log($db);
+        break;
+    default:
+        show_channels($db);
+}
+
+// Exit not really needed here, but reminds us that everything below is support functions:
+exit();
+
+
+/********** MODES **********/
+
+/**
+ * show_channels
+ *
+ * Provide a list of all channel's that we are logging information for:
+ *
+ * @param PDO A PDO object referring to the database 
+ * @return void
+ * @author Eli White <eli@eliw.com>
+ **/
+function show_channels(PDO $db) {
+    // Begin the HTML page:
+    template_header('Channels');
+    echo "\nChannels:\n<ul>\n";
+
+    // Loop through the database reading in each channel, and echoing out a <li> for it.
+    // only grab actual channels that start with # ... also pre-lowercase everything.
+    // this allows us to 'deal' with variable caps in how the channels were logged.
+    $channels = $db->query("
+        select distinct lower(chan) as c
+        from logs
+        where chan like '#%'
+        ");
+    foreach ($channels as $row) {
+        $html = utf8specialchars($row['c']);
+        $url = urlencode($row['c']);
+        echo "<li><a href=\"?m=channel&w={$url}\">{$html}</a></li>\n";        
+    }
+
+    // Finish off the page:
+    echo "\n</ul>\n";
+    template_footer();
+}
+
+/**
+ * show_days
+ *
+ * Create a calendar view of all days available for this particular channel
+ * 
+ * NOTE: May get unwieldy if large log files.  Perhaps consider in the future
+ *  making a paginated version of this?  by year?  Or a separate 'which year' page
+ *  before this?  Not to worry about now.
+ *
+ * @param PDO A PDO object referring to the database 
+ * @return void
+ * @author Eli White <eli@eliw.com>
+ **/
+function show_days(PDO $db) {
+    $channel = $_GET['w'];
+    $url = urlencode($channel);
+
+    // Begin the HTML page:
+    template_header('Daily Logs for Channel: ' . utf8specialchars($channel));
+    echo "\n<ul>\n";
+
+    // Query the database to discover all days that are available for this channel:
+    $data = array();
+    $prepared = $db->prepare("
+        select distinct date(tstamp) as day
+        from logs
+        where lower(chan) = :chan
+        ");
+    $prepared->execute(array(':chan' => $channel));
+    foreach ($prepared as $row) {
+        list($y, $m, $d) = explode('-', $row['day']);
+        $data[$y][$m][$d] = "{$y}-{$m}-{$d}";
+    }
+
+    // For now, just loop over them all and provide a list:
+    ksort($data);
+    foreach ($data as $year => $months) {
+        ksort($months);
+        foreach ($months as $month => $days) {
+            // Figure out a few facts about this month:
+            $stamp = mktime(0, 0, 0, $month, 1, $year);
+            $first_weekday = idate('w', $stamp);
+            $days_in_month = idate('t', $stamp);
+            $name = date('F', $stamp);
+
+            // We have a month ... start a new table:
+            echo <<<EOTABLE
+<div class="month">
+  <table>
+    <caption>{$name} {$year}</caption>
+    <tr><th>Sun</th><th>Mon</th><th>Tue</th><th>Wed</th><th>Thu</th><th>Fri</th><th>Sat</th></tr>
+EOTABLE;
+            // Now we need to start looping through the days in this month:
+            echo '<tr>';
+            $rowmod = 0;
+            // Loop through all day entries, no matter how many blanks we need:
+            for ($d = (-$first_weekday + 1); $d < $days_in_month + 1; $d++) {
+                if (!($rowmod++ % 7)) {
+                    // Stop/start a new row:
+                    echo '</tr><tr>';
+                }
+                echo '<td>';
+                // If this day is pre or post actual month days, make it blank:
+                if (($d < 1) || ($d > $days_in_month)) {
+                    echo '&nbsp;';
+                } elseif (isset($days[$d])) {
+                    // Make a link to the day's log:
+                    echo "<a href=\"?m=day&w={$url}&d={$days[$d]}\">{$d}</a>";            
+                } else {
+                    // Just a dead number:
+                    echo $d;
+                }
+                echo '</td>';
+            }
+            // Finish off any blanks needed for a complete table row:
+            while ($rowmod++ % 7) {
+                echo '<td>&nbsp;</td>';
+            }
+            echo "</tr></table></div>\n";
+        }
+    }
+
+    // Finish off the page:
+    echo "\n</ul>\n";
+    template_footer();    
+}
+
+/**
+ * show_log
+ *
+ * Actually show the log for this specific day
+ *
+ * @param PDO A PDO object referring to the database 
+ * @return void
+ * @author Eli White <eli@eliw.com>
+ **/
+function show_log(PDO $db) {
+    $channel = $_GET['w'];
+    $day = $_GET['d'];
+    $parts = explode('-', $day);
+    $formatted_date = "{$parts[0]}-{$parts[1]}-{$parts[2]}";
+
+    // Begin the HTML page:
+    template_header('Date: ' . utf8specialchars($formatted_date) .
+        ' - Channel: ' . utf8specialchars($channel));
+
+    // Query the database to get all log lines for this date:
+    $prepared = $db->prepare("
+        select time(tstamp) as t, type, nick, message
+        from logs
+        where lower(chan) = :chan and date(tstamp) = :day
+        order by tstamp asc
+        ");
+    $prepared->execute(array(
+        ':chan' => $channel,
+        ':day' => $day,
+        ));
+
+    // Loop through each line,
+    foreach ($prepared as $row) {
+        // Prepare some basic details for output:
+        $color = nick_color($row['nick']);
+        $time = utf8specialchars($row['t']);
+        $msg = utf8specialchars($row['message']);
+        $nick = utf8specialchars($row['nick']);
+        $type = false;
+        
+        // Now change the format of the line based upon the type:
+        switch ($row['type']) {
+            case 4: // PRIVMSG (A Regular Message)
+                echo "[$time] <span style=\"color:#{$color};\">&lt;{$nick}&gt;</span> {$msg}<br />\n";
+                break;
+            case 5: // ACTION (emote)
+                echo "[$time] <span style=\"color:#{$color};\">*{$nick} {$msg}</span><br />\n";
+                break;
+            case 1: // JOIN
+                echo "[$time] -> {$nick} joined the room.<br />\n";
+                break;
+            case 2: // PART (leaves channel)
+                echo "[$time] -> {$nick} left the room: {$msg}<br />\n";
+                break;
+            case 3: // QUIT (quits the server)
+                echo "[$time] -> {$nick} left the server: {$msg}<br />\n";
+                break;
+            case 6: // NICK (changes their nickname)
+                echo "[$time] -> {$nick} is now known as: {$msg}<br />\n";
+                break;
+            case 7: // KICK (booted)
+                echo "[$time] -> {$nick} boots {$msg} from the room.<br />\n";
+                break;
+            case 8: // MODE (changed their mode)
+                $type = 'MODE';
+            case 9: // TOPIC (changed the topic)
+                $type = $type ? $type : 'TOPIC';
+                echo "[$time] -> {$nick}: :{$type}: {$msg}<br />\n";
+        }
+    }
+        
+    // Finish up the page:
+    template_footer();
+}
+
+/**
+ * nick_color
+ *
+ * Uses a silly little algorithm to pick a consistent but unique(ish) color for
+ *  any given username.  NOTE: Augment this in the future to make it not generate
+ *  'close to white' ones, also maybe to ensure uniqueness?  (Not allow two to have
+ *  colors that are close to each other?)
+ *
+ * @return string A CSS valid hex color string
+ * @author Eli White <eli@eliw.com>
+ **/
+function nick_color($user) {
+    static $colors = array();
+    
+    if (!isset($colors[$user])) {
+        $colors[$user] = substr(md5($user), 0, 6);
+    }
+
+    return $colors[$user];
+}
+
+/**
+ * utf8specialchars
+ *
+ * Just a quick wrapper around htmlspecialchars
+ *
+ * @param string The UTF8 string to escape
+ * @return string An escaped and ready for HTML use string
+ * @author Eli White <eli@eliw.com>
+ **/
+function utf8specialchars($string) {
+    return htmlspecialchars($string, ENT_COMPAT, 'UTF-8');
+}
+
+
+/********** TEMPLATES **********/
+
+/**
+ * template_header
+ *
+ * Echo out the header for each HTML page
+ *
+ * @param $title string The title to be used for this page.
+ * @return void
+ * @author Eli White <eli@eliw.com>
+ **/
+function template_header($title) {
+    $css = template_css();
+    echo <<<EOHTML
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" 
+  "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html lang="en" xmlns="http://www.w3.org/1999/xhtml">
+  <head>
+    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
+    <title>Phergie LogViewer - {$title}</title>
+    <style type="text/css" media="all">{$css}</style>
+  </head>
+  <body>
+    <h2>Phergie LogViewer - {$title}</h2>
+EOHTML;
+}
+
+/**
+ * template_footer
+ *
+ * Echo out the bottom of each HTML page
+ *
+ * @return void
+ * @author Eli White <eli@eliw.com>
+ **/
+function template_footer() {
+    echo <<<EOHTML
+  </body>
+</html>
+EOHTML;
+}
+
+/**
+ * template_css
+ *
+ * Generate the CSS used by these HTML pages & return it.
+ *
+ * @return string The CSS in question:
+ * @author Eli White <eli@eliw.com>
+ **/
+function template_css() {
+    return <<<EOCSS
+    div.month {
+        float: left;
+        height: 15em;
+    }
+
+    div.month table {
+        border-collapse: collapse;
+        border: 2px solid black;
+        margin-right: 2em;
+    }
+
+    div.month td, div.month th {
+        text-align: center;
+        vertical-align: bottom;
+        border: 1px solid gray;
+        width: 2em;
+        height: 1.7em;
+        padding: 1px;
+        margin: 0px;
+    }
+
+    div.month th {
+        text-decoration: bold;
+        border: 2px solid black;
+    }
+
+    div.month a {
+        text-decoration: none;
+    }
+
+    a:visited, a:link {
+        color: blue;
+    }
+
+    a:active, a:hover {
+        color: red;
+    }
+EOCSS;
+}
diff --git a/plugins/Irc/extlib/phergie/Phergie/Tools/README b/plugins/Irc/extlib/phergie/Phergie/Tools/README
new file mode 100755 (executable)
index 0000000..ed8fe29
--- /dev/null
@@ -0,0 +1,6 @@
+This directory holds any assorted tools that work with Phergie but that are
+not part of Phergie itself.  Such as tools to examine data that Phergie is
+capturing/logging.
+
+Each should live inside of it's own directory, and be completely self-contained
+with instructions inside on how to use it.
diff --git a/plugins/Irc/extlib/phergie/Phergie/Ui/Abstract.php b/plugins/Irc/extlib/phergie/Phergie/Ui/Abstract.php
new file mode 100644 (file)
index 0000000..e2e2ce2
--- /dev/null
@@ -0,0 +1,116 @@
+<?php
+/**
+ * Phergie 
+ *
+ * PHP version 5
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.
+ * It is also available through the world-wide-web at this URL:
+ * http://phergie.org/license
+ *
+ * @category  Phergie 
+ * @package   Phergie
+ * @author    Phergie Development Team <team@phergie.org>
+ * @copyright 2008-2010 Phergie Development Team (http://phergie.org)
+ * @license   http://phergie.org/license New BSD License
+ * @link      http://pear.phergie.org/package/Phergie
+ */
+
+/**
+ * Base class for end-user interfaces.
+ *
+ * @category Phergie 
+ * @package  Phergie
+ * @author   Phergie Development Team <team@phergie.org>
+ * @license  http://phergie.org/license New BSD License
+ * @link     http://pear.phergie.org/package/Phergie
+ */
+abstract class Phergie_Ui_Abstract
+{
+    /**
+     * Handler for when a server connection is attempted.
+     *
+     * @param string $host Server hostname
+     *
+     * @return void 
+     */
+    public function onConnect($host)
+    {
+    }
+
+    /**
+     * Handler for when an attempt is made to load a plugin.
+     *
+     * @param string $plugin Short name of the plugin
+     *
+     * @return void 
+     */
+    public function onPluginLoad($plugin)
+    {
+    }
+
+    /**
+     * Handler for when a plugin fails to load.
+     *
+     * @param string $plugin  Short name of the plugin
+     * @param string $message Message describing the reason for the failure
+     *
+     * @return void 
+     */
+    public function onPluginFailure($plugin, $message)
+    {
+    }
+
+    /**
+     * Handler for when the bot receives an IRC event. 
+     *
+     * @param Phergie_Event_Abstract $event      Received event
+     * @param Phergie_Connection     $connection Connection on which the event 
+     *        was received
+     *
+     * @return void
+     */
+    public function onEvent(Phergie_Event_Abstract $event, 
+        Phergie_Connection $connection
+    ) {
+    }
+
+    /**
+     * Handler for when the bot sends a command to a server.
+     *
+     * @param Phergie_Event_Command $event      Event representing the command 
+     *        being sent
+     * @param Phergie_Connection    $connection Connection on which the command  
+     *        is being sent 
+     *
+     * @return void
+     */
+    public function onCommand(Phergie_Event_Command $event, 
+        Phergie_Connection $connection
+    ) {
+    }
+
+    /**
+     * Handler for when the bot terminates a connection to a server.
+     *
+     * @param Phergie_Connection $connection Terminated connection 
+     *
+     * @return void
+     */
+    public function onQuit(Phergie_Connection $connection)
+    {
+    }
+
+    /**
+     * Handler for when the bot shuts down after terminating all server 
+     * connections.
+     *
+     * @return void
+     */
+    public function onShutdown()
+    {
+    }
+}
diff --git a/plugins/Irc/extlib/phergie/Phergie/Ui/Console.php b/plugins/Irc/extlib/phergie/Phergie/Ui/Console.php
new file mode 100644 (file)
index 0000000..a0a528b
--- /dev/null
@@ -0,0 +1,223 @@
+<?php
+/**
+ * Phergie 
+ *
+ * PHP version 5
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.
+ * It is also available through the world-wide-web at this URL:
+ * http://phergie.org/license
+ *
+ * @category  Phergie 
+ * @package   Phergie
+ * @author    Phergie Development Team <team@phergie.org>
+ * @copyright 2008-2010 Phergie Development Team (http://phergie.org)
+ * @license   http://phergie.org/license New BSD License
+ * @link      http://pear.phergie.org/package/Phergie
+ */
+
+/**
+ * End-user interface that produces console output when running the bot from 
+ * a shell.
+ *
+ * @category Phergie 
+ * @package  Phergie
+ * @author   Phergie Development Team <team@phergie.org>
+ * @license  http://phergie.org/license New BSD License
+ * @link     http://pear.phergie.org/package/Phergie
+ */
+class Phergie_Ui_Console extends Phergie_Ui_Abstract
+{
+    /**
+     * Flag that toggles all console output
+     *
+     * @var bool
+     */
+    protected $enabled;
+
+    /**
+     * Format for timestamps included in console output
+     *
+     * @var string
+     * @link http://php.net/date
+     */
+    protected $format;
+
+    /**
+     * Constructor to initialize object properties.
+     *
+     * @return void
+     */
+    public function __construct()
+    {
+        $this->enabled = true;
+        $this->format = 'H:i:s';
+    }
+
+    /** 
+     * Outputs a timestamped line to the console if console output is enabled.
+     *
+     * @param string $line Line to output
+     *
+     * @return void
+     */
+    protected function console($line)
+    {
+        if ($this->enabled) {
+            echo date($this->format), ' ', $line, PHP_EOL;
+        }
+    }
+
+    /**
+     * Returns whether console output is enabled.
+     *
+     * @return bool TRUE if console output is enabled, FALSE otherwise
+     */
+    public function isEnabled()
+    {
+        return $this->enabled;
+    }
+
+    /**
+     * Sets whether console output is enabled.
+     *
+     * @param bool $enabled TRUE to enable console output, FALSE otherwise, 
+     *        defaults to TRUE
+     *
+     * @return Phergie_Ui_Console Provides a fluent interface
+     */
+    public function setEnabled($enabled = true)
+    {
+        $this->enabled = (bool) $enabled;
+        return $this;
+    }
+
+    /**
+     * Returns the format used for timestamps in console output.
+     *
+     * @return string
+     * @link http://php.net/date
+     */
+    public function getFormat()
+    {
+        return $this->format;
+    }
+
+    /**
+     * Sets the format used for timestamps in console output, overwriting 
+     * any previous format used.
+     *
+     * @param string $format Timestamp format
+     *
+     * @return Phergie_Ui_Console Provides a fluent interface
+     * @link http://php.net/date
+     */
+    public function setFormat($format)
+    {
+        $this->format = (string) $format;
+        return $this;
+    }
+
+    /**
+     * Outputs a prompt when a server connection is attempted.
+     *
+     * @param string $host Server hostname
+     *
+     * @return void 
+     */
+    public function onConnect($host)
+    {
+        $this->console('Connecting to ' . $host);
+    }
+
+    /**
+     * Outputs a prompt when a plugin is loaded successfully. 
+     *
+     * @param string $plugin Short name of the plugin
+     *
+     * @return void 
+     */
+    public function onPluginLoad($plugin)
+    {
+        $this->console('Loaded plugin ' . $plugin);
+    }
+
+    /**
+     * Outputs a prompt when a plugin fails to load.
+     *
+     * @param string $plugin  Short name of the plugin
+     * @param string $message Message describing the reason for the failure
+     *
+     * @return void 
+     */
+    public function onPluginFailure($plugin, $message)
+    {
+        $this->console('Unable to load plugin ' . $plugin . ' - ' . $message);
+    }
+
+    /**
+     * Outputs a prompt when the bot receives an IRC event. 
+     *
+     * @param Phergie_Event_Abstract $event      Received event
+     * @param Phergie_Connection     $connection Connection on which the 
+     *        event was received
+     *
+     * @return void
+     */
+    public function onEvent(Phergie_Event_Abstract $event, 
+        Phergie_Connection $connection
+    ) {
+        $host = $connection->getHostmask()->getHost();
+        $this->console($host . ' <- ' . $event->getRawData());
+    }
+
+    /**
+     * Outputs a prompt when the bot sends a command to a server.
+     *
+     * @param Phergie_Event_Command $event      Event representing the 
+     *        command being sent
+     * @param Phergie_Connection    $connection Connection on which the 
+     *        command is being sent 
+     *
+     * @return void
+     */
+    public function onCommand(Phergie_Event_Command $event, 
+        Phergie_Connection $connection
+    ) {
+        $plugin = $event->getPlugin()->getName();
+        $host = $connection->getHostmask()->getHost();
+        $type = strtoupper($event->getType());
+        $args = implode(' ', $event->getArguments());
+        $this->console(
+            $plugin . ' plugin: ' . 
+            $host . ' -> ' . $type . ' ' . $args
+        ); 
+    }
+
+    /**
+     * Outputs a prompt when the bot terminates a connection to a server.
+     *
+     * @param Phergie_Connection $connection Terminated connection 
+     *
+     * @return void
+     */
+    public function onQuit(Phergie_Connection $connection)
+    {
+        $host = $connection->getHostmask()->getHost();
+        $this->console('Disconnecting from ' . $host);
+    }
+
+    /**
+     * Outputs a prompt when the bot shuts down after terminating all server 
+     * connections.
+     *
+     * @return void
+     */
+    public function onShutdown()
+    {
+        $this->console('Shutting down');
+    }
+}
diff --git a/plugins/Irc/extlib/phergie/PhergiePackageTask.php b/plugins/Irc/extlib/phergie/PhergiePackageTask.php
new file mode 100644 (file)
index 0000000..dfc760a
--- /dev/null
@@ -0,0 +1,110 @@
+<?php
+
+require_once 'phing/tasks/ext/PearPackage2Task.php';
+
+class PhergiePackageTask extends PearPackage2Task
+{
+    protected function setOptions()
+    {
+        $this->pkg->addMaintainer('lead', 'team', 'Phergie Development Team', 'team@phergie.org');
+
+        $path = str_replace('_', '/', $this->package) . '.php'; 
+        if (file_exists($path)) {
+            $contents = file_get_contents($path);
+            preg_match_all('#/\*\*(.*)\*/#Ums', $contents, $matches, PREG_SET_ORDER);
+            $doc = $matches[1][1];
+
+            $have_summary = false;
+            $have_description = false;
+            foreach ($this->options as $option) {
+                switch ($option->getName()) {
+                    case 'summary':
+                        $have_summary = true;
+                        break;
+                    case 'description':
+                        $have_descripion = true;
+                        break;
+                }
+            }
+
+            if (!$have_summary || !$have_description) {
+                $description = substr($doc, 0, strpos($doc, '@'));
+                $description = trim(preg_replace(array('#^[\h*]*|[\h*]*$#m', '#[\h]+#m'), array('', ' '), $description));
+                $split = preg_split('/\v\v+/', $description);
+                $summary = trim(array_shift($split));
+                if (!$have_summary) {
+                    $this->pkg->setSummary(htmlentities($summary, ENT_QUOTES));
+                }
+                if (!$have_description) {
+                    $this->pkg->setDescription(htmlentities($description, ENT_QUOTES));
+                }
+            }
+
+            $doc = preg_split('/\v+/', $doc);
+            $doc = preg_grep('/@uses/', $doc);
+            $doc = preg_replace('/\s*\* @uses\s+|\s+$/', '', $doc);
+            foreach ($doc as $line) {
+                if (strpos($line, 'extension') === 0) {
+                    $line = explode(' ', $line);
+                    $name = $line[1];
+                    $optional = 'required';
+                    if (isset($line[2])) {
+                        $optional = $line[2];
+                    }
+                    $this->pkg->addExtensionDep(
+                        $optional,
+                        $name
+                    );
+                } else {
+                    $line = explode(' ', $line);
+                    $name = $line[0];
+                    $channel = $line[1];
+                    $optional = 'required';
+                    if (isset($line[2])) {
+                        $optional = $line[2];
+                    }
+                    $this->pkg->addPackageDepWithChannel(
+                        $optional,
+                        $name,
+                        $channel
+                    );
+                }
+            }
+        }
+
+        $newmap = array();
+        foreach ($this->mappings as $key => $map) {
+            switch ($map->getName()) {
+                case 'releases':
+                    $releases = $map->getValue();
+                    foreach ($releases as $release) {
+                        $this->pkg->addRelease();
+                        if (isset($release['installconditions'])) {
+                            if (isset($release['installconditions']['os'])) {
+                                $this->pkg->setOsInstallCondition($release['installconditions']['os']);
+                            }
+                        }
+                        if (isset($release['filelist'])) {
+                            if (isset($release['filelist']['install'])) {
+                                foreach ($release['filelist']['install'] as $file => $as) {
+                                    $this->pkg->addInstallAs($file, $as);
+                                }
+                            }
+                            if (isset($release['filelist']['ignore'])) {
+                                foreach ($release['filelist']['ignore'] as $file) {
+                                    $this->pkg->addIgnoreToRelease($file);
+                                }
+                            }
+                        }
+                    }
+                    break;
+
+                default:
+                    $newmap[] = $map;
+            }
+        }
+        $this->mappings = $newmap;
+
+        parent::setOptions();
+    }
+}
diff --git a/plugins/Irc/extlib/phergie/README b/plugins/Irc/extlib/phergie/README
new file mode 100644 (file)
index 0000000..d97ce05
--- /dev/null
@@ -0,0 +1,11 @@
+Phergie is an IRC bot written for PHP 5.2.
+
+Main project web site: http://phergie.org
+
+Instructions for running your own instance of Phergie: http://phergie.org/users/
+
+Architectural overview for plugin developers: http://phergie.org/developers/
+
+Support: http://phergie.org/support/
+
+Bug reports/feature requests: http://github.com/elazar/phergie/issues
diff --git a/plugins/Irc/extlib/phergie/Settings.php.dist b/plugins/Irc/extlib/phergie/Settings.php.dist
new file mode 100755 (executable)
index 0000000..87b4a95
--- /dev/null
@@ -0,0 +1,98 @@
+<?php
+
+return array(
+
+    // One array per connection, pretty self-explanatory
+    'connections' => array(
+        // Ex: All connection info for the Freenode network
+        array(
+            'host' => 'irc.freenode.net',
+            'port' => 6667,
+            'username' => 'Elazar',
+            'realname' => 'Matthew Turland',
+            'nick' => 'Phergie2',
+            // 'password' => 'password goes here if needed',
+            // 'transport' => 'ssl', // uncomment to connect using SSL
+            // 'encoding' => 'UTF8', // uncomment if using UTF8
+        )
+    ),
+
+    'processor' => 'async',
+    'processor.options' => array('usec' => 200000),
+    // Time zone. See: http://www.php.net/manual/en/timezones.php
+    'timezone' => 'UTC',
+
+    // Whitelist of plugins to load
+    'plugins' => array(
+        // To enable a plugin, simply add a string to this array containing
+        // the short name of the plugin as shown below.
+
+        // 'ShortPluginName',
+
+        // Below is an example of enabling the AutoJoin plugin, for which
+        // the corresponding PEAR package is Phergie_Plugin_AutoJoin. This
+        // plugin allows you to set a list of channels in this configuration
+        // file that the bot will automatically join when it connects to a
+        // server. If you'd like to enable this plugin, simply install it,
+        // uncomment the line below, and set a value for the setting
+        // autojoin.channels (examples for which are located further down in
+        // this file).
+
+        // 'AutoJoin',
+
+        // A few other recommended plugins:
+
+        // Servers randomly send PING events to clients to ensure that
+        // they're still connected and will eventually terminate the
+
+        // connection if a PONG response is not received. The Pong plugin
+        // handles sending these responses.
+
+        // 'Pong',
+
+        // It's sometimes difficult to distinguish between a lack of
+        // activity on a server and the client not receiving data even
+        // though a connection remains open. The Ping plugin performs a self
+        // CTCP PING sporadically to ensure that its connection is still
+        // functioning and, if not, terminates the bot.
+
+        // 'Ping',
+
+        // Sometimes it's desirable to have the bot disconnect gracefully
+        // when issued a command to do so via a PRIVMSG event. The Quit
+        // plugin implements this using the Command plugin to intercept the
+        // command.
+
+        // 'Quit',
+    ),
+
+    // If set to true, this allows any plugin dependencies for plugins
+    // listed in the 'plugins' option to be loaded even if they are not
+    // explicitly included in that list
+    'plugins.autoload' => true,
+
+    // Enables shell output describing bot events via Phergie_Ui_Console
+    'ui.enabled' => true,
+
+    // Examples of supported values for autojoins.channel:
+    // 'autojoin.channels' => '#channel1,#channel2',
+    // 'autojoin.channels' => array('#channel1', '#channel2'),
+    // 'autojoin.channels' => array(
+    //                            'host1' => '#channel1,#channel2',
+    //                            'host2' => array('#channel3', '#channel4')
+    //                        ),
+
+    // Examples of setting values for Ping plugin settings
+
+    // This is the amount of time in seconds that the Ping plugin will wait
+    // to receive an event from the server before it initiates a self-ping
+
+    // 'ping.event' => 300, // 5 minutes
+
+    // This is the amount of time in seconds that the Ping plugin will wait
+    // following a self-ping attempt before it assumes that a response will
+    // never be received and terminates the connection
+
+    // 'ping.ping' => 10, // 10 seconds
+
+);
diff --git a/plugins/Irc/extlib/phergie/Tests/Phergie/Plugin/HandlerTest.php b/plugins/Irc/extlib/phergie/Tests/Phergie/Plugin/HandlerTest.php
new file mode 100644 (file)
index 0000000..dcf52a6
--- /dev/null
@@ -0,0 +1,736 @@
+<?php
+/**
+ * Phergie
+ *
+ * PHP version 5
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.
+ * It is also available through the world-wide-web at this URL:
+ * http://phergie.org/license
+ *
+ * @category  Phergie
+ * @package   Phergie_Tests
+ * @author    Phergie Development Team <team@phergie.org>
+ * @copyright 2008-2010 Phergie Development Team (http://phergie.org)
+ * @license   http://phergie.org/license New BSD License
+ * @link      http://pear.phergie.org/package/Phergie_Tests
+ */
+
+/**
+ * Unit test suite for Pherge_Plugin_Handler.
+ *
+ * @category Phergie
+ * @package  Phergie_Tests
+ * @author   Phergie Development Team <team@phergie.org>
+ * @license  http://phergie.org/license New BSD License
+ * @link     http://pear.phergie.org/package/Phergie_Tests
+ */
+class Phergie_Plugin_HandlerTest extends PHPUnit_Framework_TestCase
+{
+    /**
+     * Plugin handler instance being tested
+     *
+     * @var Phergie_Plugin_Handler
+     */
+    protected $handler;
+
+    /**
+     * Mock Phergie_Config instance passed to the plugin handler constructor
+     *
+     * @var Phergie_Config
+     */
+    protected $config;
+
+    /**
+     * Mock Phergie_Event_Handler instance passed to the plugin handler
+     * constructor
+     *
+     * @var Phergie_Event_Handler
+     */
+    protected $events;
+
+    /**
+     * Returns a mock plugin instance.
+     *
+     * @param string $name    Optional short name for the mock plugin, defaults
+     *        to 'TestPlugin'
+     * @param array  $methods Optional list of methods to override
+     *
+     * @return Phergie_Plugin_Abstract
+     */
+    protected function getMockPlugin($name = 'TestPlugin', array $methods = array())
+    {
+        $methods[] = 'getName';
+        $plugin = $this->getMock('Phergie_Plugin_Abstract', $methods);
+        $plugin
+            ->expects($this->any())
+            ->method('getName')
+            ->will($this->returnValue($name));
+        return $plugin;
+    }
+
+    /**
+     * Sets up a new handler instance before each test.
+     *
+     * @return void
+     */
+    public function setUp()
+    {
+        $this->config = $this->getMock('Phergie_Config');
+        $this->events = $this->getMock('Phergie_Event_Handler');
+        $this->handler = new Phergie_Plugin_Handler(
+            $this->config,
+            $this->events
+        );
+    }
+
+    /**
+     * Tests iterability of the plugin handler.
+     *
+     * @return void
+     */
+    public function testImplementsIteratorAggregate()
+    {
+        $reflection = new ReflectionObject($this->handler);
+
+        $this->assertTrue(
+            $reflection->implementsInterface('IteratorAggregate'),
+            'Handler does not implement IteratorAggregate'
+        );
+
+        $this->assertType(
+            'Iterator',
+            $this->handler->getIterator(),
+            'getIterator() must return an iterator'
+        );
+    }
+
+    /**
+     * Tests countability of the plugin handler.
+     *
+     * @return void
+     */
+    public function testImplementsCountable()
+    {
+        $reflection = new ReflectionObject($this->handler);
+
+        $this->assertTrue(
+            $reflection->implementsInterface('Countable'),
+            'Handler does not implement Countable'
+        );
+
+        $this->assertType(
+            'int',
+            count($this->handler),
+            'count() must return an integer'
+        );
+    }
+
+    /**
+     * Tests the plugin handler exposing added plugins as instance
+     * properties of the handler via isset().
+     *
+     * @return void
+     */
+    public function testImplementsIsset()
+    {
+        $pluginName = 'TestPlugin';
+        $this->assertFalse(isset($this->handler->{$pluginName}));
+        $plugin = $this->getMockPlugin($pluginName);
+        $this->handler->addPlugin($plugin);
+        $this->assertTrue(isset($this->handler->{$pluginName}));
+    }
+
+    /**
+     * Tests the plugin handler exposing added plugins as instance
+     * properties of the handler.
+     *
+     * @depends testImplementsIsset
+     * @return void
+     */
+    public function testImplementsGet()
+    {
+        $plugin = $this->getMockPlugin();
+        $this->handler->addPlugin($plugin);
+        $name = $plugin->getName();
+        $getPlugin = $this->handler->getPlugin($name);
+        $this->assertTrue(isset($this->handler->$name));
+        $get = $this->handler->$name;
+        $this->assertSame($getPlugin, $get);
+    }
+
+    /**
+     * Tests the plugin handler allowing for plugin removal via unset().
+     *
+     * @depends testImplementsGet
+     * @return void
+     */
+    public function testImplementsUnset()
+    {
+        $plugin = $this->getMockPlugin();
+        $this->handler->addPlugin($plugin);
+        unset($this->handler->{$plugin->getName()});
+        $this->assertFalse($this->handler->hasPlugin($plugin->getName()));
+    }
+
+    /**
+     * Tests the plugin handler executing a callback on all contained
+     * plugins.
+     *
+     * @return void
+     */
+    public function testImplementsCall()
+    {
+        foreach (range(1, 2) as $index) {
+            $plugin = $this->getMockPlugin('TestPlugin' . $index, array('callback'));
+            $plugin
+                ->expects($this->once())
+                ->method('callback');
+            $this->handler->addPlugin($plugin);
+        }
+
+        $this->assertTrue($this->handler->callback());
+    }
+
+    /**
+     * Tests a newly instantiated handler not having plugins associated with
+     * it.
+     *
+     * @depends testImplementsCountable
+     * @return void
+     */
+    public function testEmptyHandlerHasNoPlugins()
+    {
+        $this->assertEquals(0, count($this->handler));
+    }
+
+    /**
+     * Tests a newly instantiated handler not having autoloading enabled by
+     * default.
+     *
+     * @return void
+     */
+    public function testGetAutoloadDefaultsToNotAutoload()
+    {
+        $this->assertFalse($this->handler->getAutoload());
+    }
+
+    /**
+     * Tests setAutoload().
+     *
+     * @depends testGetAutoloadDefaultsToNotAutoload
+     * @return void
+     */
+    public function testSetAutoload()
+    {
+        $this->assertSame(
+            $this->handler->setAutoload(true),
+            $this->handler,
+            'setAutoload() does not provide a fluent interface'
+        );
+
+        $this->assertTrue(
+            $this->handler->getAutoload(),
+            'setAutoload() had no effect on getAutoload()'
+        );
+    }
+
+    /**
+     * Tests addPath() providing a fluent interface.
+     *
+     * @return void
+     */
+    public function testAddPathProvidesFluentInterface()
+    {
+        $handler = $this->handler->addPath(dirname(__FILE__));
+        $this->assertSame($this->handler, $handler);
+    }
+
+    /**
+     * Tests addPath() throwing an exception when it cannot read the
+     * directory.
+     *
+     * @return void
+     */
+    public function testAddPathThrowsExceptionOnUnreadableDirectory()
+    {
+        try {
+            $this->handler->addPath('/an/unreadable/directory/path');
+        } catch(Phergie_Plugin_Exception $e) {
+            $this->assertEquals(
+                Phergie_Plugin_Exception::ERR_DIRECTORY_NOT_READABLE,
+                $e->getCode()
+            );
+            return;
+        }
+
+        $this->fail('An expected exception has not been raised');
+    }
+
+    /**
+     * Tests adding a path to the plugin handler.
+     *
+     * @return void
+     */
+    public function testAddPath()
+    {
+        $pluginName = 'Mock';
+
+        try {
+            $this->handler->addPlugin($pluginName);
+        } catch(Phergie_Plugin_Exception $e) {
+            $this->assertEquals(
+                Phergie_Plugin_Exception::ERR_CLASS_NOT_FOUND,
+                $e->getCode()
+            );
+        }
+
+        if (!isset($e)) {
+            $this->fail('Plugin loaded, path was already present');
+        }
+
+        $this->handler->addPath(dirname(__FILE__), 'Phergie_Plugin_');
+
+        try {
+            $this->handler->addPlugin($pluginName);
+        } catch(Phergie_Plugin_Exception $e) {
+            $this->fail('Added path, plugin still not found');
+        }
+    }
+
+    /**
+     * Tests addPlugin() returning an added plugin instance.
+     *
+     * @return void
+     */
+    public function testAddPluginByInstanceReturnsPluginInstance()
+    {
+        $plugin = $this->getMockPlugin();
+        $returnedPlugin = $this->handler->addPlugin($plugin);
+        $this->assertSame(
+            $returnedPlugin,
+            $plugin,
+            'addPlugin() does not return the instance passed to it'
+        );
+    }
+
+    /**
+     * Tests adding a plugin to the handler using the plugin's short name.
+     *
+     * @return void
+     */
+    public function testAddPluginByShortName()
+    {
+        $pluginName = 'Mock';
+        $this->handler->addPath(dirname(__FILE__), 'Phergie_Plugin_');
+
+        $returnedPlugin = $this->handler->addPlugin($pluginName);
+        $this->assertTrue($this->handler->hasPlugin($pluginName));
+
+        $this->assertType(
+            'Phergie_Plugin_Mock',
+            $this->handler->getPlugin($pluginName)
+        );
+
+        $this->assertSame(
+            $this->handler->getPlugin($pluginName),
+            $returnedPlugin,
+            'Handler does not contain added plugin'
+        );
+    }
+
+
+    /**
+     * Tests adding a plugin instance to the handler.
+     *
+     * @return void
+     */
+    public function testAddPluginByInstance()
+    {
+        $plugin = $this->getMockPlugin();
+        $returnedPlugin = $this->handler->addPlugin($plugin);
+        $this->assertTrue($this->handler->hasPlugin('TestPlugin'));
+
+        $this->assertSame(
+            $plugin,
+            $returnedPlugin,
+            'addPlugin() does not return added plugin instance'
+        );
+
+        $this->assertSame(
+            $plugin,
+            $this->handler->getPlugin('TestPlugin'),
+            'getPlugin() does not return added plugin instance'
+        );
+    }
+
+    /**
+     * Tests addPlugin() throwing an exception when the plugin class file
+     * can't be found.
+     *
+     * @return void
+     */
+    public function testAddPluginThrowsExceptionWhenPluginFileNotFound()
+    {
+        try {
+            $this->handler->addPlugin('TestPlugin');
+        } catch(Phergie_Plugin_Exception $e) {
+            $this->assertEquals(
+                Phergie_Plugin_Exception::ERR_CLASS_NOT_FOUND,
+                $e->getCode()
+            );
+            return;
+        }
+
+        $this->fail('An expected exception has not been raised');
+    }
+
+    /**
+     * Recursively removes all files and subdirectories in a directory.
+     *
+     * @param string $path Directory path
+     * @return void
+     */
+    private function removeDirectory($path)
+    {
+        if (file_exists($path)) {
+            $it = new RecursiveIteratorIterator(
+                new RecursiveDirectoryIterator($path),
+                RecursiveIteratorIterator::CHILD_FIRST
+            );
+            foreach ($it as $entry) {
+                if ($it->isDot()) {
+                    continue;
+                }
+                if ($entry->isDir()) {
+                    rmdir($entry->getPathname());
+                } else {
+                    unlink($entry->getPathname());
+                }
+            }
+        }
+    }
+
+    /**
+     * Tests addPlugin() throwing an exception when the plugin class file is
+     * found, but does not contain the plugin class as expected.
+     *
+     * @return void
+     */
+    public function testAddPluginThrowsExceptionWhenPluginClassNotFound()
+    {
+        $path = sys_get_temp_dir() . '/Phergie/Plugin';
+        $this->removeDirectory(dirname($path));
+        mkdir($path, 0777, true);
+        touch($path . '/TestPlugin.php');
+        $this->handler->addPath($path, 'Phergie_Plugin_');
+
+        try {
+            $this->handler->addPlugin('TestPlugin');
+        } catch(Phergie_Plugin_Exception $e) { }
+
+        if (isset($e)) {
+            $this->assertEquals(
+                Phergie_Plugin_Exception::ERR_CLASS_NOT_FOUND,
+                $e->getCode()
+            );
+        } else {
+            $this->fail('An expected exception has not been raised');
+        }
+
+        $this->removeDirectory(dirname($path));
+    }
+
+    /**
+     * Tests addPlugin() throwing an exception when trying to instantiate a
+     * class that doesn't extend Phergie_Plugin_Abstract.
+     *
+     * @return void
+     */
+    public function testAddPluginThrowsExceptionIfRequestingNonPlugin()
+    {
+        try {
+            $this->handler->addPlugin('Handler');
+        } catch(Phergie_Plugin_Exception $e) {
+            $this->assertEquals(
+                Phergie_Plugin_Exception::ERR_INCORRECT_BASE_CLASS,
+                $e->getCode()
+            );
+            return;
+        }
+
+        $this->fail('An expected exception has not been raised');
+    }
+
+    /**
+     * Tests addPlugin() throwing an exception when trying to instantiate a
+     * class that can't be instantiated.
+     *
+     * @return void
+     */
+    public function testAddPluginThrowsExceptionIfPluginNotInstantiable()
+    {
+        $this->handler->addPath(dirname(__FILE__), 'Phergie_Plugin_');
+        try {
+            $this->handler->addPlugin('TestNonInstantiablePluginFromFile');
+        } catch(Phergie_Plugin_Exception $e) {
+            $this->assertEquals(
+                Phergie_Plugin_Exception::ERR_CLASS_NOT_INSTANTIABLE,
+                $e->getCode()
+            );
+            return;
+        }
+
+        $this->fail('An expected exception has not been raised');
+    }
+
+    /**
+     * Tests adding a plugin by its short name with arguments passed to the
+     * plugin constructor.
+     *
+     * @return void
+     */
+    public function testAddPluginShortNamePassesArgsToConstructor()
+    {
+        $pluginName = 'Mock';
+        $this->handler->addPath(dirname(__FILE__), 'Phergie_Plugin_');
+
+        $arguments = array('a', 'b', 'c');
+        $plugin = $this->handler->addPlugin($pluginName, $arguments);
+
+        $this->assertAttributeSame(
+            $arguments,
+            'arguments',
+            $plugin,
+            'Arguments do not match'
+        );
+    }
+
+    /**
+     * Tests addPlugin() passing Phergie_Config to an instantiated plugin.
+     *
+     * @return void
+     */
+    public function testAddPluginPassesConstructorArguments()
+    {
+        $pluginName = 'Mock';
+        $this->handler->addPath(dirname(__FILE__), 'Phergie_Plugin_');
+        $plugin = $this->handler->addPlugin($pluginName);
+
+        $this->assertSame(
+            $this->config,
+            $plugin->getConfig(),
+            'Phergie_Config instances do not match'
+        );
+
+        $this->assertSame(
+            $this->events,
+            $plugin->getEventHandler(),
+            'Phergie_Event_Handler instances do not match'
+        );
+    }
+
+    /**
+     * Tests addPlugin() calling onLoad() on an instantiated plugin.
+     *
+     * @return void
+     */
+    public function testAddPluginCallsOnLoadOnInstantiatedPlugin()
+    {
+        $plugin = $this->getMockPlugin(null, array('onLoad'));
+        $plugin
+            ->expects($this->once())
+            ->method('onLoad');
+        $this->handler->addPlugin($plugin);
+    }
+
+    /**
+     * Tests addPlugin() returning the same plugin when called twice.
+     *
+     * @return void
+     */
+    public function testAddPluginReturnsSamePluginWhenAskedTwice()
+    {
+        $pluginName = 'Mock';
+        $this->handler->addPath(dirname(__FILE__), 'Phergie_Plugin_');
+        $plugin1 = $this->handler->addPlugin($pluginName);
+        $plugin2 = $this->handler->addPlugin($pluginName);
+        $this->assertSame($plugin1, $plugin2);
+    }
+
+    /**
+     * Tests getPlugin() throwing an exception when trying to get an
+     * unloaded plugin with autoload disabled.
+     *
+     * @depends testGetAutoloadDefaultsToNotAutoload
+     * @return void
+     */
+    public function testExceptionThrownWhenLoadingPluginWithoutAutoload()
+    {
+        $this->handler->addPath(dirname(__FILE__), 'Phergie_Plugin_');
+
+        try {
+            $this->handler->getPlugin('Mock');
+        } catch (Phergie_Plugin_Exception $expected) {
+            $this->assertEquals(
+                Phergie_Plugin_Exception::ERR_PLUGIN_NOT_LOADED,
+                $expected->getCode()
+            );
+            return;
+        }
+
+        $this->fail('An expected exception has not been raised');
+    }
+
+    /**
+     * Tests addPlugins() with a plugin short name and no plugin constructor
+     * arguments.
+     *
+     * @depends testAddPluginByShortName
+     * @depends testAddPluginByInstance
+     * @return void
+     */
+    public function testAddPluginsWithoutArguments()
+    {
+        $prefix = 'Phergie_Plugin_';
+        $this->handler->addPath(dirname(__FILE__), $prefix);
+
+        $plugin = 'Mock';
+        $this->handler->addPlugins(array($plugin));
+        $returnedPlugin = $this->handler->getPlugin($plugin);
+        $this->assertContains(
+            get_class($returnedPlugin),
+            $prefix . $plugin,
+            'Short name plugin not of expected class'
+        );
+    }
+
+    /**
+     * Tests addPlugins() with a plugin short name and plugin constructor
+     * arguments.
+     *
+     * @depends testAddPluginByShortName
+     * @depends testAddPluginByInstance
+     * @return void
+     */
+    public function testAddPluginsWithArguments()
+    {
+        $prefix = 'Phergie_Plugin_';
+        $this->handler->addPath(dirname(__FILE__), $prefix);
+
+        $arguments = array(1, 2, 3);
+        $plugin = array('Mock', $arguments);
+        $this->handler->addPlugins(array($plugin));
+        $returnedPlugin = $this->handler->getPlugin('Mock');
+        $this->assertEquals(
+            $arguments,
+            $returnedPlugin->getArguments(),
+            'Constructor arguments for instance plugin do not match'
+        );
+    }
+
+    /**
+     * Tests removePlugin() with a plugin instance.
+     *
+     * @depends testAddPluginByInstance
+     * @return void
+     */
+    public function testRemovePluginByInstance()
+    {
+        $plugin = $this->getMockPlugin();
+        $this->handler->addPlugin($plugin);
+        $this->handler->removePlugin($plugin);
+        $this->assertFalse(
+            $this->handler->hasPlugin($plugin->getName()),
+            'Plugin was not removed'
+        );
+    }
+
+    /**
+     * Tests removePlugin() with a plugin short name.
+     *
+     * @depends testAddPluginByShortName
+     * @return void
+     */
+    public function testRemovePluginByShortName()
+    {
+        $plugin = 'Mock';
+        $this->handler->addPath(dirname(__FILE__), 'Phergie_Plugin_');
+
+        $this->handler->addPlugin($plugin);
+        $this->handler->removePlugin($plugin);
+        $this->assertFalse(
+            $this->handler->hasPlugin($plugin),
+            'Plugin was not removed'
+        );
+    }
+
+    /**
+     * Tests getPlugin() when the plugin is not already loaded and
+     * autoloading is disabled.
+     *
+     * @depends testSetAutoload
+     * @return void
+     */
+    public function testGetPluginWithAutoloadEnabled()
+    {
+        $this->handler->setAutoload(true);
+        $this->handler->addPath(dirname(__FILE__), 'Phergie_Plugin_');
+        $plugin = $this->handler->getPlugin('Mock');
+        $this->assertType(
+            'Phergie_Plugin_Mock',
+            $plugin,
+            'Retrieved plugin not of expected class'
+        );
+    }
+
+    /**
+     * Tests getPlugins().
+     *
+     * @depends testGetPluginWithAutoloadEnabled
+     * @return void
+     */
+    public function testGetPlugins()
+    {
+        $plugin1 = $this->getMockPlugin('TestPlugin1');
+        $this->handler->addPlugin($plugin1);
+
+        $plugin2 = $this->getMockPlugin('TestPlugin2');
+        $this->handler->addPlugin($plugin2);
+
+        $expected = array(
+            'testplugin1' => $plugin1,
+            'testplugin2' => $plugin2,
+        );
+
+        $actual = $this->handler->getPlugins();
+        $this->assertEquals($expected, $actual);
+
+        $actual = $this->handler->getPlugins(array('testplugin1', 'testplugin2'));
+        $this->assertEquals($expected, $actual);
+    }
+
+    /**
+     * Tests the plugin receiving and using a predefined iterator instance.
+     *
+     * @depends testGetPlugins
+     * @return void
+     */
+    public function testSetIterator()
+    {
+        $plugin = $this->getMockPlugin('TestPlugin');
+        $this->handler->addPlugin($plugin);
+        $plugins = $this->handler->getPlugins();
+        $iterator = new ArrayIterator($plugins);
+        $this->handler->setIterator($iterator);
+        $this->assertSame($this->handler->getIterator(), $iterator);
+        $iterated = array();
+        foreach ($this->handler as $plugin) {
+            $iterated[strtolower($plugin->getName())] = $plugin;
+        }
+        $this->assertEquals($iterated, $plugins);
+    }
+}
diff --git a/plugins/Irc/extlib/phergie/Tests/Phergie/Plugin/IteratorTest.php b/plugins/Irc/extlib/phergie/Tests/Phergie/Plugin/IteratorTest.php
new file mode 100644 (file)
index 0000000..336b25d
--- /dev/null
@@ -0,0 +1,151 @@
+<?php
+/**
+ * Phergie
+ *
+ * PHP version 5
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.
+ * It is also available through the world-wide-web at this URL:
+ * http://phergie.org/license
+ *
+ * @category  Phergie
+ * @package   Phergie_Tests
+ * @author    Phergie Development Team <team@phergie.org>
+ * @copyright 2008-2010 Phergie Development Team (http://phergie.org)
+ * @license   http://phergie.org/license New BSD License
+ * @link      http://pear.phergie.org/package/Phergie_Tests
+ */
+
+/**
+ * Unit test suite for Pherge_Plugin_Iterator.
+ *
+ * @category Phergie
+ * @package  Phergie_Tests
+ * @author   Phergie Development Team <team@phergie.org>
+ * @license  http://phergie.org/license New BSD License
+ * @link     http://pear.phergie.org/package/Phergie_Tests
+ */
+class Phergie_Plugin_IteratorTest extends PHPUnit_Framework_TestCase
+{
+    /**
+     * Iterator instance being tested
+     *
+     * @var Phergie_Plugin_Iterator
+     */
+    protected $iterator;
+
+    /**
+     * List of mock plugin instances to be iterated
+     *
+     * @var array
+     */
+    protected $plugins;
+
+    /**
+     * Initializes the iterator instance being tested.
+     *
+     * @return void
+     */
+    public function setUp()
+    {
+        $this->plugins = array();
+        foreach (range(0, 4) as $index) {
+            $plugin = $this->getMock('Phergie_Plugin_Abstract');
+            $plugin
+                ->expects($this->any())
+                ->method('getName')
+                ->will($this->returnValue($index));
+            $this->plugins[] = $plugin;
+        }
+
+        $this->iterator = new Phergie_Plugin_Iterator(
+            new ArrayIterator($this->plugins)
+        );
+    }
+
+    /**
+     * Tests that all plugins are iterated when no filters are applied.
+     */
+    public function testIteratesAllPluginsWithNoFilters()
+    {
+        $expected = range(0, 4);
+        $actual = array();
+        foreach ($this->iterator as $plugin) {
+            $actual[] = $plugin->getName();
+        }
+        $this->assertEquals($expected, $actual);
+    }
+
+    /**
+     * Tests that appropriate plugins are iterated when plugin name filters
+     * are applied.
+     */
+    public function testIteratesPluginsWithNameFilters()
+    {
+        // Test acceptance of strings and fluent interface implementation
+        $returned = $this->iterator->addPluginFilter('0');
+        $this->assertSame($this->iterator, $returned);
+
+        // Test acceptance of arrays
+        $this->iterator->addPluginFilter(array('1', '3'));
+
+        // Test application of filters to iteration
+        $expected = array('2', '4');
+        $actual = array();
+        foreach ($this->iterator as $plugin) {
+            $actual[] = $plugin->getName();
+        }
+        $this->assertEquals($expected, $actual);
+    }
+
+    /**
+     * Tests that appropriate plugins are iterated when method name filters
+     * are applied.
+     *
+     * The same method name is used in all cases here because mocked methods
+     * of mock objects do not appear to be detected by method_exists() or
+     * ReflectionClass, so filtering by a method defined in the base plugin
+     * class seems the easiest way to test that method filtering really
+     * works.
+     */
+    public function testIteratesPluginsWithMethodFilters()
+    {
+        // Tests acceptance of strings and fluent interface implementation
+        $returned = $this->iterator->addMethodFilter('getName');
+        $this->assertSame($this->iterator, $returned);
+
+        // Test acceptance of arrays
+        $this->iterator->addMethodFilter(array('getName', 'getName'));
+
+        // Test application of filters to iteration
+        $expected = array();
+        $actual = array();
+        foreach ($this->iterator as $plugin) {
+            $actual[] = $plugin->getName();
+        }
+        $this->assertEquals($expected, $actual);
+    }
+
+    /**
+     * Tests that all plugins are iterated after filters are cleared.
+     *
+     * @depends testIteratesPluginsWithNameFilters
+     * @depends testIteratesPluginsWithMethodFilters
+     */
+    public function testIteratesPluginsAfterClearingFilters()
+    {
+        $this->iterator->addPluginFilter('0');
+        $this->iterator->addMethodFilter('method1');
+        $this->iterator->clearFilters();
+
+        $expected = range(0, 4);
+        $actual = array();
+        foreach ($this->iterator as $plugin) {
+            $actual[] = $plugin->getName();
+        }
+        $this->assertEquals($expected, $actual);
+    }
+}
diff --git a/plugins/Irc/extlib/phergie/Tests/Phergie/Plugin/Mock.php b/plugins/Irc/extlib/phergie/Tests/Phergie/Plugin/Mock.php
new file mode 100755 (executable)
index 0000000..44a5d11
--- /dev/null
@@ -0,0 +1,61 @@
+<?php
+/**
+ * Phergie
+ *
+ * PHP version 5
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.
+ * It is also available through the world-wide-web at this URL:
+ * http://phergie.org/license
+ *
+ * @category  Phergie
+ * @package   Phergie_Tests
+ * @author    Phergie Development Team <team@phergie.org>
+ * @copyright 2008-2010 Phergie Development Team (http://phergie.org)
+ * @license   http://phergie.org/license New BSD License
+ * @link      http://pear.phergie.org/package/Phergie_Tests
+ */
+
+/**
+ * Creates a plugin on the filesystem that can be used by
+ * Phergie_Plugin_Handler::addPath() to be located and loaded.
+ *
+ * @category Phergie
+ * @package  Phergie_Tests
+ * @author   Phergie Development Team <team@phergie.org>
+ * @license  http://phergie.org/license New BSD License
+ * @link     http://pear.phergie.org/package/Phergie_Tests
+ */
+class Phergie_Plugin_Mock extends Phergie_Plugin_Abstract
+{
+    /**
+     * Arguments passed to the constructor
+     *
+     * @var array
+     */
+    protected $arguments;
+
+    /**
+     * Stores all arguments for later use.
+     *
+     * @return void
+     */
+    public function __construct()
+    {
+        $this->arguments = func_get_args();
+    }
+
+    /**
+     * Returns all constructor arguments.
+     *
+     * @return array Enumerated array containing the arguments passed to the
+     *         constructor in order
+     */
+    public function getArguments()
+    {
+        return $this->arguments;
+    }
+}
diff --git a/plugins/Irc/extlib/phergie/Tests/Phergie/Plugin/PingTest.php b/plugins/Irc/extlib/phergie/Tests/Phergie/Plugin/PingTest.php
new file mode 100644 (file)
index 0000000..b9c2dde
--- /dev/null
@@ -0,0 +1,175 @@
+<?php
+/**
+ * Phergie
+ *
+ * PHP version 5
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.
+ * It is also available through the world-wide-web at this URL:
+ * http://phergie.org/license
+ *
+ * @category  Phergie
+ * @package   Phergie
+ * @author    Phergie Development Team <team@phergie.org>
+ * @copyright 2008-2010 Phergie Development Team (http://phergie.org)
+ * @license   http://phergie.org/license New BSD License
+ * @link      http://pear.phergie.org/package/Phergie
+ */
+
+require_once(dirname(__FILE__) . '/TestCase.php');
+
+/**
+ * Unit test suite for Pherge_Plugin_Ping.
+ *
+ * @category Phergie
+ * @package  Phergie_Tests
+ * @author   Phergie Development Team <team@phergie.org>
+ * @license  http://phergie.org/license New BSD License
+ * @link     http://pear.phergie.org/package/Phergie
+ */
+class Phergie_Plugin_PingTest extends Phergie_Plugin_TestCase
+{
+    protected $config = array('ping.ping'  => 10,
+                              'ping.event' => 300);
+    
+    /**
+     * Sets up the fixture, for example, opens a network connection.
+     * This method is called before a test is executed.
+     */
+    protected function setUp()
+    {
+        $this->setPlugin(new Phergie_Plugin_Ping);
+    }
+
+    /**
+     * Test the lastEvent setter and getter
+     */
+    public function testSetGetLastEvent()
+    {
+        $expected = rand(100000,200000);
+        $this->plugin->setLastEvent($expected);
+        $this->assertEquals($expected,
+                            $this->plugin->getLastEvent(),
+                            'Assert that the last event was set and gotten ' .
+                            'correctly');
+    }
+
+    /**
+     * Test the lastPing setter and getter
+     */
+    public function testSetGetLastPing()
+    {
+
+        $expected = rand(100000,200000);
+        $this->plugin->setLastPing($expected);
+        $this->assertEquals($expected,
+                            $this->plugin->getLastPing(),
+                            'Assert that the last ping was set and gotten ' .
+                            'correctly');
+    }
+
+    /**
+     * Tests the onConnect hook
+     */
+    public function testOnConnect()
+    {
+        $time = time() - 1;
+        // We need to make sure time() is going to be creater next time it is called
+        
+        $this->plugin->onConnect();
+        $this->assertNull($this->plugin->getLastPing(), 
+                          'onConnect should set last ping to null');
+        $this->assertGreaterThan($time,
+                                 $this->plugin->getLastEvent(),
+                                 'onConnect should update lastEvent with the ' .
+                                 'current timestamp');
+        $this->assertLessThan($time + 2,
+                              $this->plugin->getLastEvent(),
+                              'onConnect should update lastEvent with the ' .
+                              'current timestamp');
+    }
+
+    /**
+     * Test that the preEvent method updates the lastEvent with the current time
+     */
+    public function testPreEvent()
+    {
+        $time = time() -1;
+        $this->plugin->preEvent();
+        $this->assertGreaterThan($time,
+                                 $this->plugin->getLastEvent(),
+                                 'Last event time was set properly on preEvent');
+        $this->assertLessThan($time +2,
+                              $this->plugin->getLastEvent(),
+                              'Last Event time was set properly on preEvent');
+    }
+
+    /**
+     * @todo Implement testOnPingResponse().
+     */
+    public function testOnPingResponse()
+    {
+        $this->plugin->setLastPing(time());
+        $this->plugin->onPingResponse();
+        $this->assertNull($this->plugin->getLastPing(),
+                          'Last ping time should be null after onPingResponse');
+
+    }
+
+    /**
+     * Test that the plugin issues a quit when the ping threashold
+     * has been exceeded
+     */
+    public function testOnTickExceededPingThresholdQuits()
+    {
+        $this->plugin->setLastPing(1);
+        $this->plugin->onTick();
+        $this->assertHasEvent(Phergie_Event_Command::TYPE_QUIT);
+    }
+    
+    /**
+     * Test that the plugin issues a quit when the ping threashold
+     * has been exceeded
+     */
+    public function testOnTickPingWithinThresholdDoesNotQuits()
+    {
+        $this->plugin->setLastPing(time());
+        $this->plugin->onTick();
+        $this->assertDoesNotHaveEvent(Phergie_Event_Command::TYPE_QUIT);
+    }
+
+    /**
+     * Test that a ping is emitted when the event threashold is exceeded
+     */
+    public function testPingEmittedAfterThresholdExceeded()
+    {
+        $this->plugin->setLastEvent(time() - $this->config['ping.event'] - 1);
+        $this->plugin->onTick();
+        $this->assertHasEvent(Phergie_Event_Command::TYPE_PING);
+        $events = $this->getResponseEvents(Phergie_Event_Command::TYPE_PING);
+        foreach ($events as $event) {
+            $this->assertEventEmitter($event,
+                                      $this->plugin,
+                    'Assert that the event was emitted by the tested plugin');
+        }
+    }
+
+    /**
+     * Test that no ping is emitted when the event thresthold is not exceeded
+     */
+    public function testNoPingEmittedWhenThresholdNotExceeded()
+    {
+        $this->plugin->setLastEvent(time() - $this->config['ping.event'] +1);
+        $this->plugin->onTick();
+        $this->assertDoesNotHaveEvent(Phergie_Event_Command::TYPE_PING);
+    }
+
+    public function tearDown()
+    {
+        $this->handler->clearEvents();
+    }
+
+}
\ No newline at end of file
diff --git a/plugins/Irc/extlib/phergie/Tests/Phergie/Plugin/PongTest.php b/plugins/Irc/extlib/phergie/Tests/Phergie/Plugin/PongTest.php
new file mode 100644 (file)
index 0000000..a8bc8fd
--- /dev/null
@@ -0,0 +1,74 @@
+<?php
+/**
+ * Phergie
+ *
+ * PHP version 5
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.
+ * It is also available through the world-wide-web at this URL:
+ * http://phergie.org/license
+ *
+ * @category  Phergie
+ * @package   Phergie
+ * @author    Phergie Development Team <team@phergie.org>
+ * @copyright 2008-2010 Phergie Development Team (http://phergie.org)
+ * @license   http://phergie.org/license New BSD License
+ * @link      http://pear.phergie.org/package/Phergie
+ */
+
+require_once(dirname(__FILE__) . '/TestCase.php');
+
+/**
+ * Unit test suite for Pherge_Plugin_Pong.
+ *
+ * @category Phergie
+ * @package  Phergie_Tests
+ * @author   Phergie Development Team <team@phergie.org>
+ * @license  http://phergie.org/license New BSD License
+ * @link     http://pear.phergie.org/package/Phergie
+ */
+class Phergie_Plugin_PongTest extends Phergie_Plugin_TestCase
+{
+    /**
+     * Sets up the fixture, for example, opens a network connection.
+     * This method is called before a test is executed.
+     */
+    protected function setUp()
+    {
+        $this->setPlugin(new Phergie_Plugin_Pong);
+    }
+
+    /**
+     * Test that when a ping is received, a Phergie_Event_Command::TYPE_PONG
+     * is set to the handler
+     *
+     * @event Phergie_Event_Command::TYPE_PING
+     */
+    public function testOnPing()
+    {
+        $this->plugin->onPing();
+        $this->assertHasEvent(Phergie_Event_Command::TYPE_PONG);
+    }
+
+    /**
+     * Test that when a ping is received, a Phergie_Event_Command::TYPE_PONG
+     * is set to the handler
+     *
+     * @event Phergie_Event_Command::TYPE_PING
+     */
+    public function testOnPingResponseArguement()
+    {
+        $this->plugin->onPing();
+        $this->assertHasEvent(Phergie_Event_Command::TYPE_PONG);
+        $events = $this->getResponseEvents(Phergie_Event_Command::TYPE_PONG);
+        $this->assertTrue(count($events) === 1, 'Assert that only one pong is emitted');
+        $this->assertEventEmitter(current($events),
+                                  $this->plugin,
+                                  'Assert that the tested plugin emitted the event');
+
+    }
+
+}
diff --git a/plugins/Irc/extlib/phergie/Tests/Phergie/Plugin/SpellCheckTest.php b/plugins/Irc/extlib/phergie/Tests/Phergie/Plugin/SpellCheckTest.php
new file mode 100644 (file)
index 0000000..8ed9f0d
--- /dev/null
@@ -0,0 +1,205 @@
+<?php
+/**
+ * Phergie
+ *
+ * PHP version 5
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.
+ * It is also available through the world-wide-web at this URL:
+ * http://phergie.org/license
+ *
+ * @category  Phergie
+ * @package   Phergie
+ * @author    Phergie Development Team <team@phergie.org>
+ * @copyright 2008-2010 Phergie Development Team (http://phergie.org)
+ * @license   http://phergie.org/license New BSD License
+ * @link      http://pear.phergie.org/package/Phergie
+ */
+
+require_once dirname(__FILE__) . '/TestCase.php';
+
+/**
+ * Unit test suite for Pherge_Plugin_SpellCheck.
+ *
+ * @category Phergie
+ * @package  Phergie_Tests
+ * @author   Phergie Development Team <team@phergie.org>
+ * @license  http://phergie.org/license New BSD License
+ * @link     http://pear.phergie.org/package/Phergie
+ */
+class Phergie_Plugin_SpellCheckTest extends Phergie_Plugin_TestCase
+{
+
+    /**
+     * Current SpellCheck plugin instance
+     *
+     * @var Phergie_Plugin_SpellCheck
+     */
+    protected $spell;
+
+    /**
+     * Sets up the fixture, for example, opens a network connection.
+     * This method is called before a test is executed.
+     * 
+     * @return void
+     */
+    protected function setUp()
+    {
+        $this->config = array('spellcheck.lang' => 'en');
+
+        $this->spell = new Phergie_Plugin_SpellCheck();
+        $this->setPlugin(new Phergie_Plugin_Command());
+        
+        $config = $this->plugin->getConfig();
+        
+        $handler = new Phergie_Plugin_Handler($config, $this->handler);
+        $this->plugin->setPluginHandler($handler);
+        
+        $handler->addPlugin($this->plugin);
+        $handler->addPlugin($this->spell);
+
+        $this->spell->setEventHandler($this->handler);
+        $this->spell->setConnection($this->connection);
+    }
+
+    /**
+     * @event Phergie_Event_Request::privmsg
+     * @eventArg #zftalk
+     * @eventArg spell
+     */
+    public function testSpell()
+    {
+        $this->spell->onLoad();
+        
+        $this->copyEvent();
+        $this->plugin->onPrivMsg();
+        $this->assertDoesNotHaveEvent(Phergie_Event_Command::TYPE_PRIVMSG);
+    }
+
+    /**
+     * @event Phergie_Event_Request::privmsg
+     * @eventArg #phergie
+     * @eventArg spell test
+     */
+    public function testSpellTest()
+    {
+        $this->spell->onLoad();
+        
+        $this->copyEvent();
+        $this->plugin->onPrivMsg();
+
+        $events = $this->getResponseEvents(Phergie_Event_Command::TYPE_PRIVMSG);
+        
+        $this->assertEquals(1, count($events));
+        foreach ($events as $event) {
+            $args = $event->getArguments();
+            
+            $this->assertEquals('#phergie', $args[0]);
+            
+            $this->assertContains('CheckSpellUser:', $args[1]);
+            $this->assertContains('test', $args[1]);
+            $this->assertContains('correct', $args[1]);
+        }            
+    }
+
+    /**
+     * @event Phergie_Event_Request::privmsg
+     * @eventArg #phergie
+     * @eventArg spell testz
+     */
+    public function testSpellTestz()
+    {
+        $this->spell->onLoad();
+        
+        $this->copyEvent();
+        $this->plugin->onPrivMsg();
+        
+        $events = $this->getResponseEvents(Phergie_Event_Command::TYPE_PRIVMSG);
+        
+        $this->assertEquals(1, count($events));
+        foreach ($events as $event) {
+            $args = $event->getArguments();
+            
+            $this->assertEquals('#phergie', $args[0]);
+            
+            $this->assertContains('CheckSpellUser:', $args[1]);
+            $this->assertRegExp('/([a-z]+, ){4}/', $args[1]);
+            $this->assertContains('testz', $args[1]);
+            $this->assertContains('test,', $args[1]);
+        }
+    }
+
+    /**
+     * @event Phergie_Event_Request::privmsg
+     * @eventArg #phergie
+     * @eventArg spell testz
+     */
+    public function testSpellMoreSuggestions()
+    {
+        $config = $this->spell->getConfig();
+        
+        $this->copyEvent();
+        $config['spellcheck.limit'] = 6;
+        
+        $this->spell->onLoad();
+        $this->plugin->onPrivMsg();
+        
+        $events = $this->getResponseEvents(Phergie_Event_Command::TYPE_PRIVMSG);
+        
+        $this->assertEquals(1, count($events));
+        foreach ($events as $event) {
+            $args = $event->getArguments();
+            
+            $this->assertEquals('#phergie', $args[0]);
+            
+            $this->assertContains('CheckSpellUser:', $args[1]);
+            $this->assertRegExp('/([a-z]+, ){5}/', $args[1]);
+            $this->assertContains('testz', $args[1]);
+            $this->assertContains('test,', $args[1]);
+        }
+    }
+
+    /**
+     * @event Phergie_Event_Request::privmsg
+     * @eventArg #phergie
+     * @eventArg spell qwertyuiopasdfghjklzxcvbnm
+     */
+    public function testSpellNoSuggestions()
+    {
+        $this->spell->onLoad();
+        
+        $this->copyEvent();
+        $this->plugin->onPrivMsg();
+        
+        $events = $this->getResponseEvents(Phergie_Event_Command::TYPE_PRIVMSG);
+        
+        $this->assertEquals(1, count($events));
+        foreach ($events as $event) {
+            $args = $event->getArguments();
+            
+            $this->assertEquals('#phergie', $args[0]);
+            
+            $this->assertContains('CheckSpellUser:', $args[1]);
+            $this->assertContains('find any suggestions', $args[1]);
+        }
+    }
+    
+    /**
+     * Copy event from command to spell plugin
+     * 
+     * @return void
+     */
+    protected function copyEvent()
+    {
+        $hostmask = Phergie_Hostmask::fromString('CheckSpellUser!test@testing.org');
+
+        $event = $this->plugin->getEvent();
+        $event->setHostmask($hostmask);
+
+        $this->spell->setEvent($event);
+    }
+
+}
diff --git a/plugins/Irc/extlib/phergie/Tests/Phergie/Plugin/TerryChayTest.php b/plugins/Irc/extlib/phergie/Tests/Phergie/Plugin/TerryChayTest.php
new file mode 100644 (file)
index 0000000..e76020b
--- /dev/null
@@ -0,0 +1,99 @@
+<?php
+/**
+ * Phergie
+ *
+ * PHP version 5
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.
+ * It is also available through the world-wide-web at this URL:
+ * http://phergie.org/license
+ *
+ * @category  Phergie
+ * @package   Phergie
+ * @author    Phergie Development Team <team@phergie.org>
+ * @copyright 2008-2010 Phergie Development Team (http://phergie.org)
+ * @license   http://phergie.org/license New BSD License
+ * @link      http://pear.phergie.org/package/Phergie
+ */
+
+require_once(dirname(__FILE__) . '/TestCase.php');
+
+/**
+ * Unit test suite for Pherge_Plugin_TerryChay.
+ *
+ * @category Phergie
+ * @package  Phergie_Tests
+ * @author   Phergie Development Team <team@phergie.org>
+ * @license  http://phergie.org/license New BSD License
+ * @link     http://pear.phergie.org/package/Phergie
+ */
+class Phergie_Plugin_TerryChayTest extends Phergie_Plugin_TestCase
+{
+    /**
+     * Sets up the fixture, for example, opens a network connection.
+     * This method is called before a test is executed.
+     */
+    protected function setUp()
+    {
+        $this->setPlugin(new Phergie_Plugin_TerryChay());
+        $config = new Phergie_Config();
+        $handler = new Phergie_Plugin_Handler($config, $this->handler);
+        $this->plugin->setPluginHandler($handler);
+        $handler->addPlugin($this->plugin);
+        $handler->addPlugin(new Phergie_Plugin_Http($config));
+        $this->plugin->setConfig($config);
+        $this->connection->setNick('phergie');
+        $this->plugin->onLoad();
+    }
+
+    /**
+     * @event Phergie_Event_Request::privmsg
+     * @eventArg #zftalk
+     * @eventArg tychay
+     */
+    public function testWithTyChay()
+    {
+        $this->plugin->onPrivMsg();
+        $this->assertHasEvent(Phergie_Event_Command::TYPE_PRIVMSG);
+    }
+
+    /**
+     * @event Phergie_Event_Request::privmsg
+     * @eventArg #zftalk
+     * @eventArg terrychay
+     */
+    public function testWithTerryChay()
+    {
+        $this->plugin->onPrivMsg();
+        $this->assertDoesNotHaveEvent(Phergie_Event_Command::TYPE_PRIVMSG,
+                              'string "terrychay" should not invoke a response');
+    }
+    
+    /**
+     * @event Phergie_Event_Request::privmsg
+     * @eventArg #zftalk
+     * @eventArg terry chay
+     */
+    public function testWithTerry_Chay()
+    {
+        $this->plugin->onPrivMsg();
+        $this->assertHasEvent(Phergie_Event_Command::TYPE_PRIVMSG,
+                              'string "terry chay" should invoke a response');
+    }
+
+    /**
+     * @event Phergie_Event_Request::privmsg
+     * @eventArg #zftalk
+     * @eventArg Elazar is not Mr. Chay
+     */
+    public function testWithNoTyChay()
+    {
+        $this->plugin->onPrivMsg();
+        $this->assertDoesNotHaveEvent(Phergie_Event_Command::TYPE_PRIVMSG,
+                                      'Failed asserting that elazar is not ' .
+                                      'tychay');
+    }
+}
\ No newline at end of file
diff --git a/plugins/Irc/extlib/phergie/Tests/Phergie/Plugin/TestCase.php b/plugins/Irc/extlib/phergie/Tests/Phergie/Plugin/TestCase.php
new file mode 100644 (file)
index 0000000..36b81d6
--- /dev/null
@@ -0,0 +1,207 @@
+<?php
+/**
+ * Phergie
+ *
+ * PHP version 5
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.
+ * It is also available through the world-wide-web at this URL:
+ * http://phergie.org/license
+ *
+ * @category  Phergie
+ * @package   Phergie_Tests
+ * @author    Phergie Development Team <team@phergie.org>
+ * @copyright 2008-2010 Phergie Development Team (http://phergie.org)
+ * @license   http://phergie.org/license New BSD License
+ * @link      http://pear.phergie.org/package/Phergie_Tests
+ */
+
+/**
+ * Unit test suite for Pherge_Plugin classes
+ *
+ * @category Phergie
+ * @package  Phergie_Tests
+ * @author   Phergie Development Team <team@phergie.org>
+ * @license  http://phergie.org/license New BSD License
+ * @link     http://pear.phergie.org/package/Phergie_Tests
+ */
+abstract class Phergie_Plugin_TestCase extends PHPUnit_Framework_TestCase
+{
+    /**
+     * @var Phergie_Event_Handler
+     */
+    protected $handler;
+
+    /**
+     * @var Phergie_Connection
+     */
+    protected $connection;
+
+    /**
+     * @var array
+     */
+    protected $eventArgs;
+
+    /**
+     * @var Phergie_Plugin_Abstract
+     */
+    protected $plugin;
+
+    /**
+     * @var array
+     */
+    protected $config = array();
+
+    /**
+     * Constructs a test case with the given name.
+     *
+     * @param  string $name
+     * @param  array  $data
+     * @param  string $dataName
+     */
+    public function __construct($name = NULL, array $data = array(), $dataName = '')
+    {
+        parent::__construct($name, $data, $dataName);
+        $this->connection = new Phergie_Connection();
+        $this->handler    = new Phergie_Event_Handler();
+    }
+
+    /**
+     * Assert that a given event type exists in the event handler
+     * @param string $event
+     * @param string $message
+     */
+    public function assertHasEvent($event, $message = null)
+    {
+        self::assertTrue($this->handler->hasEventOfType($event), $message);
+    }
+
+    /**
+     * Assert that a given event type DOES NOT exist in the event handler
+     * @param string $event
+     * @param string $message
+     */
+    public function assertDoesNotHaveEvent($event, $message = null)
+    {
+        self::assertFalse($this->handler->hasEventOfType($event), $message);
+    }
+
+    /**
+     * Assert that the emitter of the given command event was the given
+     * plugin
+     *
+     * @param Phergie_Event_Command   $event
+     * @param Phergie_Plugin_Abstract $plugin
+     * @param string                  $message
+     */
+    public function assertEventEmitter(Phergie_Event_Command $event,
+                                       Phergie_Plugin_Abstract $plugin,
+                                       $message = null)
+    {
+        $this->assertSame($plugin, $event->getPlugin(), $message);
+    }
+
+    /**
+     * Gets the events added to the handler by the plugin
+     * @param string $type
+     * @return array | null
+     */
+    public function getResponseEvents($type = null)
+    {
+        if (is_string($type) && strlen($type) > 0) {
+            return $this->handler->getEventsOfType($type);
+        }
+        return $this->handler->getEvents();
+    }
+
+    /**
+     * Sets the event for the test
+     * @param array $event
+     * @param array $eventArgs
+     */
+    public function setEvent(array $event, array $eventArgs = null)
+    {
+        $eventClass = 'Phergie_Event_Request';
+        if (is_array($event)) {
+            $eventClass = $event[0];
+            $eventType  = $event[1];
+        } else {
+            throw new InvalidArgumentException("Invalid value for \$event");
+        }
+        $event = new $eventClass();
+        $event->setType($eventType);
+        $event->setArguments($eventArgs);
+        $this->plugin->setEvent($event);
+        $this->eventArgs = $eventArgs;
+    }
+
+    /**
+     * Sets the plugin to be tested
+     * If a plugin requries config for testing, an array placed in
+     * $this->config will be parsed into a Phergie_Config object and
+     * attached to the plugin
+     */
+    protected function setPlugin(Phergie_Plugin_Abstract $plugin)
+    {
+        $this->plugin = $plugin;
+        $this->plugin->setEventHandler($this->handler);
+        $this->plugin->setConnection($this->connection);
+        $this->connection->setNick('test');
+        if (!empty($this->config)) {
+            $config = new Phergie_Config();
+            foreach ($this->config as $configKey => $configValue) {
+                $config[$configKey] = $configValue;
+            }
+            $plugin->setConfig($config);
+        }
+    }
+
+    /**
+     * Overrides the runTest method to add additional annotations
+     * @return PHPUnit_Framework_TestResult
+     */
+    protected function runTest()
+    {
+        if (null === $this->plugin) {
+            throw new RuntimeException(
+                    'Tests cannot be run before plugin is set'
+            );
+        }
+        
+        // Clean the event handler... important!
+        $this->handler->clearEvents();
+
+        $info      = $this->getAnnotations();
+        $event     = null;
+        $eventArgs = array();
+        if (isset($info['method']['event']) && isset($info['method']['event'][0])) {
+            if (!is_string($info['method']['event'][0])) {
+                throw new InvalidArgumentException(
+                        'Only one event may be specified'
+                );
+            }
+            $event = $info['method']['event'][0];
+
+            if (stristr($event, '::')) {
+                $event = explode('::', $event);
+            }
+        }
+        if (isset($info['method']['eventArg'])) {
+            $eventArgs = $info['method']['eventArg'];
+        }
+        if (null !== $event) {
+            $this->setEvent($event, $eventArgs);
+        }
+
+        $testResult = parent::runTest();
+
+        // Clean the event handler again... just incase this time.
+        $this->handler->clearEvents();
+
+        return $testResult;
+    }
+
+}
diff --git a/plugins/Irc/extlib/phergie/Tests/Phergie/Plugin/TestNonInstantiablePluginFromFile.php b/plugins/Irc/extlib/phergie/Tests/Phergie/Plugin/TestNonInstantiablePluginFromFile.php
new file mode 100755 (executable)
index 0000000..f9bddd1
--- /dev/null
@@ -0,0 +1,43 @@
+<?php
+/**
+ * Phergie
+ *
+ * PHP version 5
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.
+ * It is also available through the world-wide-web at this URL:
+ * http://phergie.org/license
+ *
+ * @category  Phergie
+ * @package   Phergie_Tests
+ * @author    Phergie Development Team <team@phergie.org>
+ * @copyright 2008-2010 Phergie Development Team (http://phergie.org)
+ * @license   http://phergie.org/license New BSD License
+ * @link      http://pear.phergie.org/package/Phergie_Tests
+ */
+
+/**
+ * Creates a plugin on the filesystem that can be used by
+ * Phergie_Plugin_Handler's addPath utility to be located and loaded.
+ *
+ * @category Phergie
+ * @package  Phergie_Tests
+ * @author   Phergie Development Team <team@phergie.org>
+ * @license  http://phergie.org/license New BSD License
+ * @link     http://pear.phergie.org/package/Phergie_Tests
+ */
+class Phergie_Plugin_TestNonInstantiablePluginFromFile
+extends Phergie_Plugin_Abstract
+{
+    /**
+     * Private constructor to ensure that this class is not instantiable.
+     *
+     * @return void
+     */
+    private function __construct()
+    {
+    }
+}
diff --git a/plugins/Irc/extlib/phergie/Tests/TestHelper.php b/plugins/Irc/extlib/phergie/Tests/TestHelper.php
new file mode 100644 (file)
index 0000000..e70af44
--- /dev/null
@@ -0,0 +1,26 @@
+<?php
+/**
+ * Phergie
+ *
+ * PHP version 5
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.
+ * It is also available through the world-wide-web at this URL:
+ * http://phergie.org/license
+ *
+ * @category  Phergie
+ * @package   Phergie_Tests
+ * @author    Phergie Development Team <team@phergie.org>
+ * @copyright 2008-2010 Phergie Development Team (http://phergie.org)
+ * @license   http://phergie.org/license New BSD License
+ * @link      http://pear.phergie.org/package/Phergie_Tests
+ */
+
+error_reporting(E_ALL | E_STRICT);
+
+// Phergie components require Phergie_Autoload to function correctly.
+require_once dirname(__FILE__) . '/../Phergie/Autoload.php';
+Phergie_Autoload::registerAutoloader();
diff --git a/plugins/Irc/extlib/phergie/Tests/phpunit.xml b/plugins/Irc/extlib/phergie/Tests/phpunit.xml
new file mode 100644 (file)
index 0000000..b96589e
--- /dev/null
@@ -0,0 +1,26 @@
+<!--
+/**
+ * Phergie
+ *
+ * PHP version 5
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.
+ * It is also available through the world-wide-web at this URL:
+ * http://phergie.org/license
+ *
+ * @category  Phergie
+ * @package   Phergie_Tests
+ * @author    Phergie Development Team <team@phergie.org>
+ * @copyright 2008-2010 Phergie Development Team (http://phergie.org)
+ * @license   http://phergie.org/license New BSD License
+ * @link      http://pear.phergie.org/package/Phergie_Tests
+ */
+-->
+<phpunit colors="true" bootstrap="./TestHelper.php">
+    <testsuite name="Phergie Test Suite">
+        <directory>./</directory>
+    </testsuite>
+</phpunit>
diff --git a/plugins/Irc/extlib/phergie/build.xml b/plugins/Irc/extlib/phergie/build.xml
new file mode 100644 (file)
index 0000000..7510c75
--- /dev/null
@@ -0,0 +1,301 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<project name="Phergie" default="core">
+
+    <tstamp>
+        <format property="DSTAMP" pattern="%Y-%m-%d" />
+    </tstamp>
+
+    <taskdef name="phergiepkg" classname="PhergiePackageTask" />
+    <taskdef name="phpdocumentor" classname="phing.tasks.ext.phpdoc.PhpDocumentorTask" />
+
+    <available file="./build.properties" property="have_properties_file" />
+
+    <property name="clean" value="true" />
+
+    <target name="input">
+
+        <if>
+            <equals arg1="${have_properties_file}" arg2="true" />
+            <then>
+                <property file="./build.properties" />
+            </then>
+            <else>
+                <input propertyname="build.srcdir" defaultvalue="./" message="Source directory" /> 
+                <input propertyname="build.dstdir" defaultvalue="./" message="Destination directory" /> 
+                <input propertyname="build.version.release" message="Release version" />
+                <input propertyname="build.version.api" message="API version" />
+                <input propertyname="build.stability.release" defaultvalue="stable" message="Release stability" validArgs="snapshot,devel,alpha,beta,stable" />
+                <input propertyname="build.stability.api" defaultvalue="stable"  message="API stability" validArgs="snapshot,devel,alpha,beta,stable" />
+                <input propertyname="build.notes" message="Release notes" />
+                <input propertyname="build.phpdep" defaultvalue="5.2.0" message="PHP version required" />
+                <input propertyname="build.pearinstallerdep" defaultvalue="1.9.0" message="PEAR installer version required" />
+            </else>
+        </if>
+
+        <fileset dir="${build.srcdir}" id="core">
+            <include name="phergie.php" />
+            <include name="phergie.bat" />
+            <include name="LICENSE" />
+            <include name="Settings.php.dist" />
+            <include name="Phergie/Autoload.php" />
+            <include name="Phergie/Bot.php" />
+            <include name="Phergie/Config/Exception.php" />
+            <include name="Phergie/Config.php" />
+            <include name="Phergie/Connection/Exception.php" />
+            <include name="Phergie/Connection/Handler.php" />
+            <include name="Phergie/Connection.php" />
+            <include name="Phergie/Db/Exception.php" />
+            <include name="Phergie/Db/Manager.php" />
+            <include name="Phergie/Db/Sqlite.php" />
+            <include name="Phergie/Driver/Abstract.php" />
+            <include name="Phergie/Driver/Exception.php" />
+            <include name="Phergie/Driver/Streams.php" />
+            <include name="Phergie/Event/Abstract.php" />
+            <include name="Phergie/Event/Command.php" />
+            <include name="Phergie/Event/Exception.php" />
+            <include name="Phergie/Event/Handler.php" />
+            <include name="Phergie/Event/Request.php" />
+            <include name="Phergie/Event/Response.php" />
+            <include name="Phergie/Exception.php" />
+            <include name="Phergie/Hostmask/Exception.php" />
+            <include name="Phergie/Hostmask.php" />
+            <include name="Phergie/Plugin/Abstract.php" />
+            <include name="Phergie/Plugin/Exception.php" />
+            <include name="Phergie/Plugin/Handler.php" />
+            <include name="Phergie/Ui/Abstract.php" />
+            <include name="Phergie/Ui/Console.php" />
+        </fileset>
+
+    </target>
+
+    <target name="core" depends="input">
+
+        <property name="build.tmpdir" value="Phergie-${build.version.release}" />
+        <property name="build.tarball" value="${build.dstdir}${build.tmpdir}.tgz" />
+
+        <delete file="${build.tarball}" quiet="true" />
+
+        <mkdir dir="${build.tmpdir}" />
+
+        <copy todir="${build.tmpdir}">
+            <fileset refid="core" />
+        </copy>
+
+        <reflexive file="${build.tmpdir}/Phergie/Bot.php">
+            <filterchain>
+                <replaceregexp>
+                    <regexp
+                        pattern="const VERSION = '[^']+';"
+                        replace="const VERSION = '${build.version.release}';"
+                    />
+                </replaceregexp>
+            </filterchain>
+        </reflexive>
+
+        <phergiepkg name="Phergie" dir="${build.tmpdir}">
+            <fileset refid="core" />
+            <option name="baseinstalldir" value="/" />
+            <option name="outputdirectory" value="${build.dstdir}" />
+            <option name="channel" value="pear.phergie.org" />
+            <option name="summary" value="Phergie core library" />
+            <option name="description" value="The Phergie package provides all files necessary to run a basic IRC bot." />
+            <option name="apiversion" value="${build.version.api}" />
+            <option name="apistability" value="${build.stability.api}" />
+            <option name="releaseversion" value="${build.version.release}" />
+            <option name="releasestability" value="${build.stability.release}" />
+            <option name="phpdep" value="${build.phpdep}" />
+            <option name="pearinstallerdep" value="${build.pearinstallerdep}" />
+            <option name="license" value="http://phergie.org/license New BSD License" />
+            <option name="packagetype" value="php" />
+            <option name="notes" value="${build.notes}" />
+            <mapping name="replacements">
+                <element>
+                    <element key="path" value="phergie.php" />
+                    <element key="type" value="pear-config" />
+                    <element key="from" value="/usr/bin/env php" />
+                    <element key="to" value="php_bin" />
+                </element>
+                <element>
+                    <element key="path" value="phergie.bat" />
+                    <element key="type" value="pear-config" />
+                    <element key="from" value="@php_bin@" />
+                    <element key="to" value="php_bin" />
+                </element>
+                <element>
+                    <element key="path" value="phergie.bat" />
+                    <element key="type" value="pear-config" />
+                    <element key="from" value="@bin_dir@" />
+                    <element key="to" value="bin_dir" />
+                </element>
+            </mapping>
+            <mapping name="exceptions">
+                <element key="phergie.php" value="script" />
+                <element key="phergie.bat" value="script" />
+            </mapping>
+            <mapping name="releases">
+                <element>
+                    <element key="installconditions">
+                        <element key="os" value="windows" />
+                    </element>
+                    <element key="filelist">
+                        <element key="install">
+                            <element key="phergie.php" value="phergie" />
+                        </element>
+                    </element>
+                </element>
+                <element>
+                    <element key="filelist">
+                        <element key="install">
+                            <element key="phergie.php" value="phergie" />
+                        </element>
+                        <element key="ignore">
+                            <element value="phergie.bat" />
+                        </element>
+                    </element>
+                </element>
+            </mapping>
+            <mapping name="deps">
+                <element>
+                    <element key="type" value="ext" />
+                    <element key="name" value="pcre" />
+                </element>
+                <element>
+                    <element key="type" value="ext" />
+                    <element key="name" value="reflection" />
+                </element>
+            </mapping>
+        </phergiepkg>
+
+        <phingcall target="build" />
+
+        <phingcall target="clean" />
+
+    </target>
+
+    <target name="plugin" depends="input">
+        
+        <if>
+            <equals arg1="${have_properties_file}" arg2="true" />
+            <then>
+                <property file="./build.properties" />
+            </then>
+            <else>
+                <input propertyname="build.plugin" message="Short plugin name" />
+                <input propertyname="build.summary" message="Plugin summary" />
+                <input propertyname="build.description" message="Plugin description" />
+            </else>
+        </if>
+
+        <property name="build.class" value="Phergie_Plugin_${build.plugin}" />
+        <property name="build.tmpdir" value="${build.class}-${build.version.release}" />
+        <property name="build.tarball" value="${build.dstdir}${build.tmpdir}.tgz" />
+
+        <fileset dir="${build.srcdir}" id="plugin">
+            <include name="Phergie/Plugin/${build.plugin}.php" />
+            <include name="Phergie/Plugin/${build.plugin}/**" />
+        </fileset>
+
+        <delete file="${build.tarball}" quiet="true" />
+
+        <mkdir dir="${build.tmpdir}" />
+
+        <copy todir="${build.tmpdir}">
+            <fileset refid="plugin" />
+        </copy>
+
+        <phergiepkg name="${build.class}" dir="${build.tmpdir}">
+            <fileset refid="plugin" />
+            <option name="baseinstalldir" value="/" />
+            <option name="outputdirectory" value="${build.dstdir}" />
+            <option name="channel" value="pear.phergie.org"/>
+            <option name="summary" value="${build.summary}"/>
+            <option name="description" value="${build.description}"/>
+            <option name="apiversion" value="${build.version.api}"/>
+            <option name="apistability" value="${build.stability.api}"/>
+            <option name="releaseversion" value="${build.version.release}"/>
+            <option name="releasestability" value="${build.stability.release}"/>
+            <option name="phpdep" value="${build.phpdep}" />
+            <option name="pearinstallerdep" value="${build.pearinstallerdep}" />
+            <option name="license" value="http://phergie.org/license New BSD License"/>
+            <option name="packagetype" value="php"/>
+            <option name="notes" value="${build.notes}"/>
+        </phergiepkg>
+
+        <phingcall target="build" />
+
+        <phingcall target="clean" />
+
+    </target>
+
+    <target name="docs" depends="input">
+        
+        <property name="build.tmpdir" value="Phergie_Docs-${build.version.release}" />
+        <property name="build.tarball" value="${build.dstdir}${build.tmpdir}.tgz" />
+
+        <delete file="${build.tarball}" quiet="true" />
+
+        <mkdir dir="${build.tmpdir}" />
+
+        <phpdocumentor title="API Documentation"
+            destdir="${build.tmpdir}/api"
+            output="HTML:Smarty:PHP">
+            <fileset refid="core" />
+            <projdocfileset dir=".">
+                <include name="LICENSE" />
+            </projdocfileset>
+        </phpdocumentor>
+
+        <phergiepkg name="Phergie_Docs" dir="${build.tmpdir}">
+            <fileset dir="${build.tmpdir}">
+                <include name="api**" />
+            </fileset>
+            <option name="baseinstalldir" value="/" />
+            <option name="outputdirectory" value="${build.dstdir}" />
+            <option name="channel" value="pear.phergie.org" />
+            <option name="summary" value="Phergie core library documentation" />
+            <option name="description" value="The Phergie_Docs package provides documentation for the Phergie core libraries." />
+            <option name="apiversion" value="${build.version.api}" />
+            <option name="apistability" value="${build.stability.api}" />
+            <option name="releaseversion" value="${build.version.release}" />
+            <option name="releasestability" value="${build.stability.release}" />
+            <option name="phpdep" value="${build.phpdep}" />
+            <option name="pearinstallerdep" value="${build.pearinstallerdep}" />
+            <option name="license" value="http://phergie.org/license New BSD License" />
+            <option name="packagetype" value="php" />
+            <option name="notes" value="${build.notes}" />
+            <mapping name="exceptions">
+                <element key="api" value="doc" />
+            </mapping>
+        </phergiepkg>
+
+        <phingcall target="build" />
+
+        <phingcall target="clean" />
+
+    </target>
+
+    <target name="build">
+
+        <tar destfile="${build.tarball}" compression="gzip">
+            <fileset dir="${build.dstdir}">
+                <include name="${build.tmpdir}**" />
+                <include name="package.xml" />
+            </fileset>
+        </tar>
+
+    </target>
+
+    <target name="clean">
+
+        <if>
+            <istrue value="${clean}" />
+            <then>
+                <delete dir="${build.tmpdir}" />
+                <delete file="${build.dstdir}package.xml" />
+            </then>
+        </if>
+
+    </target>
+
+</project>
diff --git a/plugins/Irc/extlib/phergie/phergie.bat b/plugins/Irc/extlib/phergie/phergie.bat
new file mode 100644 (file)
index 0000000..4eec11d
--- /dev/null
@@ -0,0 +1,14 @@
+@echo off
+REM  Phergie 
+REM 
+REM  PHP version 5
+REM 
+REM  LICENSE
+REM 
+REM  This source file is subject to the new BSD license that is bundled
+REM  with this package in the file LICENSE.
+REM  It is also available through the world-wide-web at this URL:
+REM  http://phergie.org/license
+
+set PHPBIN="@php_bin@"
+%PHPBIN% "@bin_dir@\phergie" %*
diff --git a/plugins/Irc/extlib/phergie/phergie.php b/plugins/Irc/extlib/phergie/phergie.php
new file mode 100755 (executable)
index 0000000..f0b9f6c
--- /dev/null
@@ -0,0 +1,54 @@
+#!/usr/bin/env php
+<?php
+/**
+ * Phergie 
+ *
+ * PHP version 5
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.
+ * It is also available through the world-wide-web at this URL:
+ * http://phergie.org/license
+ *
+ * @category  Phergie
+ * @package   Phergie
+ * @author    Phergie Development Team <team@phergie.org>
+ * @copyright 2008-2010 Phergie Development Team (http://phergie.org)
+ * @license   http://phergie.org/license New BSD License
+ * @link      http://pear.phergie.org/package/Phergie
+ */
+
+/**
+ * @see Phergie_Autoload
+ */
+require 'Phergie/Autoload.php';
+Phergie_Autoload::registerAutoloader();
+
+$bot = new Phergie_Bot;
+
+if (!isset($argc)) {
+    echo
+        'The PHP setting register_argc_argv must be enabled for Phergie ', 
+        'configuration files to be specified using command line arguments; ',
+        'defaulting to Settings.php in the current working directory',
+        PHP_EOL;
+} else if ($argc > 0) {
+    // Skip the current file for manual installations
+    // ex: php phergie.php Settings.php
+    if (realpath($argv[0]) == __FILE__) {
+        array_shift($argv);
+    }
+
+    // If configuration files were specified, override default behavior
+    if (count($argv) > 0) {
+        $config = new Phergie_Config;
+        foreach ($argv as $file) {
+            $config->read($file);
+        }
+        $bot->setConfig($config);
+    }
+}
+
+$bot->run();
diff --git a/plugins/Irc/ircmanager.php b/plugins/Irc/ircmanager.php
new file mode 100644 (file)
index 0000000..5f55e6b
--- /dev/null
@@ -0,0 +1,356 @@
+<?php
+/*
+ * StatusNet - the distributed open-source microblogging tool
+ * Copyright (C) 2008, 2009, StatusNet, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 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 Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); }
+
+/**
+ * IRC background connection manager for IRC-using queue handlers,
+ * allowing them to send outgoing messages on the right connection.
+ *
+ * Input is handled during socket select loop, Any incoming messages will be handled.
+ *
+ * In a multi-site queuedaemon.php run, one connection will be instantiated
+ * for each site being handled by the current process that has IRC enabled.
+ */
+
+class IrcManager extends ImManager {
+    protected $conn = null;
+    protected $lastPing = null;
+    protected $messageWaiting = true;
+    protected $lastMessage = null;
+
+    protected $regChecks = array();
+    protected $regChecksLookup = array();
+
+    protected $connected = false;
+
+    /**
+     * Initialize connection to server.
+     *
+     * @return boolean true on success
+     */
+    public function start($master) {
+        if (parent::start($master)) {
+            $this->connect();
+            return true;
+        } else {
+            return false;
+        }
+    }
+
+    /**
+    * Return any open sockets that the run loop should listen
+    * for input on.
+    *
+    * @return array Array of socket resources
+    */
+    public function getSockets() {
+        $this->connect();
+        if ($this->conn) {
+            return $this->conn->getSockets();
+        } else {
+            return array();
+        }
+    }
+
+    /**
+     * Request a maximum timeout for listeners before the next idle period.
+     *
+     * @return integer Maximum timeout
+     */
+    public function timeout() {
+        if ($this->messageWaiting) {
+            return 1;
+        } else {
+            return 120;
+        }
+    }
+
+    /**
+     * Idle processing for io manager's execution loop.
+     *
+     * @return void
+     */
+    public function idle() {
+        // Send a ping if necessary
+        if (empty($this->lastPing) || time() - $this->lastPing > 120) {
+            $this->sendPing();
+        }
+
+        if ($this->connected) {
+            // Send a waiting message if appropriate
+            if ($this->messageWaiting && time() - $this->lastMessage > 1) {
+                $wm = Irc_waiting_message::top();
+                if ($wm === NULL) {
+                    $this->messageWaiting = false;
+                    return;
+                }
+
+                $data = unserialize($wm->data);
+                $wm->incAttempts();
+
+                if ($this->send_raw_message($data)) {
+                    $wm->delete();
+                } else {
+                    if ($wm->attempts <= common_config('queue', 'max_retries')) {
+                        // Try again next idle
+                        $wm->releaseClaim();
+                    } else {
+                        // Exceeded the maximum number of retries
+                        $wm->delete();
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * Process IRC events that have come in over the wire.
+     *
+     * @param resource $socket Socket to handle input on
+     * @return void
+     */
+    public function handleInput($socket) {
+        common_log(LOG_DEBUG, 'Servicing the IRC queue.');
+        $this->stats('irc_process');
+
+        try {
+            $this->conn->handleEvents();
+        } catch (Phergie_Driver_Exception $e) {
+            $this->connected = false;
+            $this->conn->reconnect();
+        }
+    }
+
+    /**
+    * Initiate connection
+    *
+    * @return void
+    */
+    public function connect() {
+        if (!$this->conn) {
+            $this->conn = new Phergie_StatusnetBot;
+
+            $config = new Phergie_Config;
+            $config->readArray(
+                array(
+                    'connections' => array(
+                        array(
+                            'host' => $this->plugin->host,
+                            'port' => $this->plugin->port,
+                            'username' => $this->plugin->username,
+                            'realname' => $this->plugin->realname,
+                            'nick' => $this->plugin->nick,
+                            'password' => $this->plugin->password,
+                            'transport' => $this->plugin->transporttype,
+                            'encoding' => $this->plugin->encoding
+                        )
+                    ),
+
+                    'driver' => 'statusnet',
+
+                    'processor' => 'async',
+                    'processor.options' => array('sec' => 0, 'usec' => 0),
+
+                    'plugins' => array(
+                        'Pong',
+                        'NickServ',
+                        'AutoJoin',
+                        'Statusnet',
+                    ),
+
+                    'plugins.autoload' => true,
+
+                    'ui.enabled' => true,
+
+                    'nickserv.password' => $this->plugin->nickservpassword,
+                    'nickserv.identify_message' => $this->plugin->nickservidentifyregexp,
+
+                    'autojoin.channels' => $this->plugin->channels,
+
+                    'statusnet.messagecallback' => array($this, 'handle_irc_message'),
+                    'statusnet.regcallback' => array($this, 'handle_reg_response'),
+                    'statusnet.connectedcallback' => array($this, 'handle_connected'),
+                    'statusnet.unregregexp' => $this->plugin->unregregexp,
+                    'statusnet.regregexp' => $this->plugin->regregexp
+                )
+            );
+
+            $this->conn->setConfig($config);
+            $this->conn->connect();
+            $this->lastPing = time();
+            $this->lastMessage = time();
+        }
+        return $this->conn;
+    }
+
+    /**
+    * Called via a callback when a message is received
+    * Passes it back to the queuing system
+    *
+    * @param array $data Data
+    * @return boolean
+    */
+    public function handle_irc_message($data) {
+        $this->plugin->enqueue_incoming_raw($data);
+        return true;
+    }
+
+    /**
+    * Called via a callback when NickServ responds to
+    * the bots query asking if a nick is registered
+    *
+    * @param array $data Data
+    * @return void
+    */
+    public function handle_reg_response($data) {
+        // Retrieve data
+        $screenname = $data['screenname'];
+        $nickdata = $this->regChecks[$screenname];
+        $usernick = $nickdata['user']->nickname;
+
+        if (isset($this->regChecksLookup[$usernick])) {
+            if ($data['registered']) {
+                // Send message
+                $this->plugin->send_confirmation_code($screenname, $nickdata['code'], $nickdata['user'], true);
+            } else {
+                $this->plugin->send_message($screenname, _m('Your nickname is not registered so IRC connectivity cannot be enabled'));
+
+                $confirm = new Confirm_address();
+
+                $confirm->user_id      = $user->id;
+                $confirm->address_type = $this->plugin->transport;
+
+                if ($confirm->find(true)) {
+                    $result = $confirm->delete();
+
+                    if (!$result) {
+                        common_log_db_error($confirm, 'DELETE', __FILE__);
+                        // TRANS: Server error thrown on database error canceling IM address confirmation.
+                        $this->serverError(_('Couldn\'t delete confirmation.'));
+                        return;
+                    }
+                }
+            }
+
+            // Unset lookup value
+            unset($this->regChecksLookup[$usernick]);
+
+            // Unset data
+            unset($this->regChecks[$screename]);
+        }
+    }
+
+    /**
+    * Called when the connection is established
+    *
+    * @return void
+    */
+    public function handle_connected() {
+        $this->connected = true;
+    }
+
+    /**
+    * Enters a message into the database for sending when ready
+    *
+    * @param string $command Command
+    * @param array $args Arguments
+    * @return boolean
+    */
+    protected function enqueue_waiting_message($data) {
+        $wm = new Irc_waiting_message();
+
+        $wm->data       = serialize($data);
+        $wm->prioritise = $data['prioritise'];
+        $wm->attempts   = 0;
+        $wm->created    = common_sql_now();
+        $result         = $wm->insert();
+
+        if (!$result) {
+            common_log_db_error($wm, 'INSERT', __FILE__);
+            throw new ServerException('DB error inserting IRC waiting queue item');
+        }
+
+        return true;
+    }
+
+    /**
+     * Send a message using the daemon
+     *
+     * @param $data Message data
+     * @return boolean true on success
+     */
+    public function send_raw_message($data) {
+        $this->connect();
+        if (!$this->conn) {
+            return false;
+        }
+
+        if ($data['type'] != 'delayedmessage') {
+            if ($data['type'] != 'message') {
+                // Nick checking
+                $nickdata = $data['nickdata'];
+                $usernick = $nickdata['user']->nickname;
+                $screenname = $nickdata['screenname'];
+
+                // Cancel any existing checks for this user
+                if (isset($this->regChecksLookup[$usernick])) {
+                    unset($this->regChecks[$this->regChecksLookup[$usernick]]);
+                }
+
+                $this->regChecks[$screenname] = $nickdata;
+                $this->regChecksLookup[$usernick] = $screenname;
+            }
+
+            // If there is a backlog or we need to wait, queue the message
+            if ($this->messageWaiting || time() - $this->lastMessage < 1) {
+                $this->enqueue_waiting_message(
+                    array(
+                        'type' => 'delayedmessage',
+                        'prioritise' => $data['prioritise'],
+                        'data' => $data['data']
+                    )
+                );
+                $this->messageWaiting = true;
+                return true;
+            }
+        }
+
+        try {
+            $this->conn->send($data['data']['command'], $data['data']['args']);
+        } catch (Phergie_Driver_Exception $e) {
+            $this->connected = false;
+            $this->conn->reconnect();
+            return false;
+        }
+
+        $this->lastMessage = time();
+        return true;
+    }
+
+    /**
+    * Sends a ping
+    *
+    * @return void
+    */
+    protected function sendPing() {
+        $this->lastPing = time();
+        $this->conn->send('PING', $this->lastPing);
+    }
+}