Skip to content
Matthias Hecker edited this page Apr 16, 2020 · 5 revisions

Plugins are little ruby files that are dropped in the plugins subdirectory in the rbot directory, or in the rbot configuration directory (if you want per-configuration plugins - say different plugins for different irc servers or functions).

Plugins are run in anonymous modules which derive from the main rbot module. This allows them access to rbot functions and classes, but also allows them to drop their namespace and be reloaded at runtime. This means a new plugin can be dropped in the directory while rbot is running, and that a plugin can be modified while in use. Then tell rbot to rescan (rbot: rescan), and it will reload its modules dynamically. Or use rbot: rescan <plugin name> to only reload the given plugin.

A plugin class is provided for you to derive from and you must use it. There are two modes of plugin currently. One that registers itself for one of more commands. A command being the first word of a line addressed to the bot (either public or private), the rest of the line are parameters. The second class is a listener plugin, which gets to see all messages the bot sees, addressed or not - regardless of the first word of the line. These plugins can react to more events, but should be careful not to respond willy nilly to them.

Plugins are also wrapped in exception handling to prevent and uncaught exception in the plugin from causing the bot to crash.

Command plugin example

This is example code for a very simple plugin. It responds to a message of the form length .* and returns the length of the following string:

class LengthPlugin < Plugin
  
  # return a help string when the bot is asked for help on this plugin
  def help(plugin, topic="")
    return "length <string> => return length of string <string>"
  end
  
  # reply to a private message that we've registered for
  def privmsg(m)

    # m.params contains the rest of the message, m.plugin contains the first
    # word (useful because it's possible to register for multiple commands)
    unless(m.params)
      m.reply "incorrect usage. " + help(m.plugin)
    end

    length = m.params.length
    m.reply "length is #{length}"
  end
end

# create an instance of our plugin class and register for the "length" command
plugin = LengthPlugin.new
plugin.register("length")

Note that you can do a lot more than just reply to a message. The plugin class inherits a @bot member, which gives access to the bot data and functionality. @bot.say "#channel", "message", allows you to talk to channels, @bot.say "nick", "message" allows for private messages, @bot.action lets you perform /me-style actions, and then there's lots of other goodies, @bot.nick, @bot.quit, @bot.join, etc, etc. A link to the full API docs can be found at the top of this page.

The message class contains members like .target, the target of the message (which could be the bot's nick, or a channel name), it has functions like public? and private? to see if the message was private or in a channel, and address?, to see if the message was addressed to you (either privately messaged or in a channel in the form "bot-nick: message"). There's also .replyto, which contains the nick or channel you should reply to, which depends on the manner of address. The reply member function is just a shortcut for @bot.say m.replyto "message".

To create a listener plugin, define a listen() method, which takes a message object. Bear in mind that the message may not be addressed to you, and that a reply may not therefor be expected :)

For examples of plugins (probably the best way to see right now), just look at the plugins that come with rbot - they vary in complexity and the quotes plugin is a listener.

Plugin Routing

There's now a much easier way of connecting your Plugin methods to the commands that a user has to type in. It's kinda similar to Rails routing; basically, you create an instance of your plugin class, then use the ".map" instance method to connect user-commands to methods. Here's an example:

class SimplePlugin < Plugin
  def say_my_name_please(m, params)
    if params[:name]
      m.reply "The name I'm supposed to say is: #{params[:name]}"
    else
      m.reply "No name given!"
    end
  end
end

# create a new instance
plugin = SimplePlugin.new

# add a new action (user would do: '@say_name Bob')
plugin.map 'say_name :name', :action => 'say_my_name_please'

Pretty straight-forward example there -- :name is the parameter that gets passed to the action.

The .map method has a lot of neat features. Here are some examples.

Default Value for a Parameter

This lets you set the default value(s) for any parameter(s):

plugin.map 'eat :type_of_food', :defaults => {:type_of_food => 'cookie'}

Validate that a Parameter Matches a Regular Expression

This option checks that the :limit parameter is a number and that it can have an optional minus sign before it:

plugin.map 'digg :limit', :defaults => {:limit => 5},
                          :requirements => {:limit => /^-?\d+$/}

Accept any Number of Parameters

This option lets you pass a bunch of words as a single parameter:

plugin.map 'remember *phrase'

This will be passed as a list of words, so you'll need to concatenate them to get to the phrase:

def remember(m, params)
  phrase = params[:phrase].join ' '
end

Plugin Internals

Plugins are classes derived from Plugin or CoreBotModule which themselves derive from BotModule.

Plugins are registered in a global, singleton registry, the PluginManagerClass, which holds the @botmodules hash. This hash has as its keys either :CoreBotModule or :Plugin and as values a list of plugin instances. The plugin manager class also holds a reference to the main Bot class instance, which is set from the outside using bot_associate. This makes it possible to set a mock bot class during testing even though the plugin manager itself is a static instance.

Plugin constructors must call its parent constructor, the BotModule constructor registers the plugin in the global registry.

Plugins are loaded by the registry method #load_botmodule_file via its filename. This creates a new anonymous Module and uses #module_eval to evaluate the plugin source code, which in turn will have to create the instance of the plugin.

Going forward, to remove the singleton of the plugin manager we need to find a way to load plugins differently without having to rely on the BotModule constructor. One way might be to inspect the anonymous Module after evaluating and find all the BotModule instances.