He was deeply in love when she spoke, he thought he heard bells, as if she were a garbage truck backing up.


Perl-fu 2 Documentation


Overview

Perl-fu version 2 is an IRC bot written in pure Perl. Like its version 1 predecessor, it is based on infobot (available at www.infobot.org) and shares the purpose of storing/retrieving factual information. But that isn't even close to what Perl-fu does in entirety, and is even further away from Perl-fu's potential to grow. By utilizing Perl-fu's dynamic plug-in architecture, Perl-fu can do almost anything you want!

It is assumed that if you read part of this documentation, you have read all the documentation preceding it, since a lot of it builds on what was previously explained. If something doesn't make sense, be sure you read the previous sections.


The Basics

Getting Started

Once you download Perl-fu, unpack it into its own directory. Before you run Perl-fu you will want to customize your Startup.txt file, as well as set yourself up as a global operator in your user database, and go through the interactive setup, all of which are explained below. After you've completed all this, run the bot simply via perl perlfu.pl.

Configuration File Format

Perl-fu uses a certain format for serializing many different kinds of data. An example would be Users.db, its default database file for storing user information such as access levels. If you are familiar with BNF or Parse::RecDescent, you can actually read the grammar in Configuration.pm, found in Perl-fu's main directory. If you are not, this section will try to explain it.

Call it a configuration file, call it a variable dumper, call it what you will. No matter what you use it for, the data stored in a file created or meant to be read by Configuration.pm, represents a Perl data structure, divided into hashes, arrays, and scalar values. Hashes are represented by sections and subsections in the file. Scalar values are represented by a quoted string or a numeral, lists are created with ()'s or []'s. Everything is terminated with a semicolon, and comments are allowed in the #, //, and /* ... */ varieties. Since I know that was a lot at once, I'll try to further clarify with an example:

# A Configuration.pm file example
# The entire file is a hash
# Key/Value pairs can be defined with a colon
HashKey: "HashValue";

# Or an = sign
AnotherKey = "Another Value";

# Notice how hash keys which are all one word and
# match /^\w+$/ do not need quotes
# While multi-word ones do
"A number" = 3;

# sections and subsections are just embedded hash
# references with their own key/value pairs. Their
# names follow the same constraints as the hash keys
# as far as needing quotes goes
section "Sub Section 1" {
    "Key" = "Value";
    # Lists are defined like so...
    # You can not make a list of sections, but you
    # can embed lists in other lists
    "A list" = (1, 2, 3, 4);
    'Another List': [1, 2, 3];
    'Embedded Lists' = (
        ( 1, 2, 3),
        ( 1, 2, 3),
        ( 1, 2, 3));
};

Hopefully that made sense and cleared things up. Actually using Configuration.pm to read/write from/to these files is explained later.

Interactive Setup Utility

Perl-fu version 2 introduces an interactive setup utility to help customize your bot which runs automatically on first run (in the absence of a Setup.txt file). The setup utility can be invoked by using the --setup switch on the command line like so: perl perlfu.pl --setup. You have the option to set all the settings to their default values, or you can go through manually and set your own values for some. Settings can also be changed when the bot is online via the set command (see the command list for details on usage). And when the bot is offline, if you don't want to go through the whole setup utility again, but want to change one or two settings, you can modify them directly in the Setup.txt file the utility generates.

The setup file this utility generates utilizes the Configuration.pm file format explained earlier. Following is a list of each key's name and a description of what it's for. Note that Boolean values are NOT represented via the strings "true" and "false" but rather integers that range from 0 to 1 (0 meaning false, 1 meaning true):

Name

Data Type

Default

Description

AddrFactCommand

Boolean

1

Determines whether or not the bot must be addressed in order to respond to commands having to do with factoid management/status. If set to a true value, Perl-fu will not try to match the text in a channel against factoid-related commands, unless it was addressed or private messaged. If false, Perl-fu will test all text in the channel for factoid-related commands.

AddrPlugCommand

Boolean

1

Determines whether or not the bot must be addressed in order to respond to commands defined by plug-ins. If set to a true value, Perl-fu will not try to match the text in the channel for commands defined by plug-ins, unless it was addressed or private messaged. If false, Perl-fu will test all channel text for plug-in commands.

AddrQuery

Boolean

0

Determines whether or not the bot must be addressed in order to report factoids it knows. If set to a true value, Perl-fu will not attempt to look up factoids unless it was addressed or private messaged. If set to false, it will attempt a factoid look-up with all text in the channel. And it will report the matches.

AddrStore

Boolean

0

Determines whether or not the bot must be addressed in order to learn new factoids. If set to a true value, Perl-fu will not attempt to store new factoids, unless it was addressed or private messaged. If set to a false value, Perl-fu will always look for new factoids to store in the channel.

FactDB

String

"Facts.db"

Specifies which file to use for its factoid database.

HearAction

Boolean

1

Determines whether or not the bot will handle ctcp action events. If set to a true value, Perl-fu will parse text in ctcp action (/me) events as if it were spoken normally. If set to a false value, ctcp action events will be ignored.

HearAll

Boolean

0

Determines whether or not the bot will handle private messages sent by persons who do not share a channel with the bot. If set to a true value, Perl-fu will parse text sent to it via private messages without regard to where it came from. If set to a false value, it will only parse text in private messages which were sent by persons who share one or more channels with Perl-fu.

MaxSize

Integer

500

Specifies the maximum length,  measured in characters, a factoid may be.

