Saucesum / SauceBot

SauceBot Twitch.TV Chat bot
http://www.saucebot.com
19 stars 16 forks source link

SauceBot Structure

SauceBot is designed as a flexible chat bot, capable of interfacing with many different chat services, while still providing a uniform interface to them all.

Table of Contents

Setup and Running

Prerequisites

As a prerequisite to running a SauceBot server, Node.js and MySQL must be installed on the system. Please refer to their respective documentation on how to accomplish this. Once these are installed, the dependencies.sh file in the root directory of the SauceBot project can be run to install all of the necessary node.js modules. Although SauceBot is written in CoffeeScript, it must first be compiled to JavaScript in order to be run on Node.js. This can be done using the coffee command, and while manual compilation is possible, a more convenient method is to use coffee -c -w -o <compiled> <source>, which will watch the source directory for file changes, and automatically compile any modified files into the compiled directory.

Starting the Server

The main executable for SauceBot is the server file. Once the SauceBot project is compiled, Node.js can be used to run this file, e.g., >node bin/server/server.js .... SauceBot requires a configuration file to run, whose location is passed as an argument to server. It is recommended to use the built-in configuration utility, server --config <file>, to generate this file. This will prompt the user with several questions, and then create an appropriate configuration file at the specified location. Once the configuration file exists, the server can now be started via server <file> [options...]. The full usage of server is as follows:

Usage:
    server <file> [-d | --debug | -v | --verbose | -q | --quiet] [--clear-db]
    server -h | --help
    server --version
    server --config <file>

Options:
    -h --help     Shows this screen.
    --version     Shows version.
    --config      Configures the SauceBot server.
    <file>        The config file to use [default ./server.json].
    -d --debug    Enables debug messages.
    -v --verbose  Enable verbose output messages.
    -q --quiet    Disables all non-error messages.
    --clear-db    Clears the database before starting the server.

Client-Server Communication

There is always one instance of the SauceBot server. This server communicates with SauceBot clients, which in turn communicate with various chat services. Client-server data communication is encoded with JSON. Each client represents one instance of a chat service.

Client Messages

A client can send various messages to the server; in JSON, each of these messages takes the form

{
    "cmd": command,
    "data": data
}

where command specifies the type of message being sent, and data is the payload of the message. Note that, unlike the examples, newlines within a message are not permitted, but each message must be separated by a newline. There are currently four types of messages that the server recognizes, listed with the format of their data payload:

{
    "chan": channel,
    "user": user,
    "op": opLevel,
    "msg": message
}

channel is the channel where the message was issued, user is the user who sent the message, opLevel is the optional op level of the user, and message is the raw message itself.

{
    "user": user,
    "msg": message
}

user is the sender of the private message, and message is the contents of the message.

{
    "cookie": authCookie,
    "chan": channel,
    "type": type
}

authCookie is a cookie-based token that is used to authenticate the client to ensure that it has permission to have whatever update it is requesting done, channel is optional and can be used to limit the effects of the update to one channel, and type determines what is to be updated. The possible values for type are 'Users', which forces a reload of all users from the database; 'Channels', which reloads all of the channels; 'Help' for indicating that help is being sent to channel; and if none of these, it is the name of a module which is to be reloaded.

{
    "cookie": authCookie,
    "chan": channel,
    "type": type
}

authCookie and channel are as in upd, but the available types in this message are different - 'Users' returns

Server Responses

As mentioned above, the server provides a number of functions to the Channel object in addition to the data received, so that the Channel can then respond appropriately to the client. These functions, listed with their arguments and the form of the data payload passed to the client, are as follows:

{
    "chan": channel,
    "msg": message
}

channel is the channel to send the message to, and message is the actual message to be sent.

{
    "chan": channel,
    "user": user
}

As usual, channel is the channel being operated on, and user is the user to ban from the channel.

{
    "chan": channel,
    "user": user
}

Like ban, channel is the channel that the ban is to be lifted in, and user is the user being unbanned.

{
    "chan": channel,
    "user": user,
    "time": time
}

channel is of course the channel that the timeout is to take effect, user is the user being temporarily removed from the channel, and time is the time, in seconds, that the user cannot join the channel.

{
    "chan": channel
}

Here, channel is the channel that the commercial is to be displayed in.

There are a few other messages that the server can send to the client - users, channels, and error. Channel objects do not receive functions to send these messages; however, these messages are used by the server to notify the client of certain situations, and should be handled by any client implementation. The error message will only be emitted when an internal error occurs within the server. The data for error messages is formatted as follows:

{
    "msg": err
}

where err is just a string indicating the nature of the error to the client. users messages are emitted in response to a get request with type 'Users', and the data payload of these messages will simply be a JSON array of the list of current users in the requested context, e.g.,

["user1", "user2", "user3"]

channels messages are used to provide updates to the client on the list of currently monitored channels. At the moment, a client will only receive this channel list after having made a get request with the 'Channels' type. The data payload of a channels message will be a JSON array of the channel objects, with each channel being represented by an object of the form

{
    "id": id,
    "name": name,
    "status": status,
    "bot": botname
}

where id is the unique identifier used in the database to distinguish the channel, name is the name of the channel, status is the status of the channel, either 1 to indicate that the channel is enabled or 0 for it being disabled, and botname is the name of the bot responsible for that channel.

Modules

A module is defined by a source file in the modules directory. Each channel has its own instance of a given module, so that module data can be channel specific, e.g., each channel can have its own list of chat filters, etc. To accomodate this, a channel object requests that a desired module be instantiated by module. module registers a file listener to listen for any new modules being installed, and will also manually attempt to load a module with a given name from the filesystem. Once the module instance is created, it is tied to that channel.

Requirements

While module facilitates the creation of module instances, it does not enforce many restrictions on what it loads. The only requirement imposed by the loader in module is that the loaded module has a name, description, and version attribute, and that it contains a New(channel) function that returns an instance appropriate for the channel argument that it is being created for; all of these required properties must be exported by the module file. However, module also provides a base class, Module, that implements a significant amount of module code. All modules should inherit from this class.

Module Functions

The Module class, as mentioned above, provides many functions for use by implementing modules. regCmd(trigger, level, fn) and regVar(name, fn) are used to register commands and variables, respectively, with the channel (see Message Handling). In regCmd, trigger is the name of the command being registered; level is an optional argument that specifies the permission level required to use the command, and fn is a function to be called when the command is run. regVar simply takes name, the name of the variable being registered, and fn, the handler function for the variable.

Module also defines several unimplemented functions that can be overridden by the module. These functions are load, unload, and handle. load and unload are simply no-argument functions that are called when the module is loaded and unloaded, respectively. The handle function is called whenever a message is received by the channel. It is passed the user who said the message received, the contents of the message, and the instance of the bot server, in that order.

Localization

Each module also has the option to export its own custom string values which can be localized on a per-channel basis, not only for language reasons, but also to make each channel fun and unique. A module that exports a strings map for string key-names to default values will have these values inserted into the string table, under a default entry. Each channel can then provide these strings to modules via the getString(module, key, args...) function, or a module can access its own localized strings with its str(key, args...) function. In both cases, the key is the string used to identify the string being localized, and args are optional values that can be substituted in sequentially for values of the form "@<number>@". Channel administrators can modify these strings from the default values, and getString will return these custom strings when available.

Other Options

Two more options for a module are the locked and ignore exports. locked can be used to ensure that a module cannot be disabled, and ignore specifies whether new channels should not automatically enable the module. Like the strings export, these properties are automatically stored in the database when the module is loaded.

To summarize, here is an example of the framework of a module:

# Basic information
exports.name        = 'MyModule'
exports.description = 'Basic module skeleton'
exports.version     = '1.0'

# Specifies that this module is always active
exports.locked      = true

# These are the custom strings that can be changed by an administrator of the channel
exports.strings     = {
    'string-1' : 'string 1 default value here'
    'string-2' : 'default value of string 2'
}

class MyModule extends Module
    constructor: (channel) ->
        # The default constructor stores the associated Channel instance as
        # @channel, so it is passed on to the superclass.
        # If there is no module-specific constructor, then the channel will
        # be automatically stored as an instance variable.
        super channel
        # Initialize any instance variables, etc.
        ...
    load: ->
        # Handle all data loading and initialization here, bearing in mind,
        # however, that this method may be called again to reload data,
        # although only after unload has been called.
        # Initialization may also include registering handlers, etc.
        ...
    unload: ->
        # Release all resources acquired by this module, and unregister any handlers.
        # Also be sure to save any pending changes to the database.
        ...
    handle: (user, message, bot) ->
        # This is usually unnecessary, as to be explained,
        # but you can manually work with messsages here.
        ...
exports.New = (channel) ->
    # Create and return a new instance of the module.
    new MyModule channel

Message Handling

In order to implement its functionality, a module will typically require the cooperation of its channel. Modules have two ways of receiving data from the channel - they can wait for data on the handle(user, message, bot) function, or they can register a listener with the channel via the Channel.register(trigger) function. If the direct option of listening for data is taken, the module will handle all pattern matching, parsing, etc., on its own. In the case of registering a listener, however, a Trigger object is used.

Triggers

A trigger is used for matching chat messages that take the form of !<command> [options...]. They are constructed via a call by the module to the channel's register(args...) function, which uses those arguments to call trigger.buildTrigger(module, command, opLevel, execute), with module being the module creating the trigger; command, the base of the command for matching purposes; opLevel, a level from sauce.Level, indicating the minimum permission level of the user who sent the message in order for any further processing to occur; and execute(user, args, bot), a function that runs if the trigger conditions match, taking as parameters the user who sent the command, the arguments to the command, and the bot responsible for the message.

Triggers are designed such that in the case of commands with multiple forms, for example, !timer and !timer start, only the trigger for !timer start will execute when someone sends the message "!timer start 123". The rule for trigger matching is that, if multiple triggers match a given message, the trigger with the most "parts" to it and a higher required permission level will execute over the others.

As an example, consider a command that enables users to set a timer to end after a given time, to stop a timer that has already been set, to check the status of a given timer, and to see all running timers. Suppose also that only moderator level users (sauce.Level.Mod) can start and stop timers. The following triggers would capture these messages:

    trigger.buildTrigger <module>, 'timer', sauce.Level.User, (user, args, bot) -> ...

where the execute function would check the number of arguments after "timer" to determine if the command is to show all timers, or a timer with a given name

    trigger.buildTrigger <module>, 'timer start', sauce.Level.Mod, (user, args, bot) -> ...

where args would contain, as its first entry, the name argument to the command

    trigger.buildTrigger <module>, 'timer stop', sauce.Level.Mod, (user, args, bot) -> ...

with args again containing the name of the timer to process

Message Variables

In many case, it may be useful to store variables, which may even be dynamically determined, for use in user-created commands. By registering a variable with the vars of a channel, any message processed by that Vars will have references to the variable in the message replaced with its evaluation. To register a variable, use the Vars.register(module, var, handler), with module being the module creating the variable, var being the name of the variable to register, and handler, a function taking the user who submitted the command and the rest of the arguments to the variable, and calling a callback with the replacement string for the variable. Variables are signified in a string by "$(name[ args...])".

Consider a variable $(time). A module could register this variable via the command

Vars.register <module>, 'time', (user, args, callback) -> ... # Call "callback" with result

Any message being processed by this Vars instance would have every occurrence of $(time) replaced with the time, as calculated by our handler function. This could allow custom messages (see Commands) to have embedded variables in them, and opens many new possibilities for interactivity.