autumn
autumn copied to clipboard
Easy, fresh, feature-rich IRC bots in Ruby
Autumn: A Ruby IRC Bot Framework
Version 3.0 (Jul 4, 2008)
Author | Tim Morgan ([email protected]) |
Copyright | Copyright (c)2007-2008 Tim Morgan |
License | Distributed under the same terms as Ruby. Portions of this code are copyright (c)2004 David Heinemeier Hansson; please see libs/inheritable_attributes.rb for more information. |
Autumn is a full-featured framework on top of which IRC bots (called "leaves") can be quickly and easily built. It features a very Ruby-like approach to bot-writing, a complete framework for loading and daemonizing your bots, multiple environment contexts, a database-backed model, and painless logging support.
Requirements
Autumn requires Ruby 1.9 and Bundler. It is incompatible with Ruby 1.8. To
install: gem install bundler && bundle install
. The Gemfile in the root
directory lists Autumn's gem requirements; each individual bot can have its own
Gemfile with its own requirements.
Autumn includes an optional DataMapper-backed
persistent store. If you want to use this, you will need to ensure the correct
adapter gem is loaded in the Gemfile (by default it's dm-sqlite-adapter
). Edit
the Gemfile and replace dm-sqlite-adapater
with the adapter for your database,
if necessary.
The included example bot Scorekeeper uses DataMapper. The other example bot, Insulter, is much simpler and can run under any Autumn configuration.
Directory Structure
An Autumn installation is like a Ruby on Rails installation: There is a certain directory structure where your files go. A lot of files and folders will seem confusing to people who have never used Autumn before, but bear with me. In a bit I will explain in detail what all of this stuff is. For now, here is an overview you can consult for future reference:
-
config/ - Configuration files and season definitions
- global.yml - Universal settings that apply to every season
-
seasons/ - Contains directories for each season (see "Seasons")
-
testing/ - Example season
- database.yml - Example database configuration file
- leaves.yml - Example bot configuration file
- season.yml - Season configuration
- stems.yml - Example IRC configuration file
-
testing/ - Example season
-
doc/ - HTML documentation generated by RDoc
- api/ - Autumn API documentation
- leaves/ - Autumn leaves documentation
- Gemfile - Global gem requirements
-
leaves/ - Autumn leaves. Each subdirectory contains all the code and
data for a leaf.
-
insulter/ - Very simple example leaf
- See the scorekeeper directory
-
scorekeeper/ - Database-backed, full-featured example leaf
- config.yml - Optional leaf-global configuration options
- controller.rb - The leaf's controller object
- data/ - Optional directory for data storage (not used by Autumn)
- Gemfile - Leaf-specific gem requirements
- helpers/ - Modules that extend the controller and views
- lib - Library files loaded before the leaf
- models/ - Active record-type database objects
- tasks - Additional rake tasks for this leaf
- views/ - ERb views for each of the leaf's commands
-
insulter/ - Very simple example leaf
-
libs/ - Autumn core code
- channel_leaf.rb - A leaf subclass that can ignore messages from certain channels its in
- coder.rb - Used by script/generate to write out Ruby code
- ctcp.rb - CTCP support library
- daemon.rb - Provides support for different kinds of IRC servers
- datamapper_hacks.rb - Some hacks to help DataMapper work with Autumn
- foliater.rb - Instantiates and manages stems and leaves
- formatting.rb - Provides support for different kinds of IRC client text formatting and colorization
- generator.rb - Library used by script/generate
- genesis.rb - Boots the Autumn environment
- inheritable_attributes.rb - Adds support for class-level inheritable attributes
- leaf.rb - The core bot superclass
- log_facade.rb - Simplifies logging for stems and leaves
- misc.rb - RubyCore class additions and other knick-knacks
- script.rb - Library used by script/generate and script/destroy
- speciator.rb - Manages global, season, stem, and leaf configurations
- stem.rb - IRC client library
- stem_facade.rb - Additional methods to simplify the Stem class
- log/ - Directory where (most) Autumn logs are written (see the "Logs" section)
- Rakefile - Contains the rake tasks used to control Autumn (see the "Tasks" section)
- README - This file
- README.textile - Textile-formatted readme
-
resources/ - Data files used by Autumn
- daemons/ - Data files describing different IRC server types
-
script/ - Helper scripts for controlling Autumn
- daemon - Runs Autumn as a daemon
- destroy - Destroys Autumn objects
- generate - Creates Autumn objects
- server - Starts Autumn
- shared/ - Shared code libraries available to all leaves
- tmp/ - Temporary files, such as PID files
Configuring Autumn for Your First Launch
Before you can run Autumn and try out the example leaves, you'll need to set up a few things. Here are the steps:
Configure Your Testing Season
In Autumn, your leaves run in an environment, called a "season." Each season has
different leaves and different settings for those leaves. By default, Autumn
comes with a season called "testing" already set up for you. You can edit that
season or create a new one with script/generate season [season name]
. The
files for your season are stored in the config/seasons
directory.
First, edit the stems.yml
file. This file stores information about your IRC
connection. Edit it to connect to an IRC server of your choosing. For more
information, see Stems below.
Next, edit the database.yml
file. As mentioned previously, Scorekeeper
requires the DataMapper gem because it uses a persistent store. By default it's
set up to use a SQLite database, but you can use PostgreSQL or MySQL if you'd
like. If you'd prefer not to install any of these database solutions, delete the
database.yml
file and remove the Scorekeeper leaf from the leaves.yml
and
stems.yml
files.
If you do choose to set up a database, you will have to run DB=Scorekeeper rake db:migrate
after your database.yml
file is configured and your
database is created.
Lastly, view the leaves.yml
file. You shouldn't have to make any changes to
this file, but it's a good idea to look at it to see how leaves are configured.
You can do the same with the season.yml
file. See "Seasons" and "Leaves" below
for more.
Starting the Server
Run the command script/server
to start the server. After a short while, your
leaf should appear in the channel you specified. You can type "!points Coolguy
+5" and then "!points" to get started using Scorekeeper, or "!insult Steve" to
play with Insulter. Have some fun, and when you're satisfied, stop the server by
typing "!quit".
If you'd like to daemonize your server, you can use the shell commands rake app:start
and rake app:stop
. For more information, see Tasks below.
Making Your Own Leaf
Making your own leaf using Autumn is easy. In this tutorial, I'll show you how to make a simple Fortune bot that responds to a few basic commands.
Step 1: Subclass Leaf
Create a new leaf by typing script/generate leaf fortune
. This will create a
fortune
directory in the leaves
directory, along with the bare bones of
files needed within that directory. Edit the controller.rb
file. First we'll
create an array to hold our fortunes:
FORTUNES = [
"You will make someone happy today.",
"Someone you don't expect will be important to you today.",
"Today will bring unexpected hardships."
]
As you can see, our 3 meager fortunes are stored in the FORTUNES
class
constant. Now, we'll want it to respond to the "!fortune" command, and all you
have to do is create a method called fortune_command
to make it work:
def fortune_command(stem, sender, reply_to, msg)
FORTUNES.sample
end
Our method returns a fortune at random, which is automatically transmitted to the channel or nick where the command was received.
Of course, any self-respecting fortune bot announces its presence when it starts
up, so, in your Controller
class, override the {Autumn::Leaf#did_start_up}
method to display a cheerful greeting:
def did_start_up
stems.message 'FortuneBot at your service! Type "!fortune" to get your fortune!'
end
...and that's it! You now have a fully functional fortune bot featuring -- not two -- but three unique and exciting fortunes!
(For more on that stems.message
bit, see Stems.)
Step 2: Add the Leaf to Your Season
If you want, you can add the fortune bot to your leaves.yml
and stems.yml
files to try it out. Adding a leaf is easy; simply duplicate the structure used
for another leaf's entry and change the values as appropriate. A typical
two-leaf configuration will look like:
Scorekeeper:
class: Scorekeeper
respond_to_private_messages: false
Fortune:
class: Fortune
respond_to_private_messages: true
As you notice, each leaf instance is given a name. In this example the name happens to be the same as the leaf's type name, but you could run two copies of a leaf like so:
Fortune1:
class: Fortune
Fortune2:
class: Fortune
This doesn't make a whole lot of sense for our fortune bot, but for more complicated bots it can be useful. Note that we're running two different leaves, not stems -- so this is not how you would get your bot to run on two different channels, or servers; that's all the purvue of the {Autumn::Stem}. This strategy would be used to run two independent copies of the same bot, possibly on the same stems, possibly not.
So, we've created the leaf, but we have to add it to the stem for it to work.
(Remember, a stem is an IRC connection and a leaf is a bot.) So, in your
stems.yml
file, add an entry for this leaf. Your new config will appear
something like:
Example:
nick: Scorekeeper
leaves:
- Scorekeeper
- Fortune
rejoin: true
channel: somechannel
server: irc.someserver.com
When you restart the server, the bot will come back online and will now also respond to the "!fortune" command. This is a helpful tutorial on how stems and leaves are separate. One leaf can have many stems, and one stem can have many leaves. You can combine these two entities however you need.
Step 3: Upgrade to ERb Views
You've already learned that for your *_command
-type methods, the bot responds
with whatever string your method returns. For more complicated commands,
however, you may want to upgrade to full view abstraction, a la Ruby on Rails.
This is what the views directory is for.
If you place a .txt.erb
file (named after your command) in the views
directory, it will be parsed by ERb and rendered as the result. You can pass
variables to the ERb parser by using the {Autumn::Leaf#var} method. Let's
upgrade our fortune_command
method for that:
def fortune_command(stem, sender, reply_to, msg)
var fortune: FORTUNES.sample
end
We can then write a view, fortune.txt.erb
, which will render the fortune:
<%= var :fortune %>
OK, so admittedly, this doesn't really get us anywhere, but for more complicated bots, this well help separate view and controller concerns.
For more information on view rendering, see the {Autumn::Leaf#render} method.
Seasons
Each time you start Autumn, the process launches in a certain season (a.k.a.
environment context). This season is defined in the config/global.yml
file.
You can temporarily override it by setting the SEASON
environment variable
(e.g., SEASON=production script/server
).
It's important to realize that an season is just a name, nothing more. You can have as many seasons as you like, and name them anything that you like. Autumn will load the config files for the season you've indicated as active. Autumn doesn't really care if it's named "production" or "live" or "testing-on-jeffs-machine"; it's all the same to Autumn.
Your season's configuration is stored in the season.yml
file within your
season directory. Currently it supports one directive, logging
. This sets the
minimum log level (such as debug
or warn
). If the log level is set to
debug
, it also enables console output parroting. (See the Logging
section.)
The power of seasons comes in custom configuration options. For instance,
consider that you have a testing and production season. In your testing season,
your season.yml
file contains:
dont_http: true
and in production, it contains:
dont_http: false
Now, in your code, you might have a method like:
def scores_command(stem, sender, reply_to, msg)
if options[:dont_http]
return "Some fake sports scores."
else
# go on the web and find real sports scores
end
end
Standard Configuration Options
Global
System-wide configuration is done in the config/global.yml
file. It supports
by default the following directives:
season |
String | The season to launch in. |
log_history |
Integer | The number of historical logfiles to keep (default 10). |
In addition, the following options are available (but cannot be set in the YAML file):
root |
Pathname | The root directory of the Autumn installation. |
system_logger |
{Autumn::LogFacade} | The logger instance that records system messages. |
Season
Season-specific configuration is done in the
config/seasons/[season]/season.yml
file. Currently it only supports one
directive, logging
, which takes log levels such as debug
or warn
.
Stem
Stem-specific configuration is done in the config/seasons/[season]/stems.yml
file. It's important to note that stem and leaf configurations are completely
independent of each other. (In other words, stem options do not override leaf
options, nor vice versa.) Therefore, you generally won't add custom directives
to the stems.yml
file, because you generally won't be working with stems
directly. See the {Autumn::Stem#initialize} method for option documentation.
The channel
and channels
directives can also be used to specify a password
for a password protected channel, like so:
channel:
channelname: channelpassword
or
channels:
- channel1: password1
- channel2: password2
The port
, server_type
, and channel
/channels
options are set in the
config file but not available in the options
hash. They are accessed directly
from attributes in the Stem instance, such as the channels
attribute.
Leaf
Leaf-specific configuration is done in the config/seasons/[season]/leaves.yml
file and the leaves/[leaf]/config.yml
file, with the former taking precedence
over the latter. As mentioned above, leaf and stem configurations are completely
separate, so one does not override the other. See the {Autumn::Leaf#initialize}
method for option documentation.
In addition, the following options are available (but cannot be set in the yml file):
root |
Pathname | The root directory of the leaf installation. |
The leaves.yml
file is optional. When not included, each leaf in the leaves
directory will be automatically instantiated once.
Custom Configuration Options
All configuration files support user-generated directives. You can set options at any level. Options at a more narrow level override those at a broader level.
Options are maintained and cataloged by the {Autumn::Speciator} singleton. You
could access the singleton directly, but most objects have an options
attribute providing simpler access to the Speciator.
For example, to access options in a leaf, all you do is call, for example,
options[:my_custom_option]
. my_custom_option
can be set at the global,
season, or leaf level.
Leaves
The {Autumn::Leaf} class has many tools to help you write your leaves. These include things like filters, helpers, loggers, and an easy to use IRC library. The Autumn::Leaf and Autumn::Stem class docs are the most thorough way of learning about each of these features, but I'll walk you through the basics here.
The Many Methods of Leaf
By subclassing Autumn::Leaf, you gain access to a number of neat utilities. These generally come in three classes: IRC commands that have already been written for you, utility methods you can call, and invoked methods you can override. Utility methods do things like add filters. Invoked methods are called when certain events happen, like when your leaf starts up or when a private message is received. You override them in your leaf to customize how it responds to these events.
Invoked methods | {Autumn::Leaf#will_start_up will_start_up}, {Autumn::Leaf#did_start_up did_start_up}, {Autumn::Leaf#did_receive_channel_message did_receive_channel_message}, etc. |
Utility methods | {Autumn::Leaf.before_filter before_filter}, {Autumn::Leaf#database database}, etc. |
IRC commands | quit_command , reload_command , autumn_command , etc. |
See the class docs for more information on these methods.
In addition, your leaf is designated as a listener for its {Autumn::Stem} instances. In short, this means if you want even finer control over the IRC connection, you can implement listener methods. See the {Autumn::Stem#add_listener} method for examples of such methods.
Finally, your leaf can implement methods that are broadcast by listener plugins. An example of such a plugin is the {Autumn::CTCP} class, which is included in all stems by default. Visit its class docs to learn more about how to send and receive CTCP requests.
Filters
Filters are methods that are run either before or after a command is executed. In the former case, they can also prevent the command from being run. This is useful for authentication, for instance: A filter could determine if someone is authorized to run a command, and prevent the command from being run if not.
Use filters to save yourself the effort of rewriting code that will run before
or after a command is executed. Filter methods are named [word]_filter
and
they are added to the filter chain using the
{Autumn::Leaf.before_filter before_filter} and
{Autumn::Leaf.after_filter after_filter} methods (like in Ruby on Rails). As an
example, imagine you wanted your bot to say something after each command:
class Controller < Autumn::Leaf
after_filter :outro
private
def outro_filter(stem, channel, sender, command, msg, opts)
stem.message "This has been a production of OutroBot!", channel
end
end
The result of this is that after each command, the leaf will make a dramatic
exit. (Why did I use after_filter
and not before_filter
? Because as I said
earlier, a before_filter
can stop the command from being executed; the only
way we know for sure that the command was executed -- and therefore should be
outroed -- is to use an after_filter
.)
I made the outro_filter
method private because I felt it shouldn't be exposed
to other classes; this is not a requirement of the filter framework, though.
Now let's say you wanted to prevent the command from being run in some cases.
The most obvious application of this feature is authentication. Autumn already
includes a robust authentication module, but for the sake of example, let's
pretend you wanted to do your own authentication in your leaf. So, you write a
before_filter
to determine if the user is authenticated. before_filter
s have
return values; if they return false, the filter chain is halted and the command
is suppressed. If you want to have your leaf display some sort of message (like
"Nice try!"), you need to include that in your filter.
As an example, here's a simple form of authentication that just checks a person's nick:
class Controller < Autumn::Leaf
before_filter :authenticate, only: :quit, admin: 'Yournick'
def authenticate_filter(stem, channel, sender, command, msg, opts)
sender == opts[:admin]
end
end
I'm introducing you to three new features with this sample:
- You can use the
:only
option to limit your filter to certain commands. Note that you specify the command name as a symbol, not the method name (which would bequit_command
in this case). - You can pass your own options to
before_filter
andafter_filter
; they are passed through to your method via the last parameter,opts
. - The return value of a
before_filter
is used to determine if the command should be run. So be careful that your method does not returnnil
orfalse
unless you really mean for the command to be suppressed.
Both of these examples use the parameters sent to your filter method. They are, in order:
- the Stem instance that received the command,
- the name of the channel to which the command was sent (or
nil
if it was a private message), - the sender hash,
- the name of the command that was typed, as a symbol,
- any additional parameters after the command (same as the
msg
parameter in the[word]_command
methods), - the custom options that were given to
before_filter
orafter_filter
.
There are two built-in options that you can specify for before_filter
and
after_filter
, and those are only
and except
. They work just like in Rails:
The only
option limits the filter to running only on the given command or list
of commands, and the except
option prevents the filter from being run on the
given command or list. All other options are passed to the filter for you to
use.
Filters are run in the order they are added to the filter chain. Therefore, a superclass's filters will run before a subclass's filters, and filters added later in a class definition will be run after those added earlier.
If you subclass one of your leaves, it inherits your superclass's filters. The Autumn::Leaf superclass does not have any filters by default, though by default new leaves come with a simple authentication filter that checks the user's privilege level.
Authentication
You don't need to write a before_filter
as shown above, because Autumn already
includes a robust authentication module. The {Autumn::Authentication} module
includes the {Autumn::Authentication::Base Base} class and four different
subclasses. Each of these subclasses handles a different type of authentication.
You can choose the authentication strategy you want on a leaf-by-leaf basis or
for a whole season.
To specify the kind of authentication you want, you must add an authentication
directive to your config. If you want to set it for an individual leaf, add it
to the leaves.yml
file. If you want all leaves to have the same authentication
strategy, add it to the season.yml
or global.yml
file.
The authentication
directive should be a hash that, at a minimum, includes a
key called type
. This is the snake_cased name of subclass in
Autumn::Authentication that you wish to use. As an example, here is an entry for
an Administrator bot in a leaves.yml
file, with ops-based authentication.
Administrator:
class: Administrator
authentication:
type: op
This will instantiate the {Autumn::Authentication::Op} class for use with the Administrator bot.
Other authentication strategies may require additional information. For
instance, if you want to used nick-based authentication, your leaves.yml
file
might look like:
Administrator:
class: Administrator
authentication:
type: nick
nick: MyNick
See the class docs for each subclass in Autumn::Authentication for more info on how you should set up your configs, as well as the benefits and pitfalls of each provided authentication scheme, to help you choose the one most appropriate for you.
Persistent Stores
If you would like to use a persistent store for your leaf, you should install the DataMapper gem and an adapter gem for your database of choice (MySQL, PostgreSQL, or SQLite). DataMapper works almost identically to Active Record, so if you have any Rails programming experience, you should be able to dive right in.
Once you've got DataMapper installed, you should create one or more database
connections in your config/seasons/[season]/database.yml
file. A sample
database connection looks like:
connection_name:
adapter: mysql
host: localhost
username: root
password: pass
database: database_name
or, in a smaller syntax:
connection_name: "mysql://root@pass:localhost/database_name"
If you are using the "sqlite3" adapter, the path
option is the path to the
file where the data should be written (example:
leaves/fortune/data/my_database.db
). Omit the database
option.
You can name your connection however you want, but you should name it after either your leaf or your leaf subclass. (More on this below.)
You should also create DataMapper model classes for each of your model objects.
You can place them within your leaf's models
directory. This works almost
exactly the same as the app/models directory in Rails.
Once your database, data models, and leaves have been configured, you can use
the DB=[database config name] rake db:migrate
task to automatically
populate your database.
Now, unlike Rails, Autumn supports multiple database connections. Two leaves can use two different database connections, or share the same database connection. Because of this, it's important to understand how to manage your connections. Autumn tries to do this for you by guessing which connection belongs to which leaf, based on their names.
For example, imagine you have a leaf named "Fortune" and an instance of that
leaf in leaves.yml
named "MyFortune". If you name your database connection
either "Fortune" or "MyFortune" (or "fortune" or "my_fortune"), it will
automatically be associated with that leaf. What this means is that for the
leaf's command methods (such as about_command
) and invoked methods (such as
did_receive_private_message
), the database connection will already be set for
you, and you can start using your DataMapper objects just like Active Record
objects.
If, on the other hand, you either named your database connection differently from your leaf or subclass name or you are writing a method outside of the normal flow of leaf methods (for instance, one that is directly called by a Stem, or a different listener), you will need to call the {Autumn::Leaf#database database} method and pass it a block containing your code.
This is terribly confusing, so let me give you an example. Let's assume you've
got a fortune bot running a leaf named "FortuneLeaf", so your leaves.yml
configuration is:
FortuneBot:
class: FortuneLeaf
And you have a database connection for that leaf, named after the leaf's class:
fortune_leaf:
adapter: sqlite3
path: leaves/fortune_leaf/data/development.db
Let's further assume you have a simple DataMapper object:
class Fortune
include DataMapper::Resource
property :id, Serial
property :text, String
end
Now, if we wanted to write a "!fortune" command, it might appear something like this:
def fortune_command(stem, sender, reply_to, msg)
fortunes = Fortune.all
fortunes[rand(fortunes.size)].text
end
Autumn automatically knows to execute this DataMapper code in the correct
database context ("fortune_leaf"). It knows this because your leaf's name is
FortuneLeaf
, and your database context is named the same.
But what if you wanted to use that connection for other leaves too, so you named it something like "local_database"? Now, Autumn won't be able to guess that you want to use that DB context, so you have to specify it manually:
def fortune_command(stem, sender, reply_to, msg)
database(:local_database) do
fortunes = Fortune.all
return fortunes[rand(fortunes.size)].text
end
end
If that is too tedious, you can specify the database connection manually in the
leaves.yml
file:
FortuneBot:
class: FortuneLeaf
database: local_database
OK, now onto the second special case. Let's go back to the case where Autumn can
automatically guess your database connection name. Now, imagine you want your
fortune bot to also send a fortune in response to a CTCP VERSION
request. So,
you'd implement a method like so:
def ctcp_version_request(handler, stem, sender, arguments)
fortune = random_fortune # Loads a random fortune
send_ctcp_reply stem, sender[:nick], 'VERSION', fortune.text
end
This will break -- why? Because the ctcp_version_request
method is in the
realm of the {Autumn::CTCP} class, not the Autumn::Leaf class. (You can see
this by investigating the CTCP class docs; it shows you what methods you can
implement for CTCP support.) Basically, the CTCP
class calls your method
directly, giving the Autumn::Leaf class no chance to set up the database first.
So to fix it, make a call to database
first:
def ctcp_version_request(handler, stem, sender, arguments)
fortune = database { random_fortune }
send_ctcp_reply stem, sender[:nick], 'VERSION', fortune.text
end
This will execute those methods in the scope of the database connection guessed by Autumn::Leaf. Of course, you can manually pass in a connection name if necessary.
Another important note: You will need to make a call to database
in any
child threads your leaf creates. The database context is not automatically
carried over to such threads.
Your Leaf's Module; or, "What Do I Do About Namespace Conflicts?"
So, if you have two database-backed leaves, it's entirely likely that both of
them will use some sort of DataMapper resource named Channel
, or something
similar. You can't define the class Channel
twice in two different ways, so
how do you deal with this?
The answer is: It's already dealt with for you. Go ahead and define the class twice. Or three times.
The longer explanation is: Secretly, behind the scenes, all your leaf code is
being cleverly loaded into a module named after your leaf. So, when, in your
controller.rb
code, it says class Controller < Autumn::Leaf
, you should read
it as class MyLeafName::Controller < Autumn::Leaf
. When you define your model
with class Channel
, it's really read as class MyLeafName::Channel
.
Don't worry about table names or associations or anything, either. Just go ahead
and use it as if it weren't in a module. The libs/datamapper_hacks.rb
file has
all the necessary code changes to make this bit of trickery work.
Using Support Modules
Helper modules placed in your leaf's helpers directory will automatically be
loaded and included in your leaf controller and views. To create a helper
module, place Ruby files to be loaded into the helpers
directory. Make sure
your helper modules' names end with the word "Helper".
For instance, if your leaf's name is "Fortune", and you needed two helpers, a
database helper and a network helper, you could create two modules named
DatabaseHelper
and NetworkHelper
. Any modules named in this fashion and
placed in the helpers subdirectory will be loaded and appended to the
controller and its views automatically.
Loading libraries and gems
Files placed in your leaf's lib directory will be loaded and run before your leaf's controller, helpers, and views are parsed. You can place any extra code in this file, or preload any class definitions.
If you have specific gem requirements for your leaf, place them in a Gemfile in your leaf's root directory. If you place the gems in a group named after your leaf, the gems will only be loaded if your leaf is loaded. See the Scorekeeper Gemfile for an example.
You can also extend the existing :default
or :datamapper
groups if your gem
requires.
Debugging Your Leaf
If you make a simple code change to your leaf, you can reload it without having
to restart the whole process. See the reload_command
documentation in the
Administrator leaf for more information on when and how you can reload your
leaf's code.
If an error occurs on a live production instance, it will be logged to the log file for your season. You can inspect the log file to determine what went wrong.
If the error happens before the logger is available, oftentimes it will appear
in the autumn.output
or autumn.log
files. These files are generated by the
daemon library and note any uncaught exceptions or standard outs. They are in
the tmp
directory.
The most tricky of errors can happen before the process is daemonized. If your
process is quitting prematurely, and you don't see anything in either log file,
consider running script/server
, allowing you to see any exceptions for
yourself.
Unfortunately, it's still possible that the bug might not appear when you do
this, but only appear when the process is daemonized. In this situation, I'd
recommend installing rdebug (gem install rdebug
) and stepping through the code
to figure out what's going wrong. In particular, make sure you step into the
Foliater
's {Autumn::Foliater#start_stems start_stems} method, when it creates
the new threads. It's possible your exception will rear its head once you step
into that line of code.
Stems
{Autumn::Stem} is a full-featured IRC client library, written from the ground up for Autumn. It makes extensive use of implicit protocols, meaning that most features are accessed by implementing the methods you feel are necessary.
Most of the time, you will only work with stems indirectly via leaves. For instance, if you want an "!opped" command that returns true if the sender is an operator, it would look like this:
def opped_command(stem, sender, reply_to, msg)
stem.channel_members[reply_to][sender[:nick]] == :operator ? "You are opped." : "You are not opped."
end
Let's break this down. In order to figure out if someone is opped or not, we need three pieces of information: their nick, the channel they are in, and the IRC server they are connected to.
The stem
parameter contains the Stem instance that received this message. It
is our link to that server. Through it we can perform IRC actions and make
requests.
Stem includes an attribute channel_members
, a hash of channels mapped to their
members. The channel that received the message is passed via the reply_to
parameter. So we call channel_members[reply_to]
and we receive a hash of
member names to their privilege levels. The sender
parameter contains
information about the person who sent the command, including their nick. So we
use their nick to resolve their privilege level.
Complicated? Sure it is. That's the price we pay for separating stems from leaves. But what if you, like probably 90% of the people out there who use Autumn, only have one stem? Why should you have to call the same damn stem each and every time?
Fortunately, your pleas are not in vain. For leaves that run off only one stem, the stem's methods are rolled right into the leaf. So, that "!opped" command method becomes:
def opped_command(stem, sender, reply_to, msg)
channel_members[reply_to][sender[:nick]] == :operator ? "You are opped." : "You are not opped."
end
OK, so it's not like a world-class improvement, but it helps.
The primary thing your leaf will probably do with a Stem instance is use it to send messages, like so:
def about_command(stem, sender, reply_to, msg)
stem.message "I am a pretty awesome bot!", reply_to
end
Fortunately, if you just return a string, Autumn::Leaf will automatically send it for you, simplifying our method:
def about_command(stem, sender, reply_to, msg)
"I am a pretty awesome bot!"
end
You would still interact with the stem directly if you wanted to do something like announce your leaf's presence to everyone. To do this, you'd have to send a message to every channel of every stem the leaf is a listener for:
stems.each { |stem| stem.channels.each { |channel| stem.message "Hello!", channel } }
But! {Autumn::StemFacade#message} will automatically send a message to every channel if you don't specify any channels, simplifying our code to:
stems.each { |stem| stem.message "Hello!" }
It gets even better. You can call methods on the stems
array as if it were
a stem itself! This simplifies the line significantly:
stems.message "Hello!"
Pretty nifty, huh? This also works for functions as well as methods; for instance, the {Autumn::Stem#ready?} function, which returns true if a stem is ready:
stems.ready? #=> [ true, true, false, true ] (for example)
The nitty-gritty of stems
The section above dealt with stems as they relate to leaves. But when would you need to deal with a stem directly? Generally, never. However, if you find that Autumn::Leaf doesn't have what you need, you may have to turn to Autumn::Stem to get the functionality you are looking for. So let's take a look at how Stem works.
A stem interacts with interested parties via the listener protocol. Your leaf signals its interest to a stem by calling {Autumn::Stem#add_listener}. When a leaf or any other object becomes a stem's listener, that stem then invokes methods on the listener whenever an IRC event occurs.
Let's take a simple example. Assume you wanted to build a basic textual IRC client using Stem. You'd first want to indicate that your client is a listener:
class MyClient
def initialize(stem)
@stem = stem
@stem.add_listener self
end
end
Now the stem will send method calls to your MyClient
instance every time an
IRC event occurs. None of these methods are required -- you can implement as few
or as many as you want. The different methods that Stem will send are documented
in the {Autumn::Stem#add_listener} method docs. One very important method is the
irc_privmsg_event
method. Let's implement it:
def irc_privmsg_event(stem, sender, arguments)
puts "#{arguments[:channel]} <#{sender[:nick]}> #{arguments[:message]}"
end
Now we've got the most important part of our IRC client done -- receiving messages.
You can also send IRC events using stem. It's simple: Every IRC command (such as
JOIN
and PRIVMSG
and MODE
) has a corresponding method in Stem (such as
join
and privmsg
and mode
). These methods aren't in the API docs because
they're implemented using method_missing
. Their arguments are exactly the same
as the arguments the IRC command expects, and in the same order.
So how do we send a message? Well according to RFC 1459, the basic IRC spec, the
PRIVMSG
command takes two arguments: a list of receivers, and the text to be
sent. So, we know our method call should look something like this:
@stem.privmsg recipient, message
Astute readers will note that the spec shows a list of recipients, and indeed, you can call the method like so:
@stem.privmsg [recipient1, recipient2], message
That's the basics of how Autumn::Stem works, but there's one other thing worth mentioning, and that's listener plugins. The details are in the {Autumn::Stem#add_listener} method docs, but the short of it is that these are special listeners that bestow their powers onto other listeners.
The best example of this is the {Autumn::CTCP} class. This class is indeed a
Stem listener: It listens to PRIVMSG
events from the stem, and checks them to
see if they are CTCP requests. However, it also gives you, the author of
another listener (such as your leaf) the ability to implement methods according
to its protocol.
For example, say you wanted to respond to CTCP VERSION
requests with your own
version information. You do it like so:
def ctcp_version_request(handler, stem, sender, arguments)
send_ctcp_reply stem, sender[:nick], 'VERSION', "AwesomeBot 2.0 by Sancho Sample"
end
What's going on here? Because the Autumn::CTCP class is a listener plugin, it is
sending its own method calls as well as implementing Stem's method calls. One
such call is the ctcp_version_request
method, which you can see in the CTCP
class docs. Somewhere deep in the annals of Autumn::Foliater, there is some code
similar to the following:
ctcp = Autumn::CTCP.new
stem.add_listener ctcp
Thus, every stem comes pre-fab with a CTCP listener plugin. That plugin is
intercepting PRIVMSG
events and checking if they're CTCP requests. If they
are, it is invoking methods, such as ctcp_version_request
, in all of the
stem's other listeners, among which is your leaf. Hopefully you understand how
this all fits together.
The lesson to take home here is two-fold: Firstly, if you'd like CTCP support in your leaf, know that it's the Autumn::CTCP class that is providing the method calls to your leaf, not the Autumn::Stem class. Secondly, this should hopefully give you some ideas should you want to write your own listener plugin to enhance Stem's functionality.
Autumn's Logging
Autumn uses Ruby's Logger class to log; however, it uses {Autumn::LogFacade} to
prepend additional information to each log entry. The LogFacade class has the
exact same external API as Logger, so you can use it like a typical Ruby or
Ruby on Rails logger. Many objects (such as Leaf and Stem) include a logger
attribute:
logger.debug "Debug statement"
logger.fatal $!
See the {Autumn::LogFacade} class docs for details.
Tasks
The included Rakefile contains a number of useful tasks to help you develop and
deploy your leaves. You can always get a list of tasks by typing rake --tasks
.
The various commands you can run are:
Application tasks:
rake app:start |
Starts the Autumn daemon in the background. |
rake app:stop |
Stops the Autumn daemon. |
rake app:restart |
Reloads the Autumn daemons. |
rake app:run |
Starts the Autumn daemon in the foreground. |
rake app:zap |
Forces the daemon to a stopped state. Use this command if your daemon is not running but script/daemon thinks it still is. |
Database tasks:
DB=[database config name] rake db:migrate |
Creates all the tables for a leaf, as specified by the leaf's model objects. |
Documentation tasks:
rake doc:api |
Generates HTML documentation for Autumn, found in the doc/api directory. |
rake doc:leaves |
Generates HTML documentation for your leaves, found in the doc/leaves directory. |
rake doc:clear |
Removes all HTML documentation. |
Logging tasks:
rake log:clear |
Clears the log files for all seasons. |
rake log:errors |
Prints a list of error-level log messages for the current season, and uncaught exceptions in all seasons. |
Custom leaf tasks
You can define your own leaf-specific tasks in the tasks subdirectory within
your leaf's directory. Any .rake
files there will be loaded by rake. The tasks
will be added within a task-group named after your leaf. Use Scorekeeper as an
example: If you type rake --tasks
, you'll see one other task,
rake scorekeeper:scores
. The "scores" task is defined in the
leaves/scorekeeper/tasks/stats.rake
file, and placed in the "scorekeeper" task
group by Autumn.
Also, if you open that file up, you'll notice that you have to refer to your leaf's classes by their full names, including the leaf module. (See "Your Leaf's Module" if you're confused.)
Scripts
Autumn includes some scripts to help you control it. Each script accepts a
--help
command-line argument.
script/console
Bootstraps an IRb console with the Autumn environment configured. Stems and leaves are accessile from the Foliater instance. DataMapper models can be used. Does not start any stems (in other words, no actual server login occurs).
script/daemon
Controller for the Autumn daemon. Starts, stops, and manages the daemon. Must be run from the Autumn root directory.
script/destroy
Destroys the files for leaves, seasons, and other objects of the Autumn framework.
script/generate
Generates template files for leaves, seasons, and other Autumn objects.
script/server
Runs Autumn from the command line. This script will not exit until all leaves
have exited. You can set the SEASON
environment variable to override the
season.
Thread Safety
Autumn is a multi-threaded IRC client. When a message is received, a new thread is spawned to process the message. In this thread, the message will be parsed, and all listener hooks will be invoked, including your leaf's methods. The thread will terminate once the message has been fully processed and all methods invoked.
I have made every effort to ensure that Autumn::Stem and Autumn::Leaf are thread-safe, as well as other relevant support classes such as Autumn::CTCP. It is now in your hands to ensure your leaves are thread-safe! This basically means recognizing that, while your leaf is churning away at whatever command it received, things can and will change in the background. If your command requires your leaf to have operator privileges, write your code under the assumption that operator could be taken from your leaf in the middle of executing the command. Write data in critical blocks, use transactions in your database calls ... you know the deal. Don't assume things will be the same between one line of code and the next.
If you require thread synchronization at the expense of performance, you can use
the :stem_sync
annotation. See the {Autumn::Stem} class docs under
Synchronous Methods for more information.
Getting Ready for Deployment
There's only a few things you need to do once your leaf is ready to greet the Real World:
- Create a new production season. Configure your stems, leaves, and database as necessary for your production environment.
- In
config/global.yml
, set the season to your production season. - If desired, in
script/daemon
, set the:monitor
option to true. This will spawn a monitor process that will relaunch Autumn if it crashes.
Other Information
Please see https://github.com/RISCfuture/autumn/wiki/known-bugs for a list of known bugs, and https://github.com/RISCfuture/autumn/wiki/version-history for complete version history.