PluginDirectory

String

"./Plugin"

Specifies which directory to use as the plug-in directory.

StartupFile

String

"Startup.txt"

Specifies which file to use as its Startup file.

StripColors

Boolean

1

Determines whether or not the parser will attempt to remove color codes. If true, color codes will attempt to be stripped from parsed text. If false, color codes will be left alone. However, since color codes usually rely on a control character (most commonly ^C), setting this to false does not insure that they remain intact, since StripControlChars exists.

StripControlChars

Boolean

1

Determines whether or not the parser will attempt to remove control characters. If true, all control characters, or more accurately, characters that fit into the character class [:cntrl:] will be removed from parsed text. If false, they will be left alone. Note however, that setting this to false does not insure that all contorl characters are untouched, since StripColors and StripFormatting exist.

StripFormatting

Boolean

1

Determines whether or not the parser will attempt to remove IRC formatting characters, such as bold and underline. If true formatting characters which represent bold/underline on many clients will be removed from parsed text. If false, they will be left alone.

UserDB

String

"Users.db"

Specifies which file to use for its user database.

Note that StripColors, StripControlChars and StripFormatting can cross boundries, since control characters are often used for both color and formatting.

User Access Levels and Creating a Global Operator

Perl-fu uses its user database to store information about certain users both globally and on certain networks. One such piece of information which is stored is the user's access level. A user's access level is an integer greater than or equal to 0. You may utilize as many levels as you like, but 0-2 is what is recommended, because 2 is the access level it takes to work with all of Perl-fu's administration commands. If you want you can open up Admin.pm in the plug-in directory and modify this constant, but there really is no need. At any rate, nobody can change another user's access level to a value greater than his/her own, so it makes sense to set you at the highest access level. You do this by editing the user database, which is stored in Users.db unless you specified your own filename during the interactive setup.

To add a global operator, that is, a global access level setting for a particular nick!hostmask, open the user database. If there is not already a section called "__GLOBAL__", create one. Within this section, create a subsection called "Access". Within this subsection, every key is a nick!hostmask (with optional wildcards *, meaning 0 or more unknown characters, and ?, meaning one unknown character), and every value is that mask's access level. For example, if you wanted to globally give somenick!foo@bar.com access level 2 privileges, your user database might resemble the following:

section __GLOBAL__ {
    section Access {
        "somenick!foo@bar.com" = 2;
    };
};

For more information on the user database and how it is set up, see the section entitled "The User Manager".

The Startup File

Perl-fu keeps all data representing its startup procedure in a Configuration.pm-compatible startup file, set to Startup.txt by default. It is composed of one Startup section, which contains information on which servers to connnect to, and any number of other sections, which represent information for each server. The Startup section contains one key, Servers, which is a list of servers to connect to. For each server on this list, you should also have a section with the same name as that server containing information such as the nickname, ircname, username, etc. for that server. Here's an example Startup file:

// Store the list of servers to connect to here
section Startup {
    Servers: ('irc.secup.uu.net', 'irc.efnet.nl');
};

section 'irc.secsup.uu.net' {
// Store information about each server in other sections like this one
    Nick: 'botnick1';
    Port: 6667
    Ircname: 'perl-fu'
    Username: 'perl-fu'
    section Channels {
    /* The "Channels" section contains key/value pairs.
        The keys are the channel names to join on connect,
        the values are the optional channel keys, specify an empty string for no key */
        '#poe' = '';
        "#a_private_channel" = "a private key";
    };
};

section 'irc.efnet.nl' {
    ...
};

Command-line Switches

Perl-fu supports three command-line switches: --help, --setup, and --version. All the --help switch gives you is information on all the switches. The --setup switch launches the interactive setup utility, and the --version switch displays which version of Perl-fu you are running.


Factoids

Overview

Maintaining, learning, and reporting factoids is Perl-fu's primary reason for being. A factoid is, at its most basic level, a word or phrase, which is related to another word or phrase via a verb form, usually of the verb to be. "I am a programmer.", "This site is where you can download Perl-fu", and "Robots will be victorious" are all examples of factoids. If the AddrStore option is false, which it is by default, Perl-fu will learn new factoids just from listening to conversations in the channel. If AddrStore is true, you must address the bot in order for it to learn new factoids. Note that the commands used in the rest of the factoid documentation are not all of the available commands. See the command list to view them all.

Storing Factoids

Storing a new factoid could not be easier. Just say it! If the bot is allowed to store new factoids without being addressed, you can just state it. You can always, of course, tell the bot specifically. Usually it is recommended that you address the bot when storing a new factoid, because Perl-fu will not overwrite a factoid it already knows by the same name unless specifically told to do so and addressing the bot will make it respond telling you if it was stored or not. If a factoid by the same name already exists, and you want to change it, you may overwrite it with the no command. For example, if I wanted to tell the bot that "the empire state building is tall" but it already knows that "the empire state building is in New York", it will tell me so. If I wanted to overwrite it, I could say "botname, no, the empire state building is tall".

Factoid Directives, Variables and References

Factoids may contain a number of things I call directives. Whether or not I'm using the word directive correctly is not the point. What factoid directives are, are basically commands and characters imbedded in the factoid in order to change how it is reported upon a query.

