-
Notifications
You must be signed in to change notification settings - Fork 6
Dev Decorators
In python, decorators are simply a short-hand for modifying functions. In
Sibyl, decorators are the basis for its config and plug-in frameworks. Upon
initialization, Sibyl searches the directory specified by the cmd_dir
config
option (default cmds/
) for .py
files containing decorated functions. It
also searches in protocols/
for @botconf
only. All decorators can be
imported from sibyl.lib.decorators
for use in plug-ins. Below is a short
listing with brief descriptions. For detailed explanations and examples, click
the links or scroll down the page.
-
botcmd
- chat commands -
botconf
- add options to parse from the config file -
botidle
- about once per second -
botinit
- bot initialisation
-
botcon
- successfully connected to server -
botdiscon
- disconnected from server -
botdown
- bot shutdown -
boterr
- received anERROR
message -
botfunc
- helper functions -
botgroup
- received aGROUP
message -
botmsg
- received aPRIVATE
orGROUP
message -
botpriv
- received aPRIVATE
message -
botsend
- a message is sent -
botstatus
- received a STATUS update -
botrecon
- attempting to reconnect to server -
botroomf
- failed to join a room -
botrooms
- successfully joined a room
This decorator makes the function available as a chat command using the name of
the function. You can hide the command from the help listing with the hidden
kwarg (defaults to False
). You can restrict chat commands with the ctrl
kwarg (defaults to False
), which will disable the command unless the user
sets chat_ctrl = True
in the config. You can run the command in its own
thread by setting thread=True
. Finally, you can receive arguments as a raw
string rather than a list of space and quote-blocked arguments using
raw=True
. Below we define two chat commands, foo
and explode
, as well as
some other triggers necessary for the latter.
import time,random
from sibyl.lib.decorators import botcmd,botinit,botidle
@botcmd
def foo(bot,mess,args):
frm = mess.get_user().get_name()
return 'Message from %s with args %s' % (frm,args)
@botcmd(name='explode',hidden=True,ctrl=True)
def this_name_doesnt_matter(bot,mess,args):
bot.explode_time = time.time()+random.randint(0,600)
bot.fuse_lighter = mess.get_from()
@botinit
def init(bot):
bot.add_var('explode_time')
bot.add_var('fuse_lighter')
random.seed(time.time())
@botidle
def check(bot):
if not bot.explode_time:
return
if bot.explode_time<=time.time():
bot.send('BOOM!',bot.fuse_lighter)
bot.quit('Sibyl exploded')
Functions using @botcmd
must accept 3 parameters as shown above. The bot
parameter is the SibylBot
object itself. The mess
parameter is a
sibyl.lib.protocol.Message
object. The args
parameter is a list containing
the arguments passed in the invoking message, but not including the command
name itself (see Commands for details on parsing), unless raw=True
,
in which case it's a string of the raw args without quote blocking or space
delineation. If you return a string from the function, it will be sent as a
reply to the user who invoked the command.
These functions (or the value of the name
kwarg if set) must be unique across
every plug-in. Duplicates will cause the bot to fail. Function names (or the
value of the name
kwarg) must be alphanumeric plus underscores (e.g.
[a-z][A-Z][0-9][_]
). If you want to be certain, you can test with
name.replace('_','').isalnum()
. Command names are forced to lowercase during
bot startup.
Be aware that, by default, chat commands are synchronous, and so the bot won't
process other chat commands or hooks until it has finished executing the
current command. If your command takes a long time to run, you should consider
running it in a thread with thread=True
. Please read the Race Conditions
section of the Threading tutorial.
Adds the function as an attribute of the bot itself. This is useful for cases
where other plug-ins may want to use a function. Functions using @botfunc
are
never used by the bot; they will only be called by plug-ins. Note, though, that
any plugin can use these, not just the plugin in which thay are defined.
from sibyl.lib.decorators import botfunc,botcmd
@botfunc
def reply(bot,text,mess):
bot.send(text,mess.get_from())
@botcmd
def test(bot,mess,args):
bot.reply('It works!',mess)
The bot is always passed as the first parameter, but other than that functions
using @botfunc
can define whatever additional parameters they like. These
functions must have unique names across all plug-ins. Duplicates will be
overwritten in no particular order.
When calling the function, do not pass bot
explicitly; during plugin loading
Sibyl binds @botfunc
functions to itself, meaning the SibylBot
object
will be passed as an implicit first parameter.
The function will be run during the initialisation of the bot. This is useful
for initialising variables or reading data files. These functions are run in
__init__
inside sibylbot.py
.
from sibyl.lib.decorators import botinit
@botinit
def init(bot):
bot.add_var('foo')
These functions must accept exactly 1 parameter, which is the bot itself. Also
note that @botinit
functions are called after config values have already been
loaded, so feel free to use bot.opt()
. Init functions from multiple plug-ins
are not guaranteed to execute in a specific order.
The function will run when the bot is shutting down (included rebooting). The bot is already disconnected from all chat servers at this point so sending won't work.
from sibyl.lib.decorators import botdown
@botdown
def save_config(bot):
bot.run_cmd('config',['save','*'])
These functions must accept exactly 1 parameter, which is the bot itself.
Multiple @botdown
functions are not guaranteed to execute in a specific order.
The function will run whenever the bot successfully connects to the chat server. These run after successful authentication, so you can send messages or do anything else.
import time
from sibyl.lib.decorators import botinit,botcon,botcmd
@botinit
def init(bot):
bot.add_var('last_connect',{})
@botcon
def connected(bot,proto):
bot.last_connect[proto] = time.time()
@botcmd
def alive(bot,mess,args):
pname = mess.get_protocol().get_name()
t = time.asctime(time.localtime(bot.last_connect[pname]))
return 'Connected since '+t
These functions must accept 2 parameters. The first is the bot itself and the
second is the name of the protocol that connected. If you need the protocol
object itself use bot.get_protocol()
.
The function will run whenever the bot is disconnected from the chat server.
This includes failing a reconnection attempt. We'll call the below example
discon.py
for purposes of config names.
import time
from sibyl.lib.decorators import botconf,botdiscon
@botconf
def conf(bot):
return {'name':'file',
'default':'data/sibyl_discon.log',
'valid':bot.conf.valid_wfile}
@botdiscon
def log(bot,proto,e):
with open(bot.opt('discon.file'),'a') as f:
f.write('%s | %s:%s\n' % (time.asctime(),proto,e.__class__.__name__))
These functions must accept exactly 3 parameters. The first is the bot itself,
the second is the name of the protocol that disconnected, and the third is the
exception raised by the disconnect. It can be any of: ConnectFailure
,
PingTimeout
, ServerShutdown
which can be imported from sibyl.lib.protocol
.
The function will run whenever the bot attempts to reconnect after having
disconnected. It runs just before the bot tries to reconnect. Once the result
of the connection attempt is known, @botcon
or @botdiscon
will run as
needed.
from sibyl.lib.decorators import botinit,botrecon,botcon
@botinit
def init(bot):
bot.add_var('recon_attempts',{})
@botrecon
def count(bot,proto):
bot.recon_attempts[proto] += 1
if bot.recon_attempts[proto]>5:
bot.quit('Protocol %s failed to reconnect after 5 attempts.' % proto)
@botcon
def reset(bot,proto):
bot.recon_attempts[proto] = 0
These functions must accept 2 parameters. The first is the bot itself and the second is the name of the protocol that is attempting to reconnect.
The function is run upon successfully joining a room, whether at bot initialisation, or when running a chat command.
from sibyl.lib.decorators import botrooms
@botrooms
def announce(bot,room):
bot.send('All systems operational.',room)
These functions must accept exactly 2 parameters. The first is the bot
itself,
and the second is the sibyl.lib.protocol.Room
object that was joined.
Same as @botrooms
except run when the bot fails to join a room. It also takes
a third parameter, error
, which is a string describing the reason for failure.
from sibyl.lib.decorators import botroomf
import logging
log = logging.getLogger(__name__)
@botroomf
def room_fail(bot,room,error):
bot.log.error('Failed to join room "%s" (%s)' % (room,error))
The function will be run upon receiving a message with type Message.STATUS
.
Which should be whenever a user logs on, changes their status, or logs off.
from sibyl.lib.decorators import botstatus
from sibyl.lib.protocol import Message
@botstatus
def hello(bot,mess):
if mess.get_status()[0]==Message.AVAILABLE:
name = mess.get_user().get_name()
bot.send('Welcome back %s!' % name,mess.get_from())
They must accept exactly 2 parameters: bot
is the bot itself and mess
is a
sibyl.lib.protocol.Message
object. Note that mess.get_status()
returns a
tuple of the form (status,msg)
where status
is any of: Message.UNKNOWN
,
Message.OFFLINE
, Message.EXT_AWAY
, Message.AWAY
, Message.DND
,
Message.AVAILABLE
and the second field in the tuple, msg
, is the user's
custom status text (e.g. "Out to lunch").
The function will be run upon receiving a message with type Message.ERROR
.
Doing anything significant with these is usually protocol specific.
from sibyl.lib.decorators import boterr
import logging
log = logging.getLogger(__name__)
@boterr
def error(bot,mess):
log.error('Received ERROR message: "%s"' % mess.get_text())
They must accept exactly 2 parameters: bot
is the bot itself and mess
is a
sibyl.lib.protocol.Message
object.
The function will be run upon receiving a message of type either
Message.PRIVATE
or Message.GROUP
. It is run before chat commands if the
message would invoke one.
from sibyl.lib.decorators import botmsg
@botmsg
def replace(bot,mess,cmd):
if 'emacs' in mess.get_text().lower():
bot.send('s/emacs/vi/',mess.get_from())
These functions must accept exactly 3 parameters: bot
is the bot itself,
mess
is a sibyl.lib.protocol.Message
object, and cmd
is a list containing
the command name and args if one will be run (e.g.
['bookmark','set','bob-southpark]
). If the message isn't running a command,
then cmd
will be None
. Thus functions can decide not to execute if the
invoking message is running a chat command.
Just like @botmsg
but only activates for Message.PRIVATE
messages.
Just like @botmsg
bot only activates for Message.GROUP
messages.
The function will be called approximately once per second by default (exact
frequency not guaranteed). This can be used, for example, to set an alarm by
constantly checking the current time. You can specify a frequency for execution
with the freq
kwarg, although this can never be lower than the idle_freq
config option. Finally, the hook will run in its own thread if you set the
thread
kwarg to True
.
import time
from sibyl.lib.decorators import botinit,botidle
@botinit
def init(bot):
bot.add_var('last_chime')
@botidle
def chime(bot):
t = time.localtime()
if t.tm_hour==bot.last_chime:
return
if t.tm_min==0:
bot.last_chime = t.tm_hour
for proto in bot.protocols.values():
for room in proto.get_rooms():
proto.send("Ding! It's %i o'clock!" % t.tm_hour,room)
The function must accept only one parameter, which is the bot itself. Functions
using @botidle
should not take long to execute, or else the bot may become
slow to respond to messages.
Sibyl times botidle hooks to make certain they aren't taking a long time. There
is an internal counter for each @botidle
hook. If a hook takes longer than
the idle_time
config option, sibyl increments its counter. If the hook
finishes within the limit, the counter is instead decremented (minimum 0). Once
the counter for a given hook reaches the idle_count
config option, the hook
will be deleted. In this way, only hooks that consistently exceed the time
limit will be disabled. So, by default, if a given hook takes longer than 0.1
seconds 5
times in a row, it will be disabled.
In general @botidle
hooks should only take a fraction of a second to execute;
otherwise the bot may become slow to respond. If you have a hook that takes
longer than a second, you should consider running it in its own thread by
setting thread=True
. Please read the Race Conditions
sections in the
Threading tutorial.
The function will return a list of config options that Sibyl will then look for
in the config file. These functions are called in __init__
inside
sibylbot.py
. The config values specified by functions using @botconf
will
all be set to default or read from the config file before @botinit
functions
are called.
from sibyl.lib.decorators import botconf
@botconf
def conf(bot):
return [{'name':'foo','default':'bar'},
{'name':'has_whales','default':False,'parse':bot.conf.parse_bool},
{'name':'myopt','default':0,'valid':valid,'parse':parse},
{'name':'primary_color','white':['red','blue','yellow']},
{'name':'user','black':['root']}]
def valid(conf,val):
if val<0:
conf.log('warning','Option "myopt" must be positive')
return False
if val>10:
conf.log('warning','Option "myopt" cannot exceed 10')
return False
return True
def parse(conf,opt,val):
return float(val)
The function must accept exactly 1 parameter which is the bot itself. It must
return a list of dictionaries defining config options. Each dictionary must
have the key 'name'
. Optionally they may also contain 'default'
, 'req'
,
'parse'
, 'valid'
, 'post'
, 'white'
, and 'black'
. For more explanation
see the config section on the Plug-Ins page.
The function will be called whenever sibyl sends a message. This hook is used,
for example, in the room.bridge
functionality to bridge multiple chat rooms.
IMPORTANT: If you call bot.send()
inside an @botsend
hook, always pass
hook=False
to prevent infinite looping.
import requests,lxml,re
from sibyl.lib.decorators import botsend
@botsend
def link_echo(bot,mess):
(text,to) = (mess.get_text(),mess.get_to())
urls = re.findall(r'(https?://[^\s]+)', text)
s = ''
for (i,url) in enumerate(urls):
r = requests.get(url)
title = lxml.html.fromstring(r.content).findtext('.//title').strip()
s += '[%s] %s ' % (i+1,title)
if s:
bot.send(s[:-1],to,hook=False)
The function must accept exactly 2 parameters: bot
is the bot itself and
mess
is the Message object being sent.
Also note that this hook will only be called if plug-ins follow convention and
call bot.send()
; if they call protocol.send()
directly, that message will
not execute @botsend
hooks.