Designing Perlbot Plugins
=========================



1) So you want to write a plugin
--------------------------------

This document is meant for people who want to design their own perlbot
plugin, or anyone who's just interested in how the plugin system works.

Here are the things you'll need:

- You obviously need to know perl.  In particular, you'll need a clear
  understanding of perl references and classes/objects.  The "perlref" and
  "perltoot" man pages have all you need to know.  You won't need to
  design any classes, but you'll need to deal with some objects from
  Net::IRC and perlbot itself.

- Familiarity with the Event and Connection classes from Net::IRC, since
  perlbot uses Net::IRC to handle its connection to IRC servers.  See
  plugin-events.txt for a list of events (copied straight from
  Net/IRC/Event.pm).

- An understanding of the internal classes that perlbot uses to handle
  users and channels: User and Chan (see User.pm and Chan.pm).

The way a plugin interfaces to IRC is by listening for events.  As a
plugin author, you decide which event types to listen for, and write a
subroutine to deal with each type (it's possible for one sub to handle
more than one type of event, but not recommended).  We call these
subroutines 'handler subs'.  The events that are most common for a plugin
to handle are 'public' which is generated when someone says something
publicly to a channel, and 'msg' which is generated when someone privately
messages perlbot.  See plugin-events.txt for the full list. 



2) Handler subs
---------------

Your handler subs will be passed 2 arguments.  The first is the
Net::IRC::Connection object representing perlbot's connection to its IRC
server.  The second is the Net::IRC::Event object representing the event
that is being handled by your sub. So each of your handler subs should
probably start off with this line:

	my ($conn, $event) = @_;

A note on the 'my': We strongly suggest that you 'use strict' in your
perlbot plugins, and all your other perl code too.  It really helps you
keep track of all your variables, and helps you avoid bugs caused by
misspelled variables.  If you need to declare a variable that's visible to
other plugins or perl modules, you can 'use vars'.  See the perldoc
entries for 'strict' and 'vars' if you don't have experience with them.

You need to decide what sort of behavior each event should trigger.  Let's
pretend you're writing a plugin to pull headlines from the (fictional) 
website, www.perlbot-news.com.  You might choose to handle the 'public'
and 'msg' events, and display the headlines if the text from these events
begins with '!perlbotnews'.  This would allow people to say '!perlbotnews'
in a channel with the bot, or in a private message to it, in order to
retrieve the headlines.  If someone messages the bot privately, your
plugin should send back the headlines via a private message.  If the key
phrase is said publicly in a channel, you might choose to send the
headlines publicly to the channel as well, or privately message the user
instead, so as not to bother the other users in the channel.  The best
option might be to allow the person running the bot to control this
behavior through a configuration file.  See Section 5 for more info on
configuration files.



3) Overall plugin structure
---------------------------

You're probably wondering where to put these handler subs that you're
going to write, and how to tell perlbot which subs are supposed to handle
which events.  The file to put your subs in should be called 'Plugin.pm'
and it should be placed in "plugins/PluginName".  Also, you need to put
the line "package PluginName::Plugin;" at the top of that file, and the
line "1;" at the bottom of the file (these 2 lines are to satisfy the perl
module loading mechanism).  Put all your subs and global variables in
between these 2 lines.

There's one last step.  You need to write a sub called "get_hooks", and
next is a description of what get_hooks must return.  The description
might be difficult to understand now, but the example below will surely
clear things up.  get_hooks must return a reference to a hash, where each
key/value pair is the name of an event you want to hook and a reference to
the sub you want to handle that event.  Don't worry, it's not as bad as it
sounds.  Here's an example that will cause your public_handler sub to
handle 'public' events, and your message_handler sub to handle 'msg'
events:

	sub get_hooks {
	  return {
	    public => \&public_handler,
	    msg    => \&message_handler
	  }
	}

So if you were writing a plugin called "Foo" that should handle 'public'
and 'msg' events, you would have a file called "plugins/Foo/Plugin.pm" and
it would look something like this:

	package Foo::Plugin;

	sub get_hooks {
	  return {
	    public => \&on_public,
	    msg    => \&on_msg
	  }
	}

	sub on_public {
	  # code here to handle 'public' events
	}

	sub on_msg {
	  # code here to handle 'msg' events
	}

	1;