The first directive is the alternation directive. When a factoid is parsed, if it contains pipes "|", they are treated as random alternations. The factoid uses the | character as a delimiter and splits the factoid, then continues parsing a random element of the resulting list. If you want to have a pipe in your factoid literally, you will have to escape it with a backslash \. For example, say you wanted to store a "coinflip" factoid, which would alternate between heads and tails. You might tell the bot to store it like so: "botname, coinflip is heads|tails".

The next two directives are parsed at the beginning of either the factoid or an alternation. They are called <reply> and <me>, are both case insensitive, and may both be escaped with a backslash if need be. <reply> tells the bot to say exactly what follows the <reply> directive, instead of the usual "factoid name is factoid contents" response. In our coinflip example earlier, about half the time the bot would have responded, "coinflip is heads" and the other half, "coinflip is tails". But we can make this a little more believable with the <reply> directive. So now lets tell the bot "botname, no, coinflip is <reply>Heads!|<reply>Tails!". Now, half the time it will yell "Heads!" and the other half, it will reply "Tails!". The <me> directive tells the bot to respond via a ctcp action event rather than the usual privmsg event. In most clients, a ctcp action event is done by typing "/me" before what you want to say. So we could change our coinflip example around a little bit and change it to, "botname, no, coinflip is <me>Flipped heads!|<me>Flipped tails!".

Along with directives, you may also insert a few select variables into your factoid contents, they are all case insensitive and may all be escaped with a backslash if need be. Currently, the following variables are parsed:

  • $nick - nickname of the person who queried the factoid

  • $host - host of the person who queried the factoid

  • $username - username of the person who queried the factoid

  • $date - the date as returned by localtime() at the time of the factoid query

  • $time - the time in 24-hour HH:MM:SS format at the time of the factoid query

So, using these, we can spice up our coinflip factoid even more! Lets say "botname, no, coinflip is <reply>$nick, I flipped heads!|<reply>$nick, I flipped tails!".

But directives and variables aren't the only bells and whistles you can put into a factoid, last but certainly not least, are factoid references. Factoid references are handy when you want to have one factoid be accessible by many names, or if you want the contents of one factoid in another. If the factoid being referenced is one word, you may reference it with a single asterisk in front of the factoid name. If it is multiple words, you must close the name in curly brackets {} and use an asterisk. For example, say I had a factoid: "Hello is <reply>Hi, $nick!". But I wanted another factoid "hi" to do be the same thing, and I wanted them both to be updated when I just changed one, well, that's what a reference is for. Since "hello" is one word, I can construct my "hi" factoid like so "hi is *hello". If, however, I were referencing a factoid named "hello there", for instance, I would need to use brackets like so: "hi is *{hello there}". Or maybe I wanted a factoid "comment" which would either insult someone or complement someone, and I had factoids for both, I might say "comment is *{insult}|*{complement}", Note how I used brackets there, even though insult is one word. I did that because not using brackets tells the parser to use all the following characters which are not white space as the factoid name. Which includes the alternation directive, so I had to disambiguate via brackets. One last thing to say about factoid reference syntax is that, knowing that people on IRC like to emphasize things by surrounding a word with asterisks, the parser is smart enough to ignore words with an asterisk on each side. Therefore, *foo* will not try to reference the "foo*" or "foo" factoid, it will be left alone.

The Plug-in Factoid Directive

The plug-in factoid directive attempts to take the first step toward integrating plug-in output into factoids. It's syntax looks like this: <Plugin=command> where "command" is a string you would have otherwise told the bot specifically had you been using the plug-in directly. The plug-in directive attempts to parse your command with its compatible installed plug-ins. If it matches a command, the output of that command will be inserted into the output of the factoid query in place of the Plug-in directive. If there are characters that need to be escaped in your command, such as ">" and "\", you may do so with a backslash "\". This makes it possible to send the ">" char to a command if needed without messing up directive parsing. Note that I did say compatible installed plug-ins. Plug-in authors have to make their plug-ins compatible with the plug-in directive in order for this directive to work with their plug-ins. So far, as far as bundled plug-ins go as of version 2.04, the only plug-in that is compatible with this directive is the Calculate plug-in, for experimental purposes.

Retrieving Factoids

The AddrQuery setting decides if the bot must be addressed in order for it to report factoids. By default the option is false, meaning that the bot will report a factoid whenever it has the opportunity. If you want to query for a factoid, just ask the bot about it; you can simply say the name of the factoid, or you can say "what is ...", "where is ...", etc. If it knows a factoid that matches your query, it will say so. Querying for a factoid with this method will result in the being parsed for directives, which are explained above. If you want to see what a factoid is literally, use the literal command: "botname, literal factoid name".

Modifying Factoids

There are a few ways to modify a factoid. You can of course overwrite it with a completely new one via the no command. But you may also use the is also command, and if you are Perl savvy, the s/// operator. The is also command comes in handy if you want to state a factoid as being more than one thing. If the bot already knows that "Dave is big." and you want the bot to know that Dave is also tall, tell the bot "botname, dave is also tall." Now the new factoid will be "Dave is big and tall." If you have a factoid set up which uses a lot of different responses via the alternation directive "|", then you can use a special form of the is also command, is also |. This will add another alternation onto the end of the factoid. If you know what you're doing with Perl regexes, you may also modify the factoid contents via the familiar s/// operator; however, my implementation of the s/// operator only works on operating systems which support the alarm() function, sorry Win32'ers. The s/// operator takes a real Perl regular expression as its first argument, but has a few restrictions, as a result of safety and parsing issues. Many unsafe constructs have been removed from the left side of the regex, such as (??{....}). The right side of the operator does not interpolate variables of any kind, $1, $2, $&, etc are taken as literal strings. And a few switches have been removed as well, such as the /e switch. Here's an example using the Perl-fu adaptation of the s/// operator to modify a factoid called "test". Note that a terminating semicolon is optional, not required: botname, test =~ s/findthis/replacewiththis/;

