User:Thorog/Plugins
From SquadWiki
Note: This has been converted from Markdown so there may be some errors. Parts of it (expecially the standards) are still fluid and may be subject to change.
Wurrfing IRCBot operates mainly due to a myriad number of small ruby plugins. In fact, some of the processes run by these plugins are so vital that the bot would not function properly if they weren't "plugged in".
Plugins are relatively simple to write. The only requirement is a basic knowledge of Ruby and this document.
NB: In the following passages, all code is places in pre tags, and any comments by the author which do not reflect on the actual code and how to make plugins (e.g. possible future changes) will be blockquoted, in italics.
Contents |
How plugins work
All plugins are stored in a certain folder, which is loaded on launch. Plugins effectively consist of three main parts:
- The help files, which are loaded by the Help plugin.
- An array of one or more Actions, which define how plugins parse the code given to them.
- The Plugin itself, a framework that ties the whole thing together.
When a plugin is created, it is registered with the MessageParser module. This module stores an array of all Plugins and Actions. Every time a message is received by the bot, it is passed through each item in MessageParser. Plugins run the Plugin#evaluate method, which effectively runs Action#validate on every Action stored by the plugin. Actions run Action#validate. If Action#validate returns true, Action#execute is called. The exact code that is run on validation and execution can be defined by various methods, detailed later.
The Framework
All plugins descend from the Plugin class, although not always directly. Some plugins (such as those used for 1KBL) are sub-classes of parent plugins which themselves descend from the Plugin class. Whichever way it works, all plugins must have the Plugin class somewhere in their ancestry (i.e. the command My_Plugin.kind_of? Plugin should return true).
The basic structure of a plugin looks like this:
require './classes/plugins/plugin'
class My_Plugin < Plugin
def initialize
super
@name = "My Plugin"
@description = "A simple plugin to demonstrate the plugin code pattern"
any initialisation occurs here
end
#additional methods, if desired
end
my_action = Action.new(
:name => "A new action",
:description => "This is a new action, to go inside My Plugin"
)
my_action.validate_with{|message, account| #validation block goes here}
#can also use validate_with_key or validate_with_args
my_action.execute_with{|message, account| #execution block goes here}
My_Plugin.new.add_actions my_action
It should be noted that all plugins are singleton, and thus repeated calls to, for example, My_Plugin.new will always return the same object. While it may seem unwieldy for a new class to be created for every plugin, even those whose basic composition does not differ from the base Plugin class, the singleton nature allows plugins to be easily "fetched" from other methods.
In the future it is possible that I may change my code so that individual Plugin objects represent plugins, rather than classes. Finding a plugin will depend on the @name property, and will be found via MessageParser#find. However, the presence (or absence) or a @name property currently determines whether or not the plugin shows up in help files.
Actions
An Action represents a single function performed by the bot. Often plugins have several actions, which are generally grouped together by a common theme or focus. For example, the Ghost_Plugin class contains three actions - one to process the !ghost command without any arguments, one to process the !ghost command with arguments, and one to process the !hint command. Actions basically consist of two blocks - one block to evaluate whether or not the action should be executed, and one block to be executed should the evaluation return true.
The evaluation is performed by a block stored internally as @validate_block. It is called using the Action#validate method, and can be set using a series of methods which take different arguments.
- validate_with &blck will set @validate_block to the supplied block. Two parameters - the message and the account - are passed to the block, and can be used to check whether certain conditions are met. For example, a block to check whether a beta tester said "!ping Wurrfing" (assuming the bot's name is Wurrfing) could look like:
action.validate_with do |message, account|
message.kind_of? Privmsg_Message and
message.text =~ /^!ping #{account.nick}$/i and
People.eval(message.sender.nick) >= People::Beta_Tester
end
(Some of these modules and methods haven't been discussed yet - they will come up under "Additional Resources" below.)
- validate_with_args hash_args will create a block which checks various properties of the message against values given by the hash in a "property => value" form. Allowed properties are:
- :kind or :type compares the class of the message with the class given using Object#kind_of?
- :text compares the text of the message with the value given (string or regex)
- :from or :sender compares the sender of the message with the given value (string or User object)
- :to or :target compares the target of the message (if the message is targeted) with the given value (string, User or Room object, or :me for the bot itself)
- :authority checks the sender's authority using People#eval (see below)
The same block from before would look like the following:
action.validate_with_args( :type => Privmsg_Message, :text => /^!ping Wurrfing$/i, #Doesn't support account in here :authority => People::Beta_Tester)
- validate_with_key string compares only the text of the message (assuming it is a PRIVMSG) with the given string or regex. For example, the closest we could get to the previous block is:
action.validate_with_key(/^!ping Wurrfing$/i) #doesn't support account or authority
It should be noted that Wurrfing considers ACTION messages to be CTCP messages and as such will not respond to ACTIONed commands, even if they would match the string or regex.
This is for a variety of reasons, namely that every single CTCP message type except for ACTION is treated differently to other PRIVMSG and NOTICE messages, and as such it is easier for me to treat CTCP messages as a separate class type. However if you give me a good reason why I should treat them as PRIVMSGs, feel free to discuss it with me.
The execution block is similar to the validate block in that it is passed the same to parameters in the same order. The execution block is only called when calling the validation block returns true.
Help
The purpose of the help support is for every Plugin and Action to be documented easily from the class itself, rather than create another file for documentation. Every Plugin and Action has three properties for use in help - @name, @description, and @group.
@name is simply the name of the plugin or action. When generating the help "tree", these names act as folders and documents. If a plugin or action has no name, it is not mentioned in the help. Debug procedures also use the @name value of the plugin (or, if @name is nil or blank, the class's name) for identification purposes.
@description is the general help text that the reader will view upon checking the help for that object. As such, @description should more be about what the plugin does and how to use it than how it works - such comments should be place in the code, if at all. Note that if @name is blank or nil, @description has no real purpose as the plugin will not show in the help anyway.
@group is an optional property that allows certain plugins to be grouped together arbitrarily. We will discuss this more later.
Plugins also have a fourth property, @create_subgroup, set to true by default. If this is true, all actions without their own groups will be placed in a subgroup of the plugin's @group value with the same name as the plugin. Otherwise they will all go in the folder of the plugin's @group value.
The Help Tree
The main idea of help is to create a "tree" of topics, much like, say, a UNIX or Windows file system. To extend the analogy further, plugins represent folders in this tree, while actions represent files. Similarly, a file's place in the tree can be described as <plugin>//.../<action>.
By default, each plugin makes a folder with its name in either the folder of the plugin it descends from or (if it is an immediate subclass of Plugin) in root. So for example, if we had the following classes:
class One_Step_Plugin < Plugin def initialize @name = "One-Step Plugin" end end class Another_Plugin < Plugin def initialize @name = "Another Plugin" end end class Sub_Plugin < Another_Plugin def initialize @name = "Our sub Plugin" end end
We would end up with the following diagram for our help tree:
root
|
----------------
| |
One-Step Plugin Another Plugin
| | |
<actions> <actions> Our sub Plugin
|
<actions>
It is possible to change the defaults in several ways. The @group value, if set to anything other than nil, overrides the default path to the plugin (so for example, if Sub_Plugin.group was set to "One-Step Plugin", it would appear in the "folder" /One-Step Plugin/). Setting @group to or '/' places it in the root "folder". The @create_subgroup value, set by default to @true, determines whether the plugin's actions will be placed in the folder created by the plugin, or simply in the same place as a plugin (e.g. if the plugin Sub_Plugin had @group set to nil and @create_subgroup set to false, all of its plugins would be located at /Another Plugin rather than /Another Plugin/Our sub Plugin).
The Action class has a @group property too, which allows individual actions to placed elsewhere than under their plugin.
Additional Resources
Modules
These can be found in the directory "./modules"
People
People.eval(user)
The people module keeps track of important people in chat. There are several constants that are used to distinguish between groups of people:
Constant Who Thorog Thorog, the owner of Wurrfing Trusted Only the most trusted of people Beta_Tester Beta testers for the bot OneKLiner 1KBL moderators
The "rank" of a chatter can be found by passing the user object (or the person's nick) to People.eval. Since these are numerical constants, comparison operators (<=>) can be applied to them (so, for example, Trusted > Beta_Tester evaluates to true).
Help
Help.find(path, create_new=false)
This module details the structure of the help tree, but you can also use it to your own ends by creating your own custom help groups. In order to find a particular folder, simply use the method Help.find, passing in the folder path as a string. If you wish to create the folder (should it not exist), add true as the second argument. If the folder does not exist and you did not pass true as the second argument, a CodeError is returned with code ERR_HELP_DNE. For example, the following code will change the description of a group.
my_node = Help.find '/group/subgroup/my node is here', true my_node.description = "This is the new description"
It should be noted that the only difference between "files" and "folders" here is that folders have children. Adding a child to a file will make it a folder (and in fact, there is no actual difference between files and folders in the code).
There is currently no set way of creating groups other than going about it via plugins. It is possible to make your groups via "dummy plugins" (i.e. plugins that are no different from Plugin but have some sub-classes); this is useful for organising classes, but adds to the number of plugins that must be evaluated per message. It is suggested that if you wish to place a lone action in a different place, change its @group value and set the description (if any) in that same file; while if you wish to group the actions of plugins together in one place, create a dummy plugin and set all sub-plugins' @create_subgroup to false.
Log
Log.log(file, text)
The Log module logs data to file. The main function is Log.log, which takes a file and some text as arguments (in that order). The text is appended to the specified file, which is stored in ./logs. Logs are generally suffixed with the '.log' extension, and tend to be write-only affairs, used to record events for future reading.
Mode
Mode.convert(mode, old_type=:Character, new_type=:Number) Mode.auto_convert(mode, new_type) Mode.add(m1, m2) Mode.subtract(m1, m2)
The Mode module provides several functions for switching modes between symbol (seen before nicks in NAME lists), character (used to set modes) and integer (used for internal storage) forms. Using the symbols :Character, :Number and :Symbol you can easily switch between the various types. The following table may prove useful:
Type Symbol Char Number Admin ! a 8 Op @ o 4 Halfop % h 2 Voice + v 1
The mode is generally expressed internally as a number formed by adding all applicable modes together (for example, a voiced op would have a mode of (4+1=) 5). Mode.convert will convert a mode from one type to another, while Mode.auto_convert will work out what sort of mode you pass it and convert it to new_type. Mode.add will perform addition of modes, and ignore any overlaps (e.g. adding Op+Halfop (4+2=6) to Voice+Op (4+1=5) would give Op+Halfop+Voice (4+2+1=7)). Mode.subtract subtracts m2 from m1 in a similar way.
Classes
These can be found in the directory './classes'
CodeError
CodeError.new(err_code, err_text=nil) CodeError#code CodeError#text
The CodeError is an Exception that can be given a code and some accompanying text. CodeErrors are used throughout Wurrfing to indicate errors not in the bot's code, but in a user's input. CodeErrors are generally not fatal, and almost all plugins have a catch for them.
All CodeError codes (and associated constants, prefixed ERR_ and usually followed immediately by some sort of category (e.g. ERR_GAME_ or ERR_CARD_)) are located in the file codeerror.rb, which also stores default messages in a Hash. Should error text not be supplied, the default message is used.
Talk to Thorog in order to register CodeError codes.
I am planning on restructuring CodeError significantly, including allocating numbers for codes a lot more differently. I am currently looking at making codes 100-199 core codes, 200-299 for 1kbl, and 700-999 miscellaneous plugin error codes. Although very few plugins have to deal with other plugins' errors (and it therefore might be feasible to reuse codes), the specific errors and their default messages must be easily located, and so they will be kept separate for the time being. Later on I may create a plugin-style system whereby CodeError is subclassed for various plugins and the different definitions are stored in them, thus allowing overlap of codes. However I have yet to scope out the pros and cons of such a system; for the time being, ask if you wish a certain block of error codes set aside.
Account
Account#user(username,create_new=true) Account#room(roomname,create_new=true)
The Account class is one of the most important classes in Wurrfing, not the least because it stores all information on users and rooms. The user and room methods allow plugins to find a User or Room object corresponding to a string (for example, calling Account#user("Thorog",false) will return the User object for the user Thorog).
Message
Message#sender Message#target Message#replyto Message#text
The Message class represents a single message to the server from a user. This may be a PRIVMSG to a room, or a global NICK message, or a simple PING message. There are many subclasses of message, which will be detailed below. However the most common one is the PRIVMSG, and all four methods above apply to it. Message#sender returns the user object for the person who sent the message, while Message#target returns either the room or the user (in the case of a PM) it was sent to. Message#text contains the actual text of the message (effectively the text you would see in an IRC client, not the raw message). Finally, Message#replyto will return the appropriate place that replies should be sent (the room if it was sent to a room, the sending user if it was a PM).
User, Room
User#say(string) User#act(string) User#name
User and Room share several important methods. The two you will be using most often are say and act. Say sends a PRIVMSG to the room or the user with the text given, while act does the same with a CTCP action message. name gives the room or user's name.
Plugins
These can be found in the directory './classes/plugins</tt>
Response
Response_Plugin#add_actions(act1, act2, act3...)
The Response plugin is a particularly specialised plugin that usually does not do anything. However, you may add actions to this from any other class, and these actions will only ever run once. The plugin is mainly used for getting user responses to questions, for example:
my_action.validate_with_key(/^!ask/) my_action.execute_with do |message, account| message.sender.say "What is your favourite colour?" the_reply = Action.new the_reply.validate_with_args( :from => message.sender, :to => :me, :kind => Privmsg_Message ) the_reply.execute_with do |rmsg, ract| if %w(red orange yellow green blue indigo violet).include? rmsg.text.downcase rmsg.sender.say "You may pass!" else rmsg.sender.act "tosses you into oblivion." end end Response_Plugin.add_actions the_reply end
Standards
There are several standards for plugins that should be met when coding in order to reduce the amount of mess in maintaining and storing many small files of code. There are currently two sets of standards - mandatory standards and recommended standards. Mandatory standards are standards that must be observed by all plugins, and are currently observed by all existing plugins. Recommended standards are standards that it is favourable for plugins to observe, although not all plugins currently observe them. As I update the current plugin list, more and more standard will move from being recommended to being mandatory.
Mandatory Standards
- All plugin classes must end in "_Plugin". Words must be underscore-separated, and title-case (i.e. the first letter of every word must be capital). For example, "A_New_Plugin", "Another_Plugin" and "This_Is_A_Long_Named_Plugin" are all valid, while "NewPlugin", "One-More-Plugin", "HowAboutOneMore_Plugin" and "Just_another_Plugin" aren't.
- Only one plugin per file. The file's name must be the plugin class name, all lower case, with underscores removed. Only .rb is allowed as a suffix. For example, "One_More_Plugin" must be stored in onemoreplugin.rb.
- Extensions to plugins should either go in the same file as the plugin, or in separate files in a folder named similarly to the plugin (i.e. files for the plugin file fooplugin.rb would be stored in the folder fooplugin/.
Recommended Standards
- Supporting classes to a plugin must start with the plugin's name, in order to prevent namespace collisions. For example, a class supporting the plugin Time_Plugin might be called Time_Plugin_Zone