You are free to write other subs that don't directly handle events, but
rather are called from the handler subs.  You may also split some of your
code into separate source files if it starts to get too big for one file,
but keep it all under your plugin's directory.



4) Access to global variables
-----------------------------

Many plugins will want to get at global variables such as the hashes of
User and Chan objects, and also the many utility functions that simplify
common tasks.  To do this, you can 'use Perlbot' in your plugin to have
all the symbols imported into your namespace.  Or, if you don't want all
those symbols dumped into your namespace for some reason, you can just
access them via their fully qualified name.  For example, %Perlbot::users
would be the %users hash.  See the section on "global variables" in
developer.txt for details about what the Perlbot module provides.

We've written what we think are an appropriate set of utility functions in
Perlbot.pm, but if you find yourself writing the same little bit of code
over and over, feel free to write your own utility function(s) inside your
plugin.  If you think it's something that would be of use to all plugin
authors, email the perlbot authors and let us know.



5) Config files
---------------

Perlbot provides a robust mechanism for parsing configuration files with a
special "class"-like syntax.  That is, they somewhat resemble the
definition of a class in languages like C++.  Perlbot's main configuration
file uses this syntax too.  See perlbot.txt for more information about how
the configuration files work.  Here's a quick description:

- blank or all-whitespace lines and lines starting with '#' are ignored

- whitespace within each line is ignored

- "class" definitions start with 'classname {' ALL ON ONE LINE by itself

- definitions end with '}' on a line by itself

- the body of the definition is made up of lines with the format
  'name value' where "name" is a field name with no spaces, and "value" is
  a string that is the value of that field.

Please do read perlbot.txt for a more detailed discussion with examples.

If you'd like to support a configuration file for your plugin that uses
this syntax, you can use the sub from the Perlbot module (see Section 4)
called, appropriately enough, parse_config .  parse_config takes one
argument, which is the name of the configuration file.  There is one
caveat: the current working directory for perlbot is the main perlbot
directory, but your configuration file will be 2 directories below that,
in the subdirectory that holds your plugin.  Use the $plugindir variable
from the Perlbot module to form the appropriate directory name.  If you
were writing a plugin called "Foo", you would look for your configuration
file in the directory "$plugindir/Foo/".

parse_config returns either a reference to a hash if the file parsed
correctly, or an error string if there was a parsing error.  The hash ref
that it returns on success is the base for a multi-level arrangement of
hash and array references that stores all the data from the configuration
file.  The structure might seem like a real mess at first, but once you
fully understand how it's set up, you'll see how powerful it is.  From
here on, we'll refer to the hash ref returned by parse_config as $hash.

Let's use this example configuration file.  The plugin that it configures
might be one that provides customized greeting messages from perlbot when
a user enters a channel.

	main {
	  delay     3
	}

	user {
	  name      timmy
	  quote     woop!
	  quote     Hey everyone, it's timmy!
	}

	user {
	  name      hal9000
	  quote     What are you doing, Dave?
	}

1) $hash is a reference to a hash.  The keys in that hash are the names of
all the "classes" in your configuration file.  With this example, the hash
would have keys 'main' and 'user'.  The values for these keys are array
references.  So $hash->{main} and $hash->{user} are array refs. 

2) These arrays store all the different "objects" of that class.  So
$hash->{user}[0] represents 'timmy' and $hash->{user}[1] represents
hal9000.  $hash->{main}[0] represents the single 'main' object.  Note that
even if you only have one object of a given class (like 'main' in this
example), you still need the [0].  Each of these array elements stores
another hash reference.

3) The keys for these hashes are the field names from a given class.  So
$hash->{user}[0] and $hash->{user}[1] would have keys 'name' and 'quote'. 
The values for these keys are more array references.  So for example,
$hash->{user}[0]{quote} is an array ref. 

4) These arrays store all the different values for a given field. 
$hash->{user}[0]{quote}[0] is timmy's first quote, and
$hash->{user}[0]{quote}[1] is his second quote.


