-
Notifications
You must be signed in to change notification settings - Fork 6
Dev Protocol
If you would like Sibyl to work with another chat protocol, you just have to
subclass Protocol
, User
, amd Room
from lib.protocols
and define the
necessary functions. Some of this information is available in
lib/protocols.py
itself as comments. Refer to that file for javadoc-like
function signatures. Together, the Message
, User
, Room
, and Protocol
classes provide an abstract API that allows Sibyl to interface with widely
varying chat protocols. You should also take a look at the following
explanations and brief API listings:
-
SibylBot for things like
add_var()
and callbacks -
Decorators specifically for the
@botconf
decorator
Some people may learn better via tutorial. You can follow the construction of
the mail
protocol in the sibyl_mail.py tutorial.
In order for Sibyl to use the protocol, the file must be in the protocols/
directory and named sibyl_name.py
where name
is the name of the protocol.
The name
should be lowercase, and is what users will specify in the
protocols
option in the config file. For example, you would use
sibyl_xmpp.py
with protocols = xmpp
. Each file should only have a single
Protocol
sub class.
You can use protocols/skeleton.py
as a base, edit it, and then save it as the
correct file name. Most of the information in the comments from
lib/protocols.py
is also present in protocols/skeleton.py
.
You do not need to implement this class, but you will have to use it when
coding the others. A Message
object is used to represent anything received
from the server, be it a text message, status update, or error.
A new message object has the following signature:
msg = Message(user,txt,typ=None,status=None,msg=None,room=None,
to=None,broadcast=False,users=None,hook=True,emote=False)
The first field user
must be a sub-class of sibyl.lib.protocol.User
and
denotes who sent the message. Then txt
is the message body, which can be
str
or unicode
.
The next field typ
can be any of the following enums listed below, along
with their purpose. If not specified, it defaults to Message.PRIVATE
.
Message.STATUS # status updates e.g. 'Available' or 'Away'
Message.PRIVATE # private messages between only two people
Message.GROUP # messages from a chat room
Message.ERROR # error messages from the server itself
The next two parameters are only used for Message.STATUS
messages. The
status
argument can be any of the following enums listed below, along with
their purpose.
Message.UNKNOWN # unknown or unlisted status type
Message.OFFLINE # user is offline
Message.EXT_AWAY # user is gone for a long period
Message.AWAY # user is idle
Message.DND # user does not want to be disturbed
Message.AVAILABLE # user is available
The next argument msg
is for custom status text, e.g. Back in 5 minutes
or
Listening to "Pachelbel's Canon"
. The final parameter room
is used if the
message was sent from a Room, and should be a sub-class of the
sibyl.lib.protocol.Room
class. If the message was sent from a room this
parameter must be set.
In general protocol implementers should never need to use the next two, which
are used by Sibyl internally when sending Messages. The broadcast
argument
is a boolean for whether this Message should be broadcast to everyone in a
room. The users
argument is used by the bridging code to highlight users
across the bridge in a broadcast message.
Next we have hook
which is set False
when sending messages from within
@botsend
hooks to prevent infinite looping. You shouldn't have to set this
directly, as in general it should be done via bot.send
or bot.reply
.
The final parameter is used for emote messages, e.g. /me is here
so they get
translated properly between protocols.
A brief description of each method is given below. For signatures see
lib/protocol.py
. Also note that type_to_str()
is a static method and so can
be called with Message.type_to_str()
. The second group of functions are used
internally by Sibyl. The third group of functions should not be called
directly.
get_from() # returns the Room if it exists else the User
get_protocol() # return the associated Protocol object
get_room() # return the Room that the message was sent from (or None)
get_status() # return a tuple of (status enum, status message str)
get_text() # return the message body
get_type() # return the typ enum
get_user() # return the User that sent the message
get_emote() # return True if this is an emote (e.g. "/me")
set_text(text) # set the message body
type_to_str(typ) # return a human-readable str for the given typ enum
get_to(self) # return the intended recipient of this Message
get_broadcast(self) # return if this Message should be broadcast to the room
get_users(self) # return additional users to highlight for a broadcast
get_hook(self) # return True if this Message should proc @botsend hooks
__getstate__() # controls the behavior of the pickle module
__init__() # initialise the object
The first class you must override is the User
class. It creates a User
object used to define who sent a Message
. You must override every method
prefaced with the @abstractmethod
decorator. Equality checking should include
resource/device information; if you need to check equality without those, use
user1.base_match(user2)
.
The signature for a new User
is shown below. Note that the User
class itself
is abstract, and so cannot be directly instantiated. Only sub-classes can create
an object, and so this guide will use MyUser
as an example sub-class.
usr = MyUser(proto,user,typ=None,real=None)
The first field proto
is the Protocol object, which in most cases will just
be self
when creating a MyUser
in your protocol. Next, user
can be
anything you want (str
, dict
, etc.) since only your protocol will
instantiate user objects directly; the bot itself and plugins will use your
protocol's new_user()
method to create new users. The third field typ
is
either Message.PRIVATE
or Message.GROUP
to denote whether this MyUser
is
in a chat room or not (defaults to Message.PRIVATE
). This is necessary since
some protocols like XMPP change how usernames appear in chat rooms. The last
field real
is a MyUser
object representing this user's "real" username (see
Real Usernames below for details).
Upon object instantiation, the parse()
method is called, which is where you
should set up the object. The __init__()
method should not be overwritten.
You are free to define any additional instance variables you need in the
parse()
method.
Upon instantiation of a new MyUser
the parse()
method should read the
object
passed in the user
argument and setup the object. Then the rest of
the methods in the User
class can be used to access information about the
user. Methods and brief explanations are given below. For signatures see
lib/protocol.py
. The first group are the methods that must be overriden. The
second group are already defined and available for use. The third group are
already defined but should not be called directly.
get_base() # return a str representing this MyUser without Resource/Device
get_name() # return a human-friendly name for this MyUser
parse(user) # called automatically on object init
__eq__(obj) # return True if this MyUser is equal to obj else False
__str__() # return this MyUser as a str
base_match(obj) # return True if obj is a MyUser and equal (ignoring resource)
get_protocol() # return the associated Protocol object
get_real() # return a MyUser respresenting this MyUser's "real" username
get_type() # return the type either Message.PRIVATE or Message.GROUP
set_real(real) # set this MyUser's "real" username
__getstate__() # controls the behavior of the pickle module
__hash__() # override so MyUser can be dict keys
__init__() # initialise some fields and call parse()
__ne__(obj) # override the != operator to return the oposite of ==
__repr__() # override object representation
Some protocols allow users to have custom nick names in chat rooms. Thus a
message received from a user in a chat room can look like it is from a
different person than if the same user had messaged us in private. However, it
can be helpful for the bot to know both pieces of information. The nick name in
order to reply correctly in the chat room, and the "real" user name for things
such as chat command black/white listing. The real
variable in a User
object should be set whenever applicable. If your protocol implements chat room
nick names then code accordingly. Otherwise, you can ignore the real
arg,
because by default user1.get_real()
simply returns a reference to user1
.
Next you must sub-class the Room
class, which is used to represent the name
and associated protocol of a chat room. It also allows specifying a nick name
and/or password when trying to join a room.
A new room object has the following signature. Note that the Room
class
itself is abstract, and so cannot be directly instantiated. Only sub-classes
can create an object, and so this guide will use MyRoom
as an example
sub-class.
room = MyRoom(proto,name,nick=None,pword=None)
The first field proto
is the associated Protocol object. Next, name
is the
identifier for the room, which can be any object. As with MyUser
, a MyRoom
object will only be directly created by you in the protocol class. The bot and
plugins will only create new rooms using your protocol's new_room()
method.
The next, nick
is the bot's nick name in the room as a str
. Then pword
is
the password used to join the room as a str
. The nick
and pword
parameters are only relevant when passing the MyRoom
object to join_room()
.
A bried description of each method is given below. For signatures see
lib/protocol.py
.
Upon instantiation of a new MyRoom
the parse()
method should read the
object
passed in the name
argument and setup the object. Then the rest of
the methods in the Room
class can be used to access information about the
room. Methods and brief explanations are given below. For signatures see
lib/protocol.py
. The first group are the methods that must be overriden. The
second group are already defined and available for use. The third group are
already defined, but should not be called directly.
get_name() # return the name of the room as a string
parse(name) # called automatically on object init
__eq__(obj) # return True if this MyRoom is equal to obj else False
get_nick() # return the nick to use when joining (or None)
get_occupants() # return a list of MyUser who are in this MyRoom
get_password() # return the password to use when joining (or None)
get_protocol() # return the associated Protocol object
get_real(nick) # return the real MyUser behind the specified nick name
__getstate__() # controls the behavior of the pickle module
__hash__() # override so MyRoom can be dict keys
__init__() # initialise some fields and call parse()
__ne__(obj) # override the != operator to return the oposite of ==
__repr__() # override object representation
__str__() # return this MyRoom as a str
This is where the real work is done in translating from your chosen protocol to something Sibyl can understand. In most cases this just consists of translating similar but slightly different API calls from an existing chat library into Sibyl's protocols API.
Although no one should ever need to manually create a Protocol
object (the
bot does this during start up automatically) it is important to understand how
this happens. Only sub-classes can actually be instantiated, and so this guide
will use MyProtocol
as an example sub-class. As with the User
class, you
should not override the __init__()
method in order to ensure certain things
are available. Upon creation, every Protocol
sub-class will have the
following instance variables:
bot # the SibylBot object that created this Protocol
log # the logging.Logger object for use by this Protocol
Instead of overriding __init__()
you should override the setup()
method for
any necessary actions on MyProtocol
instantiation. Note that the above
instance variables already exist and can be used in the setup()
method. For
completeness the signature for Protocol
objects is given below.
proto = MyProtocol(bot,log)
Where bot
is the SibylBot
instance and log
is a logging.Logger
.
When Sibyl first starts it reads the protocols
config option and then creates
new Protocol
objects of the selected types. After other setup, Sibyl calls
the connect()
method and checks for success. It then joins any rooms listed
in the rooms
config option using join_room()
. Finally, Sibyl sits in a
while loop constantly calling the process()
methods of every protocol. When
the protocol receives messages it processes them, converting relevant info into
Message
, User
, and Room
objects, and finally sending them to sibyl for
processing with bot._cb_message()
.
Sibyl is built expecting the protocol to queue messages asynchronously in the
background, which are then retrieved synchronously via process()
. If your
library is not asynchronous by nature, you will have to use the threading
module or similar to achieve this functionality. For details, see the
Threading Tutorial and the mail protocol example.
Protocol setup should be done in the setup()
method. If you need to access
any config options, you can use the opt()
method, for example
self.opt('xmpp.resource')
. Again, see the lib/protocol.py
file for method
signatures. The first group are the methods that must be overriden. The second
group are already defined and available for use. The third group are already
defined but should not be called directly.
broadcast(mess) # send Message, highlighting every user in the MyRoom
connect() # connect to the server or raise exceptions
get_nick(room) # return our nick name in the given MyRoom
get_occupants(room) # get the users in a room as a list of MyUser
get_real(room,nick) # return the real MyUser for the given nick
get_user() # return our full userid as a MyUser
is_connected() # return True if connected to the server else False
join_room(room) # join the specified MyRoom
new_room(name,nick,pword) # return a new MyRoom instance
new_user(user,typ,real) # return a new MyUser instance
part_room(room) # leave the specified MyRoom
process() # process any queued messages and do callbacks
send(mess) # send a Message to a MyUser or MyRoom
setup() # setup the Protocol (bot and log already defined)
shutdown() # called by the bot before exiting
_get_rooms(flag) # return list of MyRoom matching the given flag
get_name() # return the name of this protocol as a string
get_rooms(flags) # return a list of MyRoom matching the flags
in_room(room) # return True if we are in the room else False
opt(opt) # return the value of the specified config option
__add_rooms(rooms,flags) # helper method for get_rooms()
__eq__(obj) # return True if this MyProtocol is equal to obj else False
__init__() # initialise some fields and call setup()
__ne__(obj) # override the != operator to return the oposite of ==
There are 4 exceptions defined in lib/protocol.py
that should be raised at
various points in the Protocol
sub-class. So that exceptions keep track of
which protocol raised them, include the following in your file (if you're using
skeleton.py
these lines are already present):
from sibyl.lib.protocol import ProtocolError as SuperProtocolError
from sibyl.lib.protocol import PingTimeout as SuperPingTimeout
from sibyl.lib.protocol import ConnectFailure as SuperConnectFailure
from sibyl.lib.protocol import AuthFailure as SuperAuthFailure
from sibyl.lib.protocol import ServerShutdown as SuperServerShutdown
class ProtocolError(SuperProtocolError):
def __init__(self):
self.protocol = __name__.split('_')[-1]
class PingTimeout(SuperPingTimeout,ProtocolError):
pass
class ConnectFailure(SuperConnectFailure,ProtocolError):
pass
class AuthFailure(SuperAuthFailure,ProtocolError):
pass
class ServerShutdown(SuperServerShutdown,ProtocolError):
pass
Your protocol should never raise
an exception other than the ones below. You must catch any exceptions generated
by underlying libraries and re-raise them as one of the below. Note also that
all these exceptions inherit from ProtocolError
in sibyl.lib.protocol
and so
can all be caught with except ProtocolError:
.
PingTimeout # a ping attempt failed
ConnectFailure # failed to connect or server disconnected
AuthFailure # login failed due to bad credentials (don't reconnect)
ServerShutdown # server shut down (more specific than ConnectFailure)
There are 3 callback functions in SibylBot
that need to be called by
protocols. They are listed below along with the function that should call them.
For more details see the functions in lib/protocol.py
and lib/sibylbot.py
.
bot._cb_message(mess) called by process()
bot._cb_join_room_success(room) called by join_room()
bot._cb_join_room_failure(room,error) called by join_room()
Since the bot
instance variable is always defined, you can call these easily
for example with:
def process(self):
msg = self.conn.next_msg()
if msg:
self.bot._cb_message(self.convert_msg(msg))
You can define custom config options using the @botconf
decorator as
described in this section of the decorators article. For example:
from lib.decorators import botconf
@botconf
def conf(bot):
return [{'name':'server','default':None,'req':True},
{'name':'port','default':42}]
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. Otherwise, consider changing the value
to a Password
object from lib.password
in a parse
function.
If you need to store something, for example an authentication token or roster,
you can use Sibyl's peristent storage. Note that the user can disable persistent
storage in the config file. This functions just like in plug-ins, by passing
persist=True
to the bot.add_var()
method. You may only use add_var()
in
your protocol's setup()
method. Note that these variables will be members of
the bot, not your protocol.
IMPORTANT: Persistent storage may not work correctly with Message
, User
, and Room
objects
def setup(self):
self.bot.add_var('myprotocol_token',persist=True)
def connect(self):
if not self.bot.myprotocol_token:
self.bot.myprotocol_token = self.login()
else:
self.login_with_token(self.bot.myprotocol_token)
Basic logging can be done using the predefined log
instance variable, which is
a logging.Logger
object created by the bot specifically for use by this
protocol. Logging a message is as easy as self.log.debug('text')
. The
available log levels are (use in place of debug
in the code snippet):
debug detailed debug message to help with troubleshooting
info similar to debug, but might be interesting in general
warning something went wrong, but you can probably ignore it
error something went wrong, and it's probably important
critical something very important happened, possibly killing the bot
In general, a protocol shouldn't need to use the critical
level. Unhandled
exceptions will be caught in SibylBot
and logged. Example uses are given
below:
debug received a presence update
info successfully joined a room
warning the server doesn't support encryption
error kicked from a room
critical banned from a room