Developing Plugins

Why Plugins?

Plugins should be written when you either want to intercept events received from the IRC server and respond to them in some way or when you want to asynchronously send events to the IRC server. If the problem you're trying to solve with your code doesn't involve interacting with the IRC server in some way, it's probably better to structure it as a library for other plugins to use.

What You Need

The phergie-scaffold tool was created to automate the creation of files commonly included by plugin repositories. See its GitHub repository for more information on installing and using it.

Alternatively, create a directory containing a composer.json file with the contents shown below, then run composer install. This will install the bot and all its dependencies. See Composer documentation for further information.

{
  "require": {
    "phergie/phergie-irc-bot-react": "~1"
  }
}

As you're reviewing this page, the API documentation may come in handy.

top

Plugins Defined

Plugins are classes that implement the PluginInterface interface. This interface contains a single method, getSubscribedEvents(), which returns an associative array in which the keys are event names and the values are either valid callbacks or names of instance methods in the plugin class to handle those events (i.e. 'methodName' as a convenient shorthand for array($this, 'methodName')).

use Phergie\Irc\Event\EventInterface as Event;
use Phergie\Irc\Bot\React\EventQueueInterface as Queue;
use Phergie\Irc\Bot\React\PluginInterface;

class ExamplePlugin implements PluginInterface
{
    public function getSubscribedEvents()
    {
        return array(
            'irc.received.privmsg' => 'onPrivmsg'
        );
    }

    public function onPrivmsg(Event $event, Queue $queue)
    {
        // ...
    }
}

In the above example, 'irc.received.privmsg' is an event name and 'onPrivmsg' is the name of a method in the ExamplePlugin class to handle that event.

top

Supported Events

IRC Events

Plugins can listen for connection and IRC events supported by the underlying client as well as the events shown below.

Valid values for TYPE are lowercase strings that include the following:

Note that the bot handles sending connection registration events, so there's no need for a plugin to do so, but plugins can still subscribe to these events. For example, to have a plugin execute a callback once per connection, have it subscribe to the irc.event.user event.

IRC event handler methods typically accept two parameters:

One exception to this is the 'irc.sending.all' event, which takes only the $queue parameter.

top

Responding to Events

Plugins typically respond to events by sending a command to the server, such as sending a message back to the channel in which an event occurred. This is done using the $queue parameter passed to the event handler method as described at the end of the previous section. Here's an example:

use Phergie\Irc\Event\UserEventInterface as Event;
use Phergie\Irc\Bot\React\EventQueueInterface as Queue;
use Phergie\Irc\Bot\React\PluginInterface;

class GreeterPlugin implements PluginInterface
{
    public function getSubscribedEvents()
    {
        return array(
            'irc.received.join' => 'onJoin',
        );
    }

    public function onJoin(Event $event, Queue $queue)
    {
        $channel = $event->getSource();
        $message = 'Welcome to the channel ' . $event->getNick() . '!';
        $queue->ircPrivmsg($channel, $message);
    }
}

This plugin has subscribed to the 'irc.received.join' event, which occurs whenever a user joins a channel in which the bot is present. When this event occurs, the plugin's onJoin() method is invoked.

This method receives two parameters: * $event, an object that implements UserEventInterface * $queue, an object that implements EventQueueInterface

To get the channel in which the original event occurred, onJoin() invokes $event->getSource(). If the event occurs in a channel, getSource() will return the name of that channel. If the event occurs as a direct interaction between another user and the bot, getSource() will instead return the nickname of that user.

'irc.received.join' events always occur in channels. To address a user who joins a channel, onJoin() needs that user's nickname, which it obtains using $event->getNick(). This method always returns the nickname of the event's originating user, as opposed to $event->getSource() which returns either that nickname or the name of a channel depending on the context of the event.

Finally, onJoin() invokes $queue->ircPrivmsg() to send the constructed message back to the channel.

top

Asynchronous and Timed Events

To have a plugin execute a method on a short (normally sub-second) interval, have it subscribe to the 'irc.tick' event. To have more control over the interval, your plugin must access the event loop of the bot's client. For this to happen, the plugin must implement LoopAwareInterface.

use Phergie\Irc\Client\React\LoopAwareInterface;
use Phergie\Irc\Bot\React\PluginInterface;
use React\EventLoop\LoopInterface;