Maybe this will be easier to understand: If you were to explicitly
initialize $hash for the above example file, it would look like this: 

	$hash = {
	  'main' => [
	    {
	      'delay' => [3]
	    }
	  ],
	  'user' => [
	    {
	      'name'  => ["timmy"],
	      'quote' => ["woop!", "Hey everyone, it's timmy!"]
	    },
	    {
	      'name'  => ["hal9000"],
	      'quote' => ["What are you doing, Dave?"]
	    }
	  ]
	};

Like I said, it's sort of a mess.  But, we didn't intend for you to be
messing around with $hash in your main plugin code.  You're supposed to
call parse_config to get $hash, then set up some foreach loops to iterate
over the hash keys and arrays, to pull out all the values and initialize
some nicer structures that the rest of your code will use.  Here's the
code that parses the main perlbot configuration file (pulled straight from
PerlbotCore.pm in version 1.1.7, with some comments removed):

	my $ret = parse_config($CONFIG);

	# If $ret isn't a hash ref, it's an error string... :(
	if (ref($ret) ne 'HASH') {
	  print $ret, "\n";
	  exit(1);
	}

	foreach my $class ('user','bot','server','chan') {
	  foreach my $fields (@{$ret->{$class}}) {
	    &{$config_handlers{$class}}($fields);
	  }
	}
 
Note the 2 nested foreach loops.  The outer one loops $class over the 4
classes that it knows how to deal with.  Then the inner foreach loops over
@{$ret->{$class}}, which is the array of "objects" for "class" $class
(item 2 in the above description of the structure).  So $fields holds a
reference to the hash representing one object from the config file (item
3). 

Then there's a slightly tricky line inside the inner for loop.  We have a
hash set up, called %config_handlers, where the keys are each of the
"classes" we know how to deal with, and the values are references to subs
that handle an "object" from that class.  It's really just a robust way to
implement a "switch" statement in perl.  Here's the 'chan' handler, which
illustrates the different things you can do with the structure.

	chan => sub {
	  my $name = to_channel($_[0]->{name}[0]);
	  my $chan = new Chan($name, $_[0]->{flags}[0]);
	  foreach my $op (@{$_[0]->{op}}) {
	    $chan->add_op($op) if (exists($users{$op}));
	  }
	  if($_[0]->{logging}) {
	    $chan->logging($_[0]->{logging}[0]);
	  }
	  $channels{$name} = $chan;
	}

Let's go over it line by line (remember, $_[0] is the first parameter
passed to the sub, which contains the reference to the hash from step 3):

          my $name = to_channel($_[0]->{name}[0]);

This pulls out the first value for 'name' from this channel.  It
explicitly grabs the first one since it wouldn't make sense to have more
than one name.  If multiple names are given, the second and later ones are
simply ignored.  It then passes the name through to_channel which is in
Perlbot.pm.  to_channel appends a '#' on the front of a string if it
doesn't already have a '#' or '&'.  Thus a channel name that starts with
'#' can be given in the configuration file without the '#' if the owner is
a little lazy.

          my $chan = new Chan($name, $_[0]->{flags}[0]);

