-
Notifications
You must be signed in to change notification settings - Fork 6
Dev Plug Ins
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()
andsend()
-
Protocol for interacting with
Message
,User
, andRoom
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.
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.
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
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 ofparse
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.
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'
.
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
.
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
.
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']
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.
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.
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.