Skip to content
Browse files

Added initial draft of internal API and design documentation

  • Loading branch information...
1 parent 87307a2 commit e9d31c74f980dcc35f4c30564c18d2e5b5ef5bec Andrew Brown committed Sep 12, 2012
Showing with 503 additions and 0 deletions.
  1. +503 −0 doc/documentation.rst
503 doc/documentation.rst
@@ -0,0 +1,503 @@
+Abbott Documentation
+Abbott is a generic plugin framework designed with support in mind for an IRC
+bot. The core of Abbott consists of a general plugin loading and
+inter-communication mechanism, and a mechanism for plugins to save persistent
+data. All bot functionality is provided by plugins, the core is just there to
+tie all plugins together.
+Abbott is built on Twisted, and heavily uses twisted concepts. For example, the
+entire bot is single threaded; plugins are expected not to block for IO or any
+other reason. Rather, they are expected to make use of Twisted deferreds and
+the Twisted reactor's callLater() function, as well as Twisted's facilities
+for IO.
+For those that aren't familiar, Twisted deferreds are, put simply, a promise
+that a value will exist at a later time. They are a kind of monad. If a
+function returns a deferred, it typically means that the function will provide
+its value at a later time through the deferred. The caller assigns a callback
+function to the deferred, which will be called when the value is ready. Plugins
+make heavy use of the defer.inlineCallbacks decorator to simplify the common
+pattern of pausing a function to wait for a deferred, then resuming the
+function. For more information, consult the Twisted docs on deferreds.
+Plugin API
+Plugins typically inherit from the pluginbase.BotPlugin type, and should live
+in a module within the abbott.plugins package. Plugins should not typically
+override the constructor (but can still be useful to initialize values before
+any other method is called). The constructor takes three parameters: the plugin
+name, the Transport object, and the PluginBoss object (see below for about the
+Transport and PluginBoss objects).
+The default constructor assigns the transport and pluginboss to the local
+attributes by the same names, and calls the reload() method.
+The following three methods are required of every plugin:
+start() takes no parameters, and is called once when the plugin has been
+loaded. This is the usual place to do any initialization and set up all the
+callbacks that the plugin wants to listen to.
+stop() takes no parameters, and is called once when the plugin is to be
+unloaded. This method should cancel any timed, recurring, or deferred events as
+appropriate. Transport hooks are unhooked automatically and do not need to be
+handled here. If nothing needs to be done, a plugin may leave this method
+reload() takes no parameters, and is called whenever an external event signals
+to the plugin that its configuration has changed in the persistent store. The
+plugin's job for this method is to make any necessary changes to its runtime in
+response to the new configuration. The default implementation of reload() is to
+retrieve the new config and assign it to the local attribute “config”. If this
+is sufficient, then there is no reason to override this method.
+There are also the following methods, described in the Transport section of
+this document: received_event(), received_middleware_event(),
+incoming_request(), install_middleware(), listen_for_event(),
+Plugin Boss
+The plugin boss is a core class. One instance exists per invocation of Abbott,
+and it handles loading, unloading, and reloading of plugins, as well as
+handling persistent data for the plugins. Plugins are provided the plugin boss
+instance as the third parameter to their constructor and it's typically
+assigned to a plugin's “pluginboss” attribute.
+There is one method of the plugin boss of special interest to plugins: the
+get_plugin_config() method. It is called with a single parameter: the plugin
+name (provided as the first argument to the constructor and typically set to
+the “plugin_name” attribute), to retrieve the persistent data for this plugin.
+The default implementation of a plugin's reload() method calls
+get_plugin_config() and assigns the result to the “config” attribute.
+Config object
+The config returned by get_plugin_config() is an object that implements the
+dictionary interface, and has an additional method: save(), which saves the
+config to disk. Anytime a change is made to the config, save() should be
+Any basic type can be stored in a config: dictionaries, lists, strings (unicode
+only), floats, ints. Implementation note: config is saved with JSON so only
+JSON types are allowed.
+Transport Layer
+The transport layer provides inter-plugin communication for all Abbott plugins,
+and plugins are expected to exclusively use the provided transport layer for
+all communication with other plugins. Every plugin is provided an instance of a
+Transport object as the second parameter to its constructor and it's
+typically assigned to a plugin's “transport” attribute.
+The transport layer has two ways for plugins to communicate. Both are similar,
+but are intended for different situations and uses. One is the *Event* system,
+where plugins can broadcast events to any other plugin, and the other is the
+*Request* system where a plugin can *request* some value or action of another
+An event is a generic broadcast from any plugin to any other plugin. That is,
+any plugin can broadcast any event, and any plugin can listen for any event. It
+can be thought of as *something has happened* and plugins to which the event is
+relevant can react to it, such as an incoming IRC message. However, it can also
+be used for outgoing events, such as outgoing IRC messages (which are broadcast
+by any plugin and listened for by the IRC plugin).
+Events are named by a string, with two components separated by a dot. By
+convention, the first section is a general category of message, and the second
+is the specific event. For example, ”irc.on_privmsg” is broadcast by the IRC
+plugin when a privmsg is received by the server.
+Plugins can listen for an event normally, or as “middleware”. When an event
+enters the transport object, all middleware handlers are called, and then all
+normal handlers are called. Middleware handlers are allowed to modify or even
+swallow an event, so they are handy for providing additional functionality,
+either transparently, or functionality that other plugins are aware of and can
+use (such as adding new attributes to an event).
+Event API
+The following methods are provided on the base BotPlugin for communicating with
+the transport's Event system.
+ These two methods install the plugin as a listener for the specified event.
+ Events may be globbed, such as “irc.*” to listen for all irc events. A
+ plugin will typically call one or more of these methods in its start()
+ method, such as self.listen_for_event("irc.*")
+ These two methods are called by the transport layer when events that were
+ requested earlier were fired by some plugin. The parameter is the event
+ object that was sent. See below for information about Event objects.
+ Middleware handlers are expected to return the (possibly modified) event
+ object, or None to indicate the event is to be swallowed and no more
+ handlers called.
+ The default implementation of these methods dispatch to a method whose name
+ is derived from the event name. For events, this is on_event_%s() and for
+ middleware it's on_middleware_%s() where %s is the event name with dots
+ replaced with underscores. For example, the event irc.on_privmsg calls
+ on_event_irc_on_privmsg(). Plugins may override this behavior if they wish.
+To send an event, a plugin must first make a new instance of an Event object
+(abbott.transport.Event). Event objects take one positional parameter: the
+event name, and zero or more keyword parameters: the event attributes. Each
+keyword parameter is assigned directly to the object's attributes, so
+Event("some.event", attr=1) will have event.attr == 1. Event objects are just a
+Once an Event object is constructed, it is sent by passing it to the
+transport's send_event() method. Recall that the transport object is assigned
+to the “transport” attribute of each plugin, so plugins typically call
+self.transport.send_event(eventobj). Or, to be more concise:
+self.transport.send_event(Event("", ...))
+Requests are similar to events. The difference is that only one plugin is
+allowed to listen for a particular request, and request handlers are expected
+to return a twisted deferred that will fire at some point. Requests are meant
+to be a way of requesting from a plugin some data or some action that returns
+Requests are named the same as events: with a string made of two components
+separated by a dot. Requests and Event names do not share a namespace.
+Requests API
+The following methods are provided on the base BotPlugin for communicating with
+the transport's Request system.
+ Indicates the plugin will provide a handler for the given request name.
+ Note that the name cannot be globbed here; a plugin must declare every
+ request name it wishes to handle.
+incoming_request(name, \*args, \**kwargs)
+ Called when another plugin has issued a request by the given name. args and
+ kwargs are as passed by the caller. This function is expected to return a
+ deferred, and is an error to return anything else.
+ The default implementation of this method dispatches to a method whose name
+ is derived from the request name. The format is on_request_%s() where %s is
+ the request name with periods replaced by underscores.
+To issue a request, a plugin should call transport.issue_request(name, \*args,
+\**kwargs). The args and kwargs are passed as-is and are defined by which
+request is being called.
+Command Plugins
+The CommandPluginSuperclass is a subclass of BotPlugin that plugins themselves
+may subclass to gain lots of boilerplate code to handle IRC commands. Plugins
+that derive from CommandPluginSuperclass depend on several IRC-related plugins
+in order to function, thus is tightly integrated with IRC. A future feature
+would be to abstract a command interface away from IRC and have an IRC-command
+connector so that other data sources can interface with the same command
+plugins, but since Abbott was built to be an IRC bot, the command plugins
+tightly integrate with the IRC plugins right now.
+The CommandPluginSuperclass allows plugins to declare in their start() method
+commands that they provide, and the superclass automatically handles analyzing
+incoming lines that look like commands, parsing them, dispatch, permissions,
+and automatic help text.
+Plugins that derive from CommandPluginSuperclass (hereby called “command
+plugins”) declare commands they provide by calling self.install_command() for
+each command they provide. This is typically done in the start() method, but
+make sure to call super(my_command_plugin, self).start() since the super class
+also has some stuff in start().
+Here are the parameters for install_command(). All except cmdname and callback
+are optional.
+ A string indicating the command name itself. This is used in the help
+ listing and, unless cmdmatch is specified, is also how you invoke the
+ command.
+ The callable to call when the command is invoked. The callable gets two
+ parameters: the event that initiated the command, and a re.Match object,
+ used to retrieve the parameters.
+ A string to explain the arguments of this command. Something like
+ “<required arg> [optional arg] ...”
+ A regular expression that matches the *arguments* of this command. It
+ should not include the command name, and it *should* end in a dollar sign
+ unless you know what you're doing.
+ A permission string that is required for this command to succeed. If this
+ is None or not specified, then everyone can invoke this command. See below
+ for about the permission system.
+ Commands are typically invoked with a prefix. For example, if the prefix is
+ ! then a command is invoked by saying “!cmdname”. By default, commands use
+ the globally defined prefix. If you wish to also add another prefix to
+ invoke this command, add it here.
+ Text to say along with the usage text for this command in the help output.
+ This ought to explain what the command does.
+In addition to defining commands, plugins may define command *groups*. A
+command group is a way of logically grouping commands and not polluting the
+global command namespace. Grouped commands are invoked with::
+ !groupname commandname [args] ...
+(assuming ! is the prefix). To declare a group, invoke self.install_cmdgroup()
+in the start() method. This takes the following arguments
+ The name of this command group
+ Assign a custom prefix for all commands in this group
+ Assign a *default* permission for all commands in this group. You can still
+ override this for individual commands.
+ Text to display when showing help information for this group.
+the install_cmdgroup() method returns a group object. Plugins then invoke the
+group.install_command() method to install group commands (with the same
+parameters and details as the above install_command() method)
+Authentication and authorization are provided by the stock plugin auth.Auth. It
+is used by command plugins, and so it's worth explaining it here instead of
+with the auth plugin.
+Commands can declare a permission that is required in order to invoke it.
+Permissions are strings that are made of components separated by dots. They are
+somewhat heirarchical and can contain as many components as you wish. Users are
+granted permission to e.g. “perm.action” and then may perform commands that are
+listed as requiring permission “perm.action”.
+Users may be granted a globbed permission, such as “perm.*” to grant all
+subpermissions of perm. Users may also be assigned the permission “perm”
+directly, and they get all sub-permissions of that permission. The difference
+is those users will also be granted permission to use commands assigned just
+“perm”, while users with “perm.*” cannot invoke commands with just “perm”.
+Globs do not transcend dots, so permissions such as “perm.*.asdf” are also
+possible, although I have yet to think of a good use for that pattern.
+Permissions are assigned on a per-channel basis, or globally. The super-user
+permission is simply “*”. Also, default permissions can be granted that apply
+to all users regardless of their authentication or identification.
+This section lists each plugin that comes with abbott, what they do, and the
+events and requests they each provide / listen for / react to.
+This is the main IRC bot plugin. It handles connecting to an IRC server, and
+acts as a connector between the server and the Abbott transport layer: messages
+from the server are relayed as events, and events are relayed to the server.
+A note about unicode: this plugin correctly handles unicode for all string
+parameters to its events. It WILL pass unicode objects to events that it emits,
+and correctly handles unicode objects to events it listens for. For outgoing
+lines, unicode control characters are stripped out except for a small whitelist
+that includes standard IRC color codes and CTCP codes.
+Also implements rate limiting for messages to the server. If 5 lines are sent
+to the server in less than 2 seconds, then a rate limit of 1 line every 2
+seconds is set until no lines have been sent for 2 seconds.
+All event emitted take the form irc.on_* and all events that are listend for
+take the form irc.do_*.
+Events emitted
+Event("irc.on_join", channel)
+ Emitted when a channel is joined. The channel parameter is the name of the
+ channel joined.
+Event("irc.on_part", channel)
+ Emitted when we part a channel.
+Event("irc.on_privmsg", user, channel, message, direct)
+ Emitted when we receive a PRIVMSG from the server.
+ user
+ The user that sent the message
+ channel
+ The channel that the message was received on
+ message
+ The message content
+ direct
+ this is a boolean that is set to True when channel is equal to the
+ bot's nickname, indicating this was sent directly to the bot instead of
+ seen on a channel.
+Event("irc.on_notice", user, channel, message)
+ Same as privmsg but for NOTICE messages. The lack of a direct parameter is
+ probably an oversight.
+Event("irc.on_mode_change", user, channel, set, mode, arg)
+ Emitted when we witness a mode change on a channel.
+ user
+ the user that instigated the mode change
+ channel
+ the channel where the mode changed
+ set
+ True or False whether the mode was set or unset
+ mode
+ A single character indicating what mode was changed
+ arg
+ The argument, or None if this mode doesn't take an argument
+Event("irc.on_user_joined", user, channel)
+ Emitted when we witness a user joinind a channel. Arguments are
+ self-explanatory.
+Event("irc.on_user_part", user, channel)
+ Emitted when we witness a user part a channel.
+Event("irc.on_user_quit", user, message)
+ Emitted when we witness a user quit
+Event("irc.on_user_kick", kickee, channel, kicker, message)
+ Emitted when we witness a user being kicked.
+Event("irc.on_action", user, channel, data)
+ Emitted when a user performs an action on a channel
+Event("irc.on_topic_updated", user, channel, newtopic)
+ Emitted when a user updates a topic on a channel
+Event("irc.on_nick_change", oldnick, newnick)
+ Emitted when we witness a user change nicks
+Event("irc.on_unknown", prefix, command, params)
+ Emitted on events which *twisted* doesn't have a handler for. This is
+ sort-of a catch-all, but this is not necessarily all IRC messages which we
+ don't have handlers for. Check the twisted code for exactly which messages
+ are caught with irc_unknown().
+Events Listened for
+Send these events from your plugin to do something in IRC! Note that for events
+that fail (such as doing things that require OP without OP), there is no way to
+tell if it succeeded or failed unless you listen for the appropriate failure
+message from the server, which will be emitted via irc.on_unknown.
+Event("irc.do_join_channel", channel)
+ Join the specified channel
+Event("irc.do_leave_channel", channel)
+ Leave the channel
+Event("irc.do_kick", channel, user, reason)
+ Kick the user from the channel (bot must have OP or this will silently
+ fail, unless you are listening for the appropriate failure message that
+ will probably come through irc.on_unknown)
+ reason is optional
+Event("irc.do_invite", user, channel)
+ Sends an invite to the user for the given channel.
+Event("irc.do_topic", channel, topic)
+ Attempts to set the channel topic. This will silently fail if the bot is
+ not OP or the channel is not +t.
+Event("irc.do_mode", channel, set, modes, limit, user, mask)
+ Attempt to perform a mode change on the channel. Set is a boolean, and
+ modes is the modestring to set or unset.
+ You must specify one of limit, user, or mask for modes that take
+ parameters. See twisted.words.protocols.irc.IRCClient.mode for details.
+Event("irc.do_say", channel, message, length)
+ Send a message to a channel. This is just like do_msg below except channel
+ must be a channel, not a user.
+ length is optional and indicates the maximum length of a single line.
+ messages beyond that will automatically be sent as multiple messages. If
+ not specified, a good value is estimated and used automatically.
+Event("irc.do_msg", channel, message, length)
+ The standard interface to IRC's PRIVMSG. user is either a nick or a
+ channel. length is the same as irc.do_say above.
+Event("irc.do_notice", user, message)
+ Sends a NOTICE to the given user (a nick or channel).
+Event("irc.do_away", message)
+ Sets the client as away with the given message
+ Clears the client's away status
+Event("irc.do_whois", nickname, server)
+ Issues a whois to the server for the given nickname.
+ server is optional as per the IRC protocol.
+ Note that responses are emitted through a series of irc.on_unknown events.
+Event("irc.do_setnick", nickname)
+ Issues a request to change the client's nickname.
+Event("irc.do_quit", message)
+ Issues a QUIT message to the server with the optional message.
+Event("irc.do_raw", line)
+ Sends a raw line to the server. Trailing newline is not required. This can
+ be used to send non-standard messages such as freenode's REMOVE command, or
+ commands that twisted simply doesn't know about such as NAMES.
+Requests Provided
+ Deferred fires immediately with the bot's current nickname
+A command plugin that

0 comments on commit e9d31c7

Please sign in to comment.
Something went wrong with that request. Please try again.