Removing Factoids

To remove a factoid, you may either modify it with the s/// operator as to yield a zero-length result, or you may simply use the forget command like so, "botname, forget factoid name".

The Null-Reply Trick

Sometimes a word or set of words is said very very often, and you want Perl-fu to deliberately say nothing. But deleting the factoid isn't good enough because it'll just get set again! You can set a factoid to "<reply>" with nothing after it. Then it will literally say nothing when that factoid is looked up!

Addressing in Private Messages

In a private message (/msg). It is assumed that the bot is being directly addressed. You may still address it manually if you like, but it will just be parsed out and ignored. Therefore, in a private message the only setting of concern is the "HearAll" setting, which if set to a false value, will instruct the bot to ignore persons who query it from outside any channel the bot is in. Persons who share one or more channels with the bot are known as "denizens".

Factoid Access Levels

Factoid access levels prevent important factoids from being modified/deleted by users who lack the proper permissions. To set or change a factoid's access level, use this syntax: set fact[oid] [access] level 'factoidname' newlevel. If the factoid name is more than one word, you should quote it in either single or double quotes, if it is a single word, you do not need to quote it. You can not set a factoid's access level to be larger than your own user access level.

Getting Database Status

Use the status command to get the current status of the factoid database, including total referenced factoids, the number of additions, modifications, and deletions made, and uptime.


Plug-ins

Overview

A plug-in is an external library of commands and/or event handlers designed for the purpose of extending Perl-fu's capabilities. If you know how to program in Perl and POE, writing plug-ins is not a difficult undertaking. In fact in version 2, it's easier than ever before. But even if you aren't very familiar with Perl or POE, installing them on your own is still extremely easy.

Installing

A plug-in consists of at least one .pm file. Be sure to read the README file if it exists to make sure you are up-to-date on prerequisite module versions and possible installation details. If there is no README file, you may safely assume that all you need to do is copy the .pm file into the directory specified by your PluginDirectory setting, which should be "./Plugin" relative to the directory where your perlfu.pl file is contained unless you changed it to something else, and that you need only standard modules bundled with Perl 5.6.1. After it is installed and you are sure your prerequisites are fulfilled and you have followed any special instructions in the README file. The plug-in is considered installed. If the bot is running, you may tell it via IRC to "reload plugins" and it will refresh its plug-in library.


Creating Your Own Plug-in

Overview

Perl-fu version 2 sports a brand new plug-in interface. Sorry, version 1 plug-ins will have to be ported. The main goal of the version 2 architecture was to give more power/information to the plug-in programmer, while not making it overly complicated. And after a very long period of tinkering, I believe I have found a good balance. But keep this in mind: Version 2 plug-ins require a working knowledge of the Perl programming language, POE and POE::Component::IRC.

Perl-fu version 2 plug-ins are POE::Sessions. By using POE directly at the plug-in level, the programmer has a lot more freedom, since he has all of POE to play with. The Perl-fu plug-in programmer can now implement non-blocking operations and even multitask with ease.

Available Tools

Perl-fu version 2 plug-ins may or may not utilize the external library "Tools.pm". All Tools.pm offers is a collection of useful functions which perform common or frequent tasks. It's an Exporter, so the plug-in programmer may import only the symbols he desires. More functions may be added in future versions, but currently, as of version 2.02, Tools.pm contains the following subroutines. Note that square brackets [] do not imply array references but rather the quality of a parameter being optional.

  • NickMask($nickmask) - Performs the simple yet extremely common task of separating a nickname from a hostmask as provided by a POE::Component::IRC event. It returns a list of two elements, the first is the nickname, the second is the hostmask.

  • Output($text[, $id]) - Prints $text to STDOUT in the same fashion Perl-fu outputs most of its text. It looks essentially like this, where $pkg is the package of your plug-in and $text is the text you feed it: "$pkg> $text\n". If you give it a second argument, it will insert that number just before the greater-than sign. For an example of the behavior with a second argument, look at the terminal window while Perl-fu connects. It will display something to the degree of: "Client 1> Connecting to irc.something.net". The number 1 was passed as the second argument. It returns nothing useful.

  • OutputStr($text[, $id]) - Forms a string identical to what Output() would have printed, but returns it rather than prints it.

  • ReplyTo($environment_object) - Takes an Environment object reference and returns a string corresponding to the appropriate party to reply to in commands and events where it makes sense to reply such as irc_public, irc_msg, etc. This takes the guesswork of whether or not a command was triggered via an irc_public or irc_msg out of the output process. Look in the example plug-in later in this section to see this useful little function in action.

Using Configuration.pm

File Syntax

Configuration.pm files are, as described above, files intended for data storage. They utilize a recursive descent grammar I wrote myself. Configuration.pm files don't necessarily have to be configuration files, they basically are just files which serialize a Perl hash, with limitations, into a readable format. Configuration.pm files consist of key/value pairs, sections, and comments. Using combinations of these, one can form fairly complex data-types consisting of scalar, array, and hash values. Sections (hashes) can be filled with either key/value pairs or subsections. A subsection equates to a hash reference, where the name of the section is the key in the parent section where it will be stored. A key can be any scalar value, and a key's value may be either a scalar or a list value. You have a list of scalars, or a list of lists to arbitrary depth, but you may not create a list of sections. Both key and section names need not be quoted if they match /^\w+$/.