This creates a Chan object.  The constructor expects the channel name and
the flags for the channel.  The flags value is pulled from the 'flags'
field in the channel's definition in the configuration file. 

          foreach my $op (@{$_[0]->{op}}) {

This loops $op over each value for the 'op' field from this channel's
definition.  Each 'op' entry in a channel definition is the name of a user
who should be a channel operator (mode +o). 

             $chan->add_op($op) if (exists($users{$op}));

This adds $op as a channel op only if the user is an actual user.

         if($_[0]->{logging}) {

If the definition for this channel has a 'logging' field,

             $chan->logging($_[0]->{logging}[0]);

the value for the (first) 'logging' field is passed to Chan->logging
(expected values are 'on' or 'off'). 


Right now, you can only read configuration data with parse_config.  We're
looking into writing some code that will allow you to write a
configuration file back to disk.  It will most likely be the exact reverse
of parse_config; you will pass one of these big messy structures and a
filename to write_config, and it will write out a configuration file with
that data.



6) The plugin help facility
---------------------------

Perlbot provides a standard way for users to get online help for your
plugin.  If a user says "#help" or "!help" to perlbot, it will give them
the general syntax of the help command and a list of installed plugins. 
They can then message "#help pluginname" (or "!help...") for general help
on that plugin and a list of subtopics that they can look up for further
information.  "#help pluginname subtopic" returns help on that subtopic.

To specify the help messages for your plugin, create a file called
'help.txt' in your plugin's directory.  Blank lines in this file are
ignored. 

The first non-blank line should be the general help text for your plugin,
which the user will see when they say "#help pluginname".  If you want to
send multiple lines back to the user, separate the lines with '\\'.  The
entire string must be on ONE line!

The next non-blank line should be the name of a subtopic, and next should
be the help text for that subtopic.  Once again, the help text must be all
on one line, and \\ serves as a line separator.  The rest of the file
continues in this fashion, with subtopics and help text on alternating
lines.

In a future version of perlbot, we might write a parser that will allow
for a more natural help.txt format.  Perhaps something similar to the
current format, but with real carriage returns instead of \\, and 1 or
more blank lines to separate a help text entry from the next subtopic.



7) Forking / threading
----------------------

Any plugin that could potentially take a long time to do its thing (such
as retrieve a web page or call an external program) should create a
parallel thread of execution in which to do its own processing, to allow
perlbot to continue processing new events.  This may be accomplished via a
fork() call, but that creates an entire copy of the perlbot process, which
is quite inefficient.  With many such forks going on in rapid succession,
the system hosting the bot may run out of RAM and start thrashing. 

We're currently looking into something more lightweight, like threads.  If
you have any suggestions, please mail the authors. 



8) Sharing data or interfacing multiple plugins
-----------------------------------------------

You might want to write a plugin that provides some functionality for a
few other plugins.  For example, you might want to implement some online
games, so you write one plugin for each game, and one plugin to handle
general user settings for all the games.  In each of your game plugins,
you might want to find out some user setting, so you would need to
communicate with your settings plugin.  All you need to do is provide some
subroutine or global variable in the settings plugin (let's call it
GameSettings), and then reference $GameSettings::some_variable from your
different game plugins. 

Alternately, if you are familiar with the Exporter package, you could use
@EXPORT_OK to provide some symbols for export, but only if the 'use'ing
module asks for them.  Then, in your game plugins, you could just say:

	use GameSettings qw($some_variable);

and then use $some_variable without the 'GameSettings::' prefix.

You might also have shared files (perhaps a database of some sort) that
reside in the common plugin's directory. 

If your plugin requires another plugin, and you want to check to see if that
other plugin is installed, just grep @plugins (from the Perlbot module) for
the name of that plugin in your get_hooks sub.  If the grep fails, which means
the required plugin isn't installed, then you could say:

	return {};

which will effectively disable your plugin (but users will still see it in
the list of loaded plugins, and will be able to request help on it).

In the future we will probably make this process a little cleaner.  Maybe
get_hooks will be allowed to return a string containing the name of the
required plugin if that plugin isn't found.  If get_hooks returns a string
instead of a hash reference, an error message will be printed, and your
plugin will be properly and completely disabled. 



9) Designing well-behaved plugins
---------------------------------

- Plugins should never ever export any symbols by default.

- All files that a plugin uses during its operation, such as extra custom
  perl modules, text or database files, etc., should reside under its own
  directory. 

- If your plugin listens for users to say a key phrase in the form of
  '!command', then make sure you look for that phrase only at the
  beginning of an event's text (use a regexp like /^!command/).

- If your plugin scans all public/msg text for certain phrases (for
  example, "what is X" or "where can I find Y") or passes all the text on
  to some entity (such as an artificial intelligence program or module),
  make sure you do NOT scan or pass on lines that start with '#' or '!',
  unless you are SURE this is what you want to do.  If you have a plugin
  that looks for "!spell <a list of words to spellcheck>" and another that
  looks for "what is X" in general speech, you don't want the second
  plugin to trigger when someone says "!spell what is yor questt?" 
