Skip to content

Dev Plug Ins

Joshua Haas edited this page Feb 4, 2018 · 8 revisions

Sibyl plug-ins are simple python scripts. Their filename must end in .py in order for Sibyl to load them. Plug-in files can contain anything, such as extra helper functions. If you want Sibyl to use a function for something, it must be decorated via sibyl.lib.decorators. All decorators can be imported from lib/decorators.py, for example from sibyl.lib.decorators import botcmd. The most commonly used decorators are botcmd, botinit, and botconf. You should also take a look at the following explanations and brief API listings:

  • SibylBot for important things like opt() and send()
  • Protocol for interacting with Message, User, and Room objects
  • Decorators for executing functions in response to specific events

Some people may learn better via tutorial. You can follow the construction of the alarm command in the alarm.py tutorial.

Decorators

You can define anything you want inside a plug-in, but Sibyl will only load those functions that have a decorator from sibyl.lib.decorators. Function names do not have to be unique across plug-ins, except for chat commands using @botcmd and bot functions using @botfunc. Decorated functions are stored in a dictionary, except for @botfunc functions which are stored as members of the bot itself. For detailed explanations of each decorator, go here.

Variables

Plug-ins are more than welcome to add their own member variables to the bot object, but to ensure no conflicts, they must use the add_var method. This will log helpful messages and prevent the bot from starting if needed. For example, to add the member variable foo with default value 'bar':

from lib.decorators import botinit

@botinit
def init(bot):
  bot.add_var('foo','bar')

This variable can then be accessed and modified from anywhere else in the plug-in as bot.foo. If no default value is specified, the variable will be initialised to None. You must only call add_var in a @botinit hook.

Sibyl also provides a method for persistent storage. All you have to do is pass persist=True in the add_var call. Note that this can be disabled globally with the persistence config option. Persistent variables are stored to the state_file on bot shutdown, and loaded on bot startup. Values loaded from disk are available immediately after the add_var() call.

IMPORTANT: Disabling a plugin will also delete its persistent variables.

IMPORTANT: Persistent storage may not work correctly with Message, User, and Room objects

Adding Config Options

It is very easy to add your own custom config options, all you have to do is use the @botconf decorator. Functions using it must return a dictionary or list of dictionaries with the following keys:

  • name - the name of the option (e.g. 'username')
  • default - the default value of the option (default: None)
  • req - if True, this config option must be set by the user or the bot will fail (default: False)
  • parse - a function to parse the value string to an object (default: None)
  • valid - a function to validate that the result of parse makes sense (default: None)
  • post - a function that performs tasks that depend on other config options (default: None)
  • white - a list of strings enumerating all valid values
  • black - a list of strings forbidding certain values

Only the name field is required, all other fields are optional. Sibyl automatically prepends the name of the plugin file to its config options for increased clarity and to prevent conflicts. Config options can then be accessed and modified using bot.opt('plugin.my_age'). Let's say the example below is in the file age.py.

from sibyl.lib.decorators import botconf

@botconf
def conf(bot):
  return {'name':'my_age','default':42}

@botcmd
def age(bot,mess,args):
  return bot.opt('age.my_age')

One more quick note, the general plugin has the config command, which can be used in chat to view and edit config options. However, any option whose name ends with 'password' will be redacted. Please use this convention to ensure passwords don't end up in chat. For more complex data structures, such as a dict with passwords as values, wrap the password strings in sibyl.lib.password.Password and use Password.get() to retrieve them. Such objects stringify as 'REDACTED' no matter their contents.

Parsing

After Sibyl reads the contents of the config file, the values of every option are strings. If, for example, you want my_age to end up as 12 rather than '12', you would want to create a parse function. If the parse function raises an exception, then the default value (or None if not specified) will be used.

def parse_age(conf,opt,val):
  return int(val)

Config parse functions must accept the 3 parameters shown above. The conf parameter is the sibyl.lib.config.Config object. The opt parameter is the name of the config option being parsed. The val parameter is the value for that option, as a string, that was found in the config file. The function must return the parsed version of val or raise an exception if unable to parse it. There are some pre-defined parse functions in lib.config.Config that you might find useful, namely parse_bool, parse_int, parse_float, and parse_pass.

These can be used easily, and would be specified in the @botconf function, for example by adding the following to the returned dictionary: 'parse':bot.conf.parse_int'.

Validating

After sibyl parses all of the config options, some options may need additional checking. For example, you might want to make certain that a file can be written or that an integer is in a certain range. This can be done with a validator function. These should not raise exceptions, and should return a boolean for whether the given value is valid or not. In this case, since we parsed the value for my_age into an int before, the validation would look like:

def valid_age(conf,val):
  return (val>0)

Config valid functions must accept the 2 parameters shown above. The conf parameter is the sibyl.lib.config.Config object. The val parameter is the value for the current config option, already parsed if needed. The function must return True or False. There are some pre-defined valid functions in sibyl.lib.config.Config that you might find useful, namely valid_ip, valid_rfile, valid_wfile, valid_dir, and valid_nump.

We would specify the above validator function inside the @botconf function by adding the following to the dictionary: 'valid':valid_age.

Post Functions

Sometimes a config option needs additional parsing or validation that depends on other config options. In such a case, you can set a post function that will receive the full config options dictionary. Post hooks from multiple options are not guaranteed to run in any particular order. Like a parse function, if a post function raises an exception, the default value for the option will be used.

def post_age(conf,opts,opt,val):
  if opts['chat_ctrl'] and val<18:
    conf.log('warning','You should be at least 18 to use chat_ctrl')
    raise ValueError

Config post functions must accept the 4 parameters shown above. The conf parameter is the sibyl.lib.config.Config object. The opts parameter is a dict containing every config option after parsing and validation. You should only read from this dict, not modify it. The opt parameter is the name of the option currently being evaluated. The val parameter is the value for the current config option, already parsed and validated. The function must return the new value for the config option, even if the value is unchanged.

We would specify the above post function inside the @botconf function by adding the following to the dictionary: 'post':post_age.

Black/White Listing

For simpler options, an alternative to writing parse or validation functions is to use a black or white list. Although you can use them together, I expect they will usually be used separately. Note that the default value for an option is special, and will always be valid no matter what is specified in these lists. Both lists are case sensitive.

For example, to restrict an option to only the primary colors via whitelist:

'white':['red','blue','yellow']

Or to prevent a user from specifying emacs or pico as their preferred editor:

'black':['emacs','pico']

Logging

Before raising an exception or returning False, any of the above functions should log a message so the user knows why the config value they entered is unusable. These messages should generally be level 'warning'. Logging is accomplished using the Config object. For an example see above in the "Post Functions" section.

Running Another Command

Sometimes it is conventient to run a chat command from inside a plug-in. To make this easier, Sibyl has the run_cmd method. An example is shown below.

import time
from lib.protocol import Message
from lib.decorators import botcmd

@botcmd
def tellall(bot,mess,args):
  """tell all users something next time they join the room"""

  if mess.get_type()!=Message.GROUP:
    return 'This command only works in rooms'

  proto = mess.get_protocol()
  frm = mess.get_from()

  users = proto.get_occupants(frm.get_room())
  for user in users:
    if user!=frm:
      bot.run_cmd('tell',[args],mess)

The run_cmd function has 1 required argument and 3 optional kwargs. The first argument is the name of the chat command to run. The args option allows you to send a list of args to the command (default []). However, for certain commands (those with raw=True set), you must pass a string rather than a list. The mess option allows you to pass a Message object to the command (default None). The check_bw option allows you to control if black/white listing should be applied to your invocation of run_cmd (note: only works if mess is also supplied).

Note that many @botcmd functions return a string that would normally be sent to the user. If you want this to happen while using run_cmd, you must catch its return value and either return it in your command or send it yourself.

Dependencies

Many plug-ins may require others to work. For example, the bookmark plugin couldn't do much without library or xbmc. There are two ways to specify this, both of which are special global variables commonly located near the top of a file.

__depends__ = ['library','bookmark']
__wants__ = ['room']

Any plug-ins specified in the __depends__ variable should be required for your plug-in to work at all. If any of them are missing when the bot is initialising, the failed dependency will be logged and the bot will fail to start. On the other hand, the __wants__ variable should be used for plug-ins that enhance functionality but are not required. Missing plug-ins included here will just result in an error message in the logs. It is up to the developer to check if plug-ins specified in __wants__ have been loaded or not.

if not bot.has_plugin('xbmc'):
  return 'This feature not available because the "xbmc" plugin is missing.'

Conveniently, you can check using SibylBot.has_plugin() as shown above.

Clone this wiki locally