class Plugin implements PluginInterface, LoopAwareInterface
{
    public function setLoop(LoopInterface $loop)
    {
        $loop->addPeriodicTimer(
            5,                            // Every 5 seconds...
            array($this, 'timedCallback') // ... execute this callback
        );
    }

    public function timedCallback()
    {
        // ...
    }
}

Sending events to a server requires access to an event queue for the connection to that server (i.e. each connection has its own event queue). To do this, plugins must first access the connection object, then use that together with the event queue factory to access that connection's event queue object. Here's an example of a plugin that does this:

use Phergie\Irc\Client\React\LoopAwareInterface;
use Phergie\Irc\ConnectionInterface;
use Phergie\Irc\Bot\React\AbstractPlugin;
use React\EventLoop\LoopInterface;
use React\EventLoop\Timer\TimerInterface;

class Plugin extends AbstractPlugin implements LoopAwareInterface
{
    // Getting connection objects:

    protected $connections;

    public function getSubscribedEvents()
    {
        return [
            'connect.after.each' => 'addConnection',
        ];
    }

    public function addConnection(ConnectionInterface $connection)
    {
        $this->getConnections()->attach($connection);
    }

    public function getConnections()
    {
        if (!$this->connections) {
            $this->connections = new \SplObjectStorage;
        }
        return $this->connections;
    }

    // Setting up the asynchronous callback that uses the event queue:

    public function setLoop(LoopInterface $loop)
    {
        $loop->addPeriodicTimer(30, array($this, 'myTimerCallback'));
    }

    public function myTimerCallback(TimerInterface $timer)
    {
        $factory = $this->getEventQueueFactory();
        foreach ($this->getConnections() as $connection) {
            $queue = $factory->getEventQueue($connection);
            // Use the queue to do whatever you like
        }
    }
}

top

Custom Events

In addition to the core supported events that plugins can send and receive, they can also communicate with each other by sending and receiving custom events. To do this, they must implement EventEmitterAwareInterface. Though this is relatively trivial to do, as the interface only contains a single setEventEmitter() method, a shortcut to doing so is to extend AbstractPlugin, which provides an implementation of the interface.

Once obtained via setEventEmitter(), the event emitter object (which implements EventEmitterInterface) has an emit() method that can be used to emit an event that any plugins subscribed to it will receive.

$eventEmitter->emit('namespace.event.subevent', $parameters);

Event names are specified as strings. They are conventionally namespaced to avoid naming collisions with other plugins, with name segments delimited using periods.

$parameters is an array of parameter values received by event handler methods of subscribed plugins.

top

Logging

Plugins can gain access to the same logger instance used by core logic by implementing LoggerAwareInterface. Though this is relatively trivial to do, as the interface only contains a single setLogger() method, a shortcut to doing so is to extend AbstractPlugin, which provides an implementation of the interface.

Once obtained via setLogger(), the logger object can be used to log whatever events may be relevant to monitoring or debugging the plugin. In particular, one noteworthy shortcoming of Phergie's use of event callbacks is that there's no way to accurately attribute events sent by plugins in log messages (for debugging purposes) that isn't extremely hacky. As such, logging a message when a plugin sends an event is an advisable practice.

top

Installation

Plugins are conventionally installed using composer. To support this, a composer.json file should be included with the plugin source code that provides information about the plugin and any dependencies it has on other plugins or libraries. See the composer.json files included with existing plugins for examples.

top

Bot Customization

Dependencies

The bot is represented by the Bot class, which is used by the bot runner. In addition to the logger, this class supports replacing other dependencies via configuration.

Here's an example configuration file that implements overrides of these dependencies:

return array(
  'connections' => array(
    // ...
  ),
  'plugins' => array(
    // ...
  ),
  'client' => new My\Client,
  'parser' => new My\Parser,
  'converter' => new My\Converter,
  'eventQueueFactory' => new My\EventQueueFactory
);

top

Plugin Processors

Plugins sometimes require some common form of dependency injection or other modification after they're loaded. This is handled by plugin processors, which can be set via the 'pluginProcessors' configuration key as an array of objects implementing PluginProcessorInterface. If no value is set, by default, the bot will use these plugin processors:

top

Core Development

Running Tests

To run the phergie-irc-bot-react unit test suite:

curl -s https://getcomposer.org/installer | php
php composer.phar install
php composer.phar require phpunit/phpunit
cd tests
../vendor/bin/phpunit

top