Sections are declared using the "section" keyword, followed by a section name, quoted if need be. The body of the section is started with an open curly bracket and ended with a closed curly bracket. And all sections, as well as key/value declarations must end with a semicolon. Here is an example section, it is called "Color". The ellipsis is not valid syntax, it is merely there as an indication that more would usually be entered into a section.

section Color {
    ...
};

A key/value pair consists of a key, operator, value and terminating semicolon. Like a section name, the key can be quoted any time, but it only has to be quoted when it does not match /^\w+$/. The operator may either be an equals sign "=" or colon ":". The value may be either an array or scalar value. A scalar value has the option of being either a quoted string, an unquoted numeric value, or undefined. Either single or double quotes may be used to quote a string value, and you may escape characters within the string via the backslash character. Numeric values need no quoting. To specify a scalar value as undefined, type "undef" without quotes. List values may be used by enclosing a comma-delimited list of either scalar values or embedded list values between either square or curly brackets. It doesn't matter if square or curly brackets are used. Terminate the statement with a semicolon. Here are a few example key/value pairs, between them, they utilize most kinds of key type, operator, and value type. The last example uses square brackets for the inner lists for aesthetic purposes, but more parentheses could have been used as well, as could square brackets have been used on the top-most list. I just feel I need to make sure everyone understands there is no difference, as long as they match, that is, a list started with a parenthesis may not end in a closing square bracket and vise versa.

String_1 = "A quoted string scalar value";
'String 2': 'Another quoted string';
"Numeric 1" = 10.3;
'List of scalars' = ("foo", 'bar', undef, 10);
'List of lists': ( [1, 2], [1, 2], [1, 2] );

Configuration.pm files may also be commented with three different comment types. Similar to Perl itself, one may comment out everything on a line starting at a number/pound sign "#" to the end of the the line. Similar to C/C++ commenting, one may also utilize the familiar double forward slash "//" and forward-asterisk "/* .. */"method to comment out single lines and multiple lines respectively. Here are a few examples:

# Here's a single line comment via the pound sign
// And another via a C-ish comment
section 'kerblabble' { # I could comment on kerblabble
    /* stuff would
        go
        in
        here
    */
};

And last but not least, is one final example in syntax. This shows a Configuration.pm-compatible file, and the resulting hash reference that it serializes:

# Configuration.pm Serialization #
Key1 = "Value1";
section Section1 {
    Key1 = 10;
    Key2 = (1, 2, 3, 4);
    Key3 = ((1, 2), (1, 2));
};

# Resulting Perl Hash Reference #
$VAR1 = {
    'Key1' => 'Value1',
    'Section1' => {
        'Key1' => 10,
        'Key2' => [1, 2, 3, 4],
        'Key3' => [
            [1, 2],
            [1, 2]
        ]
    }
};

Using the Class

Configuration.pm is capable of reading a file produced manually into a Perl hash reference, or saving your own Perl hash reference to a file of your choice. To use the Configuration class, you must first "use" it.

use Configuration;

You may then obtain a Configuration object by invoking the package method new().

my $conf_obj = Configuration->new($filename);

$filename is a string which represents the file to which you will be writing or from which you will be reading. If the file exists at the time of instantiation, the Configuration class will attempt to parse it. If it does not yet exist, nothing will be done. In either circumstance, a blessed object is returned, on which you can invoke the rest of the class methods.

# Read in the serialized variable as the
# file dictated at the time of instantiation
my $hash_reference = $conf_obj->Data(); 

# Re-parse the file and return the serialized
# variable therein 
my $hash_reference = $conf_obj->Reload();

# Store contents of a current hash reference
# into the file. Careful though, the class
# will write over the existing file. All
# serialized data and comments will be lost
$conf_obj->Save($hash_reference);

The Environment Object

Overview

In Perl-fu version one, there were approximately four arguments passed to command handlers. In the Perl-fu version 2.0 beta, which I never released, this number grew to about 6 because of the new multi-server functionality and other issues, and even with those, the Tools.pm library had to be filled to the brim with functions who handled things like getting the current network and checking a nickname's denizen status. This was obviously not the best way to go about it. In an attempt to solve the problem of not having enough information at your fingertips when writing command/event handlers, I created the Environment class. The environment class is an information storage class designed to access many pieces of information at will, while avoiding long parameter lists. And the best feature is that more can be added to the environment at a later time without breaking existing code.

An environment object is passed to all command handlers and all event handlers in a plug-in. In event handlers, which are explained later, the environment object may be found in @_ at an offset designated by the ENV constant, which is exported from PluginManager.pm. In command handlers, the environment object may be found at offset ARG1, which is exported by POE::Session.

Composition

The Environment holds all sorts of data, accurate at the moment when the command or event was first triggered (not when it is handled). To retrieve data from an Environment object, use the Get() object method. Get() takes one or more scalar values (strings) for parameters, which represent keys to the internal environment hash. When given one parameter, if it is a valid key, one scalar value which corresponds to the value in the internal hash will be returned. If Get() is invoked with multiple arguments, then an array is returned, whose elements are the values of the given hash keys, in the same order. In this way, Get() behaves much like a hash slice. The reason for using the Get() method rather than just working with the hash directly is error checking. Get() will throw an error if the user attempted to access an unknown environment hash key. If you wish to work with the hash directly though, just go ahead and do so, the Environment object is a blessed reference to a hash whose keys are exactly the same as the strings you would send into Get(). Following is a table of environment hash keys, and what their values are.

Key Name

Data Type

Description

Addressing

String

Holds addressing information returned by the parser representing how the bot was addressed, if the bot was addressed. The string may be either of the following three "direct", "indirect", or "none". Direct addressing means that the bot was addressed, and that a comma, colon, hyphen or other valid separating character or character sequence was used. Indirect addressing indicates that the bot was addressed, but no separation characters were used, that is, the bot's nickname was simply used before all the important text or after all of it. None indicates that no addressing at all was used. The following examples assume that the bot's nickname is currently set to "Perl-fu" on the current network.

Direct Example: Perl-fu, Hello.
Indirect Example: Perl-fu Hello.
None Example: Hello.

Note that the value of this key is unreliable and meaningless in events where it does not make sense to have addressing, such as join/part/quit/ping/etc events.

BotNick

String

Holds a string containing the bot's nickname on the network from which the event originated.

Denizens

Blessed Ref

Holds a blessed (object) reference to Perl-fu's denizen manager. Details on using the denizen manager are provided in a later section.

Event

String

Holds a string representing the IRC event which triggered the event or command handler. Examples would be "irc_public", "irc_msg", "irc_join", etc.

Network

String

Holds a string containing the current network name from which the event originated. If Perl-fu was unable to obtain a network name during the connection process, it will contain the server name that was connected to. Note: Do not rely on this string as the correct name of the server that you are connected to if it is not a network name! It will merely hold the string that was used for the connect in the startup file.

NickMask

String

Holds the nick!hostmask string returned by many POE::Component::IRC events, usually in ARG0. This key will be unreliable for event handlers in which it does not make sense to have a nickmask, such as an irc_ping event for example.

PCI

String

Holds the session alias of the POE::Component::IRC session which originally produced the event.

Recip

Array Ref

Holds an array reference of recipients of the event, where it makes sense to have a recipient list, otherwise it is unreliable. It only makes sense to use this key in a small set of IRC events, including but not limited to irc_public and irc_msg.

Text

String

Holds a string containing the text sent by an individual in an event where it makes sense to have such a key. Just like "Recip", it only make sense to use "Text" in a small set of IRC events, including but not limited to irc_public and irc_msg.

Users

Blessed Ref

Holds a blessed (object) reference to Perl-fu's user manager. Details on using the user manager are provided in a later section.

Note: Many of these environment keys, such as Addressing, NickMask, Recip, and Text will be unreliable/meaningless in certain events. It only makes sense to use them in events including but not limited to irc_public and irc_msg.

The User Manager

On each network, different people have to be kept track of. Some have non-standard access rights, some are to be ignored. The user manager keeps track of all kinds of information about all kinds of users on each network. It uses Configuration.pm to maintain a database of this information. The database is divided into sections for each network and one section called "__GLOBAL__", which overrides all others. Within each network section are subsections, which correspond to different attributes. Attributes that the Perl-fu core program makes use of include "Access" and "Ignore", but you may add more if need be. Within each attribute is a set of key/value pairs. Each key is a nick!hostmask, containing optional wildcards. Wildcards are asterisks and question marks, meaning zero or more unknown characters and one unknown character respectively. And each value is whatever that attribute for that nick!hostmask on that network is set at. For the Access attribute, the values are access levels (defaulting to a range from 0 to 2). Inside the Ignore attribute, the values are simply true/false values (integers 1 and 0 respectively). For other attributes the values may be something completely different.

Perl-fu's user manager can be found under the "Users" key of the Environment object passed to all your event and command handlers. Following is a table of methods the UserManager class supports. Note that "nickmask" refers to a string containing the nickname and the hostmask as if it were provided by a POE event handler: nick!user@host.com, and "wildcards" refer to the asterisk and question mark, meaning zero or more unknown characters and one unknown character respectively.

Method

Description

new(
 $filename
)

new() is not an object method, it is a package method. Call it like so:

UserManager->new($file);

new() takes only one argument, the filename of the file with which the manager will be associated. When you call Save() later, it will be saved to this file. Chances are you will not need this, because having multiple user managers has no obvious upside. However, if you find a creative use and have good reason to do this, make sure that the filename is different from that of the Perl-fu user database filename.

Save()

Save() serializes the data in the user manager into the file specified by new(). If you are using the UserManager object provided in the Environment, then it will save to the file specified in the UserDB setting. But likewise, if you were using the UserManager in the Environment, calling this function directly would be pointless, since it is already called automatically before the program ends.

SetAttribute(
 $network,
 $nickmask,
 $attrib,
 $value
)

SetAttribute() takes 4 parameters. The network on which to store the information, the nickmask of the user for which we are setting the attribute, the attribute name, "Access" or "Ignore" for example, and the value to set it to. The nickmask may contain wildcards to include multiple users or multiple possible forms of the same user.

GetAttribute(
 $network,
 $nickmask,
 $attrib
)

GetAttribute() takes 3 parameters, the same as SetAttribute() sans a new value. The difference in usage is that in GetAttribute(), $nickmask should not contain wildcards, as you are checking a specific individual for their attribute status.

RemoveAttribute(
 $network,
 $nickmask,
 $attrib
)

RemoveAttribute() takes 3 parameters. The network it applies to, the nickmask it applies to on the network, and the attribute to remove it from. Nickmask must exactly match (character for character) the nickmask you set in the first place via SetAttribute(), wildcards and all. After this call, the key/value pair representing the nickmask in the attribute's section will be deleted.

Among all the methods available to you, unless you create your own user manager for some reason, you will probably only be using the GetAttribute() method.

The Denizen Manager

The denizen manager is for maintaining a data structure representing with which people Perl-fu shares channels. There are many methods in the denizen manager, but seeing no point in a plug-in programmer using most of them I will simply explain the one which makes most sense to be using, IsDenizen(). This method takes two arguments, the network on which you're checking denizen status of an individual, and the nickmask of that individual (no wildcards). It returns a true/false value, true if the nickmask shares at least one channel with Perl-fu on the network you specified, and false if he doesn't. Perl-fu's denizen manager may be found in the Environment under they key "Denizens".

Commands and Events

Perl-fu plug-ins handle information in two ways, commands and events, which are analogous to active and passive event handlers in Perl-fu version 1 plug-ins respectively. How to use them both is detailed in later sections, this section just attempts to the describe the differences between the two. If you know anything about POE::Component::IRC, you know that it fires session events which relate to activity on IRC. These events are listed in POE::Component::IRC's documenation on CPAN if you want a full list of them. These are exactly what Perl-fu events are, they are unaltered PoCo::IRC events which are relayed to your plug-in on request. Commands however, are usually used to perform specific tasks or instructions. A plug-in command undergoes a certain amount of parsing, such as white space compression and end punctuation removal before being tested for any specific command.

Inline Commands

Inline commands are just like normal commands, only they are meant for the purpose of being compatible with the new Plug-in directive (explained above in the factoids section). The difference between inline commands and normal commands is only the return value, in normal commands, it is discarded because it is assumed that the plug-in author has already sent his output to whichever PoCo::IRC is handling the current connection. But inline commands are meant to produce output suitable for insertion into a factoid. This is done by returning a string.

Parts of a Plug-in

For a Perl-fu plug-in to work as expected, a certain number of required parts must be present. Each is described in detail in a later section. If a plug-in does not work as expected, make sure that it has all of the following requirements as a first step to debugging.

The Package and Final Return Value

Every plug-in should have its own package, in order to prevent symbol name conflicts. While it is not required by any means, putting your plug-in in the "Plugin" namespace is the convention. So your plug-in might use the namespace "Plugin::MyPlugin" or similar. It is also important that the last Perl statement in your plug-in is a string which matches the plug-in's package name exactly. Perl-fu loads plug-ins by do()'ing them, and it uses the return value of do() to know which package to call new() and other functions on.

The Plug-in Manager

The plug-in manager is the class which Perl-fu uses to maintain its plug-in library. While plug-ins have no reason to use it directly, they should "use" the plug-in manager anyway, because it exports the constant ENV, which is the offset in @_ where the environment object is held in event handlers. In later versions, other constants may also be exported, and every plug-in using the plug-in manager helps ensure that the plug-ins don't break. To use the plugin manager, just put the following code near the top of your plug-in code:

use PluginManager;

The new() Function

All plug-ins must have a new() function. The new() function creates and returns a POE::Session object reference. Perl-fu version 2 plug-ins basically are POE::Session objects. The other functions in your plug-in package are either state handlers for the session, or the commands() or events() functions, which are described next. Perl-fu automatically handles your session's reference count so that you don't have to worry about keeping it alive without receiving events. Think of your plug-in's session as a persistent session that doesn't die until either the plug-ins are reloaded or the bot shuts down. A sample new() function might look something like this:

sub new {
    return POE::Session->create(
        inline_states => {
            _start => sub {},
            _stop => sub {},
            ...
        }
    );
}

The commands() Function and Command Handlers

Perl-fu calls the commands() function for every plug-in it finds in the plug-in directory when it loads them. the commands() function's purpose is to return a hash reference. The keys of this hash reference are qr//'ed regular expressions, which are used to test for a command. If the text someone types, after a certain amount of parsing, matches one of your plug-in's regular expressions defined in this hash reference, then they are said to have typed a command. The values associated with the keys in the hash reference returned by commands() are events to send to your plug-in's POE::Session. If you choose to capture any text in your command regular expressions, that is assign values to $1, $2, etc, they are sent to your command handler as an array reference in ARG0. The first element is the value of $1, the second is the value of $2, and so on. Note that Perl-fu's parser condenses all area of white space to a single space. So using the \s character is not needed, just use spaces. It makes it more readable.

Functions that handle the events contained in your commands() hash reference are known as command handlers. Since they handle a POE::Session event, the values in @_, match those of any other POE handler. All the constants exported by POE::Session apply. In command handlers, ARG0 is a reference to an array containing any captured text in your regular expression; index 0 contains $1, index 1 contains $2 and so on. ARG1 is an environment object accurate at the time when the event that caused the command occurred. Unless Perl-fu is busy to the point where many commands are in the event queue, chances are that the data in the environment object sent will still be accurate at the time of your handler's execution as well.

The events() Function and Event Handlers

The events() function allows your plug-in to handle pure IRC events raw without any Perl-fu parsing getting in the way. The events() function is supposed to return a list of strings. Each string is a state that your plug-in's session wants to handle. You may use any named or numbered POE::Component::IRC event in this list. Examples would be "irc_join", "irc_part", "irc_nick", "irc_public", "irc_005", etc. When an event you state in this list occurs, that event, in its unaltered form, is relayed to your plug-in's session. You may then handle it however you wish. The only thing that is altered is of course the SENDER, since it is relayed by Perl-fu's main session, it won't come directly from the POE::Component::IRC from which it occurred. And an environment object is also added to @_ at the offset defined by the constant ENV, which is exported by PluginManager.

The inline_commands() Function and Inline Commands Handlers

The inline_commands() function is used to make your plug-in compatible with the plug-in factoid directive. It returns a hash reference, just like commands(), the keys are qr//'ed regexes which will match commands typed by the user, and the values are states to trigger in the plug-in's POE::Session.

Handling the session states is where inline commands and regular commands differ. Inline commands, unlike regular commands, are intended to return a string value, whereas regular command handler return values are ignored. The string your inline command handler returns will be inserted into a factoid when it is queried. This sort of behavior comes in useful for plug-ins like Calculate.pm, which return short yet meaningful values. It also might come in useful if you were to write an RSS plug-in with the purpose of getting data from the internet into a factoid.

Special Event States

Your plug-in's session, being a POE::Session, will be sent the usual list of pre-defined event names like "_child", "_default", "_start", and "_stop". Currently, Perl-fu sends only one more predefined event name, "_shutdown", although more may be added in future versions. The purpose of the "_shutdown" event is to allow for runtime plug-in reloading. When you create your plug-in, Perl-fu's plug-in manager keeps a reference to it, and maintains all the commands and events your plug-in wants to handle. It also increments the reference count to your plug-in for you, so you don't have to worry about persistence. When a user wants to reload the plug-ins, the manager decrements the reference count and sends the "_shutdown" event. The reason it sends this event is because decrementing the reference count is not always all it takes to kill a POE::Session. Child sessions, alarms and delays all keep a POE::Session alive. It is your duty as a plug-in author to take care of all these things in the "_shutdown" handler, that is kill all child sessions, remove all alarms and delays, etc, so that your plug-in's session will die when it needs to.

Don't Forget

The thing that I forget most of the time when writing plug-ins, either has to do with taking care of dependencies or the class name string. Make sure that every module your plug-in uses is installed and up-to-date. And make sure that your class name string matches the package in which your plug-in exists. While it is not required that it be in the "Plugin::" namespace, it is convention.

Documenting Your Plug-in

Plug-in documentation for Perl-fu version 2 is not yet standardized. A fudoc plug-in of sorts is on the way. For now, package your plug-in with a README file or document with POD.

An Example Plug-in

Following is an example plug-in which will attempt to sum up the basics of Perl-fu version 2 plug-in programming. This file would be located in the directory designated by the PluginDirectory setting "./Plugin" by default. You can try to copy/paste this into a plug-in and run it, I wrote it all at once in my editor and did not check it for syntax or logic errors. Its true purpose is just so that you can get a taste of what a plug-in actually looks like all together, and so that you can see how all the pieces fit in. If you want to check out plug-ins that are tested and have no errors and work fine, look at the bundled ones that come with Perl-fu, such as the Seen plug-in and the Karma plug-in.

package Plugin::MyPlugin;
use strict;
use warnings;
use POE;
use PluginManager;
use Tools qw(Output NickMask ReplyTo);

sub new {
    return POE::Session->create(
        inline_states => {
            _start => sub {
                Output("Starting...");
            },
            _stop => sub {
                Output("Stopping...");
            },
            _shutdown => sub {
                $_[KERNEL]->alarm_remove_all();
            },
            irc_join => sub {
                # a very annoying sub that greets joiners
                my($kernel, $nickmask, $channel, $env) =
                 @_[KERNEL, ARG0, ARG1, ENV];
                my($nick, $mask) = NickMask($nickmask);
                my $pci = $env->Get('PCI');
                $kernel->post($pci, 'privmsg', $channel,
                 "$nick: Welcome to $channel!");
            },
            cmd_saythis => sub {
                 # another annoying sub that just says what
                 # it's told to say
                my($kernel, $args, $env) = @_[KERNEL, ARG0, ARG1];
                my $text = $args->[0];
                my($pci, $nickmask) = $env->Get('PCI', 'NickMask');
                my($nick, $mask) = NickMask($nickmask);
                $kernel->post($pci, 'privmsg', ReplyTo($env),
                 "$nick, you told me to say $text");
            },
            inline_saythis => sub {
                # will insert text into a factoid
                my($kernel, $args, $env) = @_[KERNEL, ARG0, ARG1];
                my $text = $args->[0];
                return $text;
            }
        }
    );
}

sub commands {
    return {
        qr/say this (.+)/i => cmd_saythis
    };
}

sub inline_commands {
    return {
        qr/say this (.+)/i => inline_saythis
    }
}

sub events {
    return qw(irc_join);
}

"Plugin::MyPlugin";

See Also

Credits

I wrote Perl-fu! I wrote it all! From scratch dang-namit! If you have beef with Perl-fu you have beef with me! Send all your praise, questions and death threats to jinzougen@jinzougen.net! ... by the way, thank you Rocco "dngor" Caputo for creating POE and thank you Dennis Taylor for creating POE::Component::IRC, upon which Perl-fu is heavily based.

No animals were injured during the creation of Perl-fu. And only a few were sacrificed thereafter.