initial commit; add jabberbot.py from source
[jabberbot/examples.git] / jabberbot.py
blob699419eca7943bd7f37f4c8e104bd1e13907bac2
1 #!/usr/bin/python
3 # JabberBot: A simple jabber/xmpp bot framework
4 # Copyright (c) 2007-2009 Thomas Perl <thpinfo.com>
6 # This program is free software; you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation; either version 3 of the License, or
9 # (at your option) any later version.
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
16 # You should have received a copy of the GNU General Public License
17 # along with this program. If not, see <http://www.gnu.org/licenses/>.
19 # Homepage: http://thpinfo.com/2007/python-jabberbot/
23 import sys
25 try:
26 import xmpp
27 except ImportError:
28 print >>sys.stderr, 'You need to install xmpppy from http://xmpppy.sf.net/.'
29 sys.exit(-1)
30 import inspect
33 """A simple jabber/xmpp bot framework
35 This is a simple bot framework around the "xmpppy" framework.
36 Copyright (c) 2007-2009 Thomas Perl <thpinfo.com>
38 To use, subclass the "JabberBot" class and implement "bot_" methods
39 (or whatever you set the command_prefix to), like this:
41 class StupidEchoBot(JabberBot):
42 def bot_echo( self, mess, args):
43 "The command description goes here"
44 return 'You said: ' + args
46 def bot_subscribe( self, mess, args):
47 "HIDDEN (Authorize the presence subscription request)"
48 # The docstring for this command has "HIDDEN" in it, so
49 # the help output does not show this command.
50 f = mess.getFrom()
51 self.conn.Roster.Authorize( f)
52 return 'Authorized.'
54 def unknown_command( self, mess, cmd, args):
55 "This optional method, if present, gets called if the
56 command is not recognized."
57 if args.split()[0].startswith( 'cheese'):
58 return 'Sorry, cheesy commands not available.'
59 else:
60 # if we return None, the default 'unknown command' text will get printed.
61 return None
63 username = 'jid@server.example.com'
64 password = 'mypassword'
66 bot = StupidEchoBot( username, password)
67 bot.serve_forever()
69 """
71 __author__ = 'Thomas Perl <thp@thpinfo.com>'
72 __version__ = '0.6'
75 class JabberBot(object):
76 command_prefix = 'bot_'
78 def __init__( self, jid, password, res = None):
79 """Initializes the jabber bot and sets up commands."""
80 self.jid = xmpp.JID( jid)
81 self.password = password
82 self.res = (res or self.__class__.__name__)
83 self.conn = None
84 self.__finished = False
86 self.commands = { 'help': self.help_callback, }
87 for (name, value) in inspect.getmembers( self):
88 if inspect.ismethod( value) and name.startswith( self.command_prefix):
89 self.commands[name[len(self.command_prefix):]] = value
91 def log( self, s):
92 """Logging facility, can be overridden in subclasses to log to file, etc.."""
93 print '%s: %s' % ( self.__class__.__name__, s, )
95 def connect( self):
96 if not self.conn:
97 conn = xmpp.Client( self.jid.getDomain(), debug = [])
99 if not conn.connect():
100 self.log( 'unable to connect to server.')
101 return None
103 if not conn.auth( self.jid.getNode(), self.password, self.res):
104 self.log( 'unable to authorize with server.')
105 return None
107 conn.RegisterHandler( 'message', self.callback_message)
108 conn.sendInitPresence()
109 self.conn = conn
111 return self.conn
113 def quit( self):
114 """Stop serving messages and exit.
116 I find it is handy for development to run the
117 jabberbot in a 'while true' loop in the shell, so
118 whenever I make a code change to the bot, I send
119 the 'reload' command, which I have mapped to call
120 self.quit(), and my shell script relaunches the
121 new version.
123 self.__finished = True
125 def send( self, user, text, in_reply_to = None):
126 """Sends a simple message to the specified user."""
127 mess = xmpp.Message( user, text)
128 if in_reply_to:
129 mess.setThread( in_reply_to.getThread())
130 mess.setType( in_reply_to.getType())
132 self.connect().send( mess)
134 def callback_message( self, conn, mess):
135 """Messages sent to the bot will arrive here. Command handling + routing is done in this function."""
136 text = mess.getBody()
138 # If a message format is not supported (eg. encrypted), txt will be None
139 if not text:
140 return
142 if ' ' in text:
143 command, args = text.split(' ',1)
144 else:
145 command, args = text,''
147 cmd = command.lower()
149 if self.commands.has_key(cmd):
150 reply = self.commands[cmd]( mess, args)
151 else:
152 unk_str = 'Unknown command: "%s". Type "help" for available commands.' % cmd
153 reply = self.unknown_command( mess, cmd, args) or unk_str
154 if reply:
155 self.send( mess.getFrom(), reply, mess)
157 def unknown_command( self, mess, cmd, args):
158 """Default handler for unknown commands
160 Override this method in derived class if you
161 want to trap some unrecognized commands. If
162 'cmd' is handled, you must return some non-false
163 value, else some helpful text will be sent back
164 to the sender.
166 return None
168 def help_callback( self, mess, args):
169 """Returns a help string listing available options. Automatically assigned to the "help" command."""
170 usage = '\n'.join(sorted(['%s: %s' % (name, command.__doc__ or '(undocumented)') for (name, command) in self.commands.items() if name != 'help' and (not command.__doc__ or not command.__doc__.startswith('HIDDEN'))]))
172 if self.__doc__:
173 description = self.__doc__.strip()
174 else:
175 description = 'Available commands:'
177 return '%s\n\n%s' % ( description, usage, )
179 def idle_proc( self):
180 """This function will be called in the main loop."""
181 pass
183 def serve_forever( self, connect_callback = None, disconnect_callback = None):
184 """Connects to the server and handles messages."""
185 conn = self.connect()
186 if conn:
187 self.log('bot connected. serving forever.')
188 else:
189 self.log('could not connect to server - aborting.')
190 return
192 if connect_callback:
193 connect_callback()
195 while not self.__finished:
196 try:
197 conn.Process(1)
198 self.idle_proc()
199 except KeyboardInterrupt:
200 self.log('bot stopped by user request. shutting down.')
201 break
203 if disconnect_callback:
204 disconnect_callback()