-
Notifications
You must be signed in to change notification settings - Fork 6
Dev alarm
Here we will be writing a Sibly plug-in that allows users to set custom alarms. Development will be step-by-step and detailed. Many common decorators and API calls will be used and briefly explained.
We want a chat command that allows users to set an alarm that will trigger the bot to send a message at some point in the future. We would also like to create a config option to disable alarms in chat rooms so you can only use them in private chat.
We will be editing a file called alarm.py
which will be located in the
cmds/custom/
directory. We have to import the @botcmd
decorator in order to
flag our function as a chat command. For more info on decorators
click here. As long as you start Sibyl using run.py
the sibyl
directory will be on your python path, allowing for easy imports.
As shown below, our command currently has Sibyl reply with Not implemented!
when invoked in chat. For more details on Plug-Ins click here. Note
that mess
is the Message
object that triggered the command, and args
contains anything said in the same message after the command itself as a list.
from sibyl.lib.decorators import botcmd
@botcmd # this tells sibyl our function is a chat command
def alarm(bot,mess,args): # our function receives some info about who called it
return 'Not implemented!' # returned text is sent as a response to the user
We will keep the interface to our alarm
command very simple. In chat, a user
can say alarm TIME
to set a new alarm for the specified time. This is easy to
use in the function. If a user says sibyl alarm 9:00
in a chat room, then
Sibyl will automatically call our alarm
function and pass it the remaining
arguments. In the above example, args
would be ['9:00']
. If the user said
something like sibyl alarm in 30 minutes
then args
would be
['in','30','minutes']
. For the purpose of this tutorial we will only be
implementing the first case.
We will require the user to specify a time as H:MM
in 24-hour format. To let
users know how to use our command, we can add a short help string that will then
be used by Sibyl's built-in help
command.
def alarm(self,mess,args):
"""set an alarm to go off later - alarm H:MM"""
return 'Not implemented!'
We'll use python's convenient datetime
module to handle the time math. Visit
the python docs for more info. We need to convert the user-specified
time from a string into hour and minute, then use those to create a
datetime.datetime
object. This will make later logic much easier.
import datetime
from sibyl.lib.decorators import botcmd
@botcmd
def alarm(bot,mess,args):
"""set an alarm to go off later - alarm H:MM"""
# args will look like ['9:00'] so take the first item in the list and split it
(hr,mi) = args[0].split(':')
# get the current date and change its time to match the target time
now = datetime.datetime.now()
target = now.replace(hour=int(hr),minute=int(mi),second=0,microsecond=0)
# if the specified time is actually tomorrow, add 1 day to our target
if target<now:
target += datetime.timedelta(1)
Currently, when our function exits, we have no way to remember when the alarm
was. We need a place to store our alarms so we can access them even after this
function returns. The easiest way to do this is to define a new instance
variable in the SibylBot
instance which will persist even after our alarm
function ends.
In order to play nice between plug-ins, the correct way to create a new
variable is with bot.add_var()
. It makes certain that plug-ins don't
accidentally overwrite each others' variables. In order to use this, we need
the @botinit
decorator. We add the variable alarms
which will be
initialised to []
(an empty list).
from sibyl.lib.decorators import botinit
@botinit # this line tells sibyl to run the function during startup
def init(bot): # the only info our function gets is the bot itself
bot.add_var('alarms',[]) # we create a variable that we can access as bot.alarms
We actually need to store two things. Of course we need to store the time the
alarm should go off, but we also need to store who set the alarm so we know who
we should send a message to when it does. Now we simply have to add a few lines
to the end of our alarm
function to store the new alarm and the message that
created it. We simply add the Message
object and the alarm time to the
alarms
list as a tuple. While we're at it, let's add some user feedback too.
bot.alarms.append((mess,target)) # store the alarm and the Message that created it
return 'Alarm added' # respond to the user letting them know success
Of course, we need a way to trigger alarms when it's time. Although this isn't
guaranteed to activate the exact moment the current time equals the target
time, we can use @botidle
to get pretty close. Functions using @botidle
are
called about once per second by default. We simply iterate over every stored
alarm and check if its time has passed. If it has, we send a message to the
user who originally set the alarm. If it hasn't, we add it back to bot.alarms
to be checked again in the future.
from sibyl.lib.decorators import botidle
@botidle
def idle(bot):
# get the current time
now = datetime.datetime.now()
# create a new list where we will put alarms that didn't trigger
not_triggered = []
# look through all our alarms
for (mess,target) in bot.alarms:
# if the alarm time has passed, send the user who sent it a message
if target<=now:
# get the display name of the user from the stored Message object
name = mess.get_user().get_name()
# get the sender from the stored Message object
frm = mess.get_from()
# send a message that looks like "Bob: ALARM!"
bot.send(name+': ALARM!',frm)
# if the alarm time hasn't passed, keep the alarm stored and waiting
else:
# add the alarm to our list of ones to keep
not_triggered.append((mess,target))
# replace our stored alarms with the updated list
bot.alarms = not_triggered
It's pretty easy to add custom config options to Sibyl. It just involves another decorated function, but for more info visit this page. This is a great way to let users customize how commands behave.
Finally we need one more decorator @botconf
in order to define a custom
config option. If the user sets alarm.allow_rooms = False
in the config file,
then users will only be able to set alarms in private chats. Note that even
though we specify our option's name as only allow_rooms
sibyl automatically
adds the file name to the front, so in order to use the option you have to use
alarm.allow_rooms
. We also have to parse the option from a str
to a bool
.
from sibyl.lib.decorators import botconf
@botconf
def conf(bot):
return {'name': 'allow_rooms', # the name of our option
'default': True, # its default value
'parse': bot.conf.parse_bool} # how to interpret it in the config file
Now we have to edit our alarm
function to check what the value of the config
option is. We simply add the below to the start of the function. We also need
another import in order to check if the message was private or from a room. To
get the value of a config option, simply use bot.opt(name)
. Note that you must
specify the full name, including the name of the plugin.
from sibyl.lib.protocol import Message
@botcmd
def alarm(bot,mess,args):
"""set an alarm to go off later - alarm H:MM"""
# check if the Message is from a room but the user has disabled allow_rooms
if mess.get_type()==Message.GROUP and not bot.opt('alarm.allow_rooms'):
# let the user know why their alarm wasn't created and exit the command
return 'Alarms are disabled in chat rooms'
We made some assumptions that might not always work out. For example, if a user
says sibyl alarm foo bar
then our input parsing will fail and python will
throw an exception. We have the option of accounting for that or letting Sibyl
handle it. Any un-caught exceptions raised during the execution of a chat
command are caught by SibylBot
. However, it's nice to give users feedback, so
instead let's add a try
/except
clause in the alarm
function.
# if anything inside the try block encounters an error, we'll go to the except block
try:
(hr,mi) = args[0].split(':')
now = datetime.datetime.now()
target = now.replace(hour=int(hr),minute=int(mi),second=0,microsecond=0)
# let the user know why their alarm wasn't created and exit the command
except ValueError:
return 'Time must be in the format H:MM'
You can find the final code below, as well as in the examples/alarm.py
file. You can test the version you've been editing, or just copy the
aforementioned file to cmds/custom/
and then restart sibyl. You can test the
behavior of the config option without restarting sibyl using the config
chat
command:
sibyl config set alarm.allow_rooms False
sibyl config set alarm.allow_rooms True
You can also check that sibyl help alarm
returns our help text. And finally
the full script:
import datetime
from sibyl.lib.protocol import Message
from sibyl.lib.decorators import botinit,botconf,botcmd,botidle
@botinit
def init(bot):
bot.add_var('alarms',[])
@botconf
def conf(bot):
return {'name': 'allow_rooms',
'default': True,
'parse': bot.conf.parse_bool}
@botcmd
def alarm(bot,mess,args):
"""set an alarm to go off later - alarm H:MM"""
if mess.get_type()==Message.GROUP and not bot.opt('alarm.allow_rooms'):
return 'Alarms are disabled in chat rooms'
try:
(hr,mi) = args[0].split(':')
now = datetime.datetime.now()
target = now.replace(hour=int(hr),minute=int(mi),second=0,microsecond=0)
except ValueError:
return 'Time must be in the format H:MM'
if target<now:
target += datetime.timedelta(1)
bot.alarms.append((mess,target))
return 'Alarm added'
@botidle
def idle(bot):
now = datetime.datetime.now()
not_triggered = []
for (mess,target) in bot.alarms:
if target<=now:
name = mess.get_user().get_name()
frm = mess.get_from()
bot.send(name+': ALARM!',frm)
else:
not_triggered.append(alarm)
bot.alarms = not_triggered