-
Notifications
You must be signed in to change notification settings - Fork 27
Development
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.
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.
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.
Plugins can listen for connection and IRC events supported by the underlying client as well as the events shown below.
- irc.received.each - occurs when any type of event is received from a server
-
irc.received.TYPE - occurs when an event of type
TYPE
(e.g.privmsg
) is received from a server - irc.sending.all - occurs after an event has been processed by all plugins, at which point all synchronous responses have been enqueued
- irc.sending.each - occurs before any type of event is sent to a server
-
irc.sending.TYPE - occurs before an event of type
TYPE
(e.g.privmsg
) is sent to a server - irc.sent.each - occurs after any type of event is sent to a server
-
irc.sent.TYPE - occurs when an event of type
TYPE
(e.g.privmsg
) is sent to a server
Valid values for TYPE
are lowercase strings that include the following:
-
IRC client commands, e.g.
'privmsg'
or'notice'
; -
IRC server reply names, e.g.
'err_nosuchnick'
or'rpl_listend'
; and -
CTCP client commands prefixed with the string
'ctcp.'
, e.g.'ctcp.action'
.
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:
-
$event
, an object that contains data about the event and implementsEventInterface
or subinterfaces of it such asUserEventInterface
,ServerEventInterface
, andCtcpEventInterface
; and -
$queue
, an object that sends events back to the server that sent the original event and implementsEventQueueInterface
, a subinterface ofGeneratorInterface
where most of its methods are found.
One exception to this is the 'irc.sending.all'
event, which takes only the $queue
parameter.
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 implementsUserEventInterface
-
$queue
, an object that implementsEventQueueInterface
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.
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 object for the connection, 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
}
}
}
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.
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.
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.
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.
-
'client'
- an object that implementsClientInterface
and is used for low-level IRC client-server interactions and emission of custom events, e.g.Client
-
'parser'
- an object that implementsParserInterface
and is used to parse data from streams of IRC interactions, e.g.Parser
-
'converter'
- an object that implementsParserConverterInterface
and is used to convert parsed IRC interaction data into event objects, e.g.ParserConverter
-
'eventQueueFactory'
- an object that implementsEventQueueFactoryInterface
and is used to obtain connection-specific objects that implementEventQueueInterface
-- such asEventQueue
-- for sending events to IRC servers, e.g.EventQueueFactory
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
);
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:
-
EventEmitterInjector
injects the plugin with the bot's client if the plugin implementsEventEmitterAwareInterface
. If so, the plugin can use the client to emit events to which other plugins can subscribe. -
EventQueueFactoryInjector
injects the plugin with the bot's event queue factory if the plugin implementsEventQueueFactoryAwareInterface
. If so, the plugin can access event queues for all connections, which is useful when emitting asynchronous and timed events. -
ClientInjector
injects the plugin with the bot's client if the plugin implementsClientAwareInterface
for contexts in which the client is not being used solely as an event emitter. -
LoggerInjector
injects the plugin with the same logger used by the bot if the plugin implementsLoggerAwareInterface
. -
LoopInjector
injects the plugin with the event loop used by the bot's client if the client implementsLoopAccessorInterface
and the plugin implementsLoopAwareInterface
. If so, the plugin can use the event loop to execute stream and timed operations.
To run the phergie-irc-bot-react unit test suite:
curl -s https://getcomposer.org/installer | php
php composer.phar install
cd tests
../vendor/bin/phpunit