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/
28 print >>sys
.stderr
, 'You need to install xmpppy from http://xmpppy.sf.net/.'
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>
39 __author__
= 'Thomas Perl <thp@thpinfo.com>'
43 def botcmd(*args
, **kwargs
):
44 """Decorator for bot command functions"""
46 def decorate(func
, hidden
=False):
47 setattr(func
, '_jabberbot_command', True)
48 setattr(func
, '_jabberbot_hidden', hidden
)
52 return decorate(args
[0], **kwargs
)
54 return lambda func
: decorate(func
, **kwargs
)
57 class JabberBot(object):
58 AVAILABLE
, AWAY
, CHAT
, DND
, XA
, OFFLINE
= None, 'away', 'chat', 'dnd', 'xa', 'unavailable'
60 MSG_AUTHORIZE_ME
= 'Hey there. You are not yet on my roster. Authorize my request and I will do the same.'
61 MSG_NOT_AUTHORIZED
= 'You did not authorize my subscription request. Access denied.'
63 def __init__( self
, jid
, password
, res
=None):
64 """Initializes the jabber bot and sets up commands."""
65 self
.jid
= xmpp
.JID(jid
)
66 self
.password
= password
67 self
.res
= (res
or self
.__class
__.__name
__)
69 self
.__finished
= False
76 for name
, value
in inspect
.getmembers(self
):
77 if inspect
.ismethod(value
) and getattr(value
, '_jabberbot_command', False):
78 self
.debug('Registered command: %s' % name
)
79 self
.commands
[name
] = value
81 ################################
83 def _send_status(self
):
84 self
.conn
.send(xmpp
.dispatcher
.Presence(show
=self
.__show
, status
=self
.__status
))
86 def __set_status(self
, value
):
87 if self
.__status
!= value
:
91 def __get_status(self
):
94 status_message
= property(fget
=__get_status
, fset
=__set_status
)
96 def __set_show(self
, value
):
97 if self
.__show
!= value
:
101 def __get_show(self
):
104 status_type
= property(fget
=__get_show
, fset
=__set_show
)
106 ################################
112 """Logging facility, can be overridden in subclasses to log to file, etc.."""
113 print self
.__class
__.__name
__, ':', s
117 conn
= xmpp
.Client(self
.jid
.getDomain())
119 if not conn
.connect():
120 self
.log( 'unable to connect to server.')
123 if not conn
.auth( self
.jid
.getNode(), self
.password
, self
.res
):
124 self
.log( 'unable to authorize with server.')
127 conn
.RegisterHandler('message', self
.callback_message
)
128 conn
.RegisterHandler('presence', self
.callback_presence
)
129 conn
.sendInitPresence()
131 self
.roster
= self
.conn
.Roster
.getRoster()
132 self
.log('*** roster ***')
133 for contact
in self
.roster
.getItems():
134 self
.log(' ' + str(contact
))
135 self
.log('*** roster ***')
140 """Stop serving messages and exit.
142 I find it is handy for development to run the
143 jabberbot in a 'while true' loop in the shell, so
144 whenever I make a code change to the bot, I send
145 the 'reload' command, which I have mapped to call
146 self.quit(), and my shell script relaunches the
149 self
.__finished
= True
151 def send(self
, user
, text
, in_reply_to
=None, message_type
='chat'):
152 """Sends a simple message to the specified user."""
153 mess
= xmpp
.Message(user
, text
)
156 mess
.setThread(in_reply_to
.getThread())
157 mess
.setType(in_reply_to
.getType())
159 mess
.setThread(self
.__threads
.get(user
, None))
160 mess
.setType(message_type
)
162 self
.connect().send(mess
)
164 def status_type_changed(self
, jid
, new_status_type
):
165 """Callback for tracking status types (available, away, offline, ...)"""
166 self
.debug('user %s changed status to %s' % (jid
, new_status_type
))
168 def status_message_changed(self
, jid
, new_status_message
):
169 """Callback for tracking status messages (the free-form status text)"""
170 self
.debug('user %s updated text to %s' % (jid
, new_status_message
))
172 def broadcast(self
, message
, only_available
=False):
173 """Broadcast a message to all users 'seen' by this bot.
175 If the parameter 'only_available' is True, the broadcast
176 will not go to users whose status is not 'Available'."""
177 for jid
, (show
, status
) in self
.__seen
.items():
178 if not only_available
or show
is self
.AVAILABLE
:
179 self
.send(jid
, message
)
181 def callback_presence(self
, conn
, presence
):
182 jid
, type_
, show
, status
= presence
.getFrom(), \
183 presence
.getType(), presence
.getShow(), \
186 if self
.jid
.bareMatch(jid
):
187 # Ignore our own presence messages
191 # Keep track of status message and type changes
192 old_show
, old_status
= self
.__seen
.get(jid
, (self
.OFFLINE
, None))
194 self
.status_type_changed(jid
, show
)
196 if old_status
!= status
:
197 self
.status_message_changed(jid
, status
)
199 self
.__seen
[jid
] = (show
, status
)
200 elif type_
== self
.OFFLINE
and jid
in self
.__seen
:
201 # Notify of user offline status change
203 self
.status_type_changed(jid
, self
.OFFLINE
)
206 subscription
= self
.roster
.getSubscription(str(jid
))
208 # User not on our roster
212 self
.log(presence
.getError())
214 self
.debug('Got presence: %s (type: %s, show: %s, status: %s, subscription: %s)' % (jid
, type_
, show
, status
, subscription
))
216 if type_
== 'subscribe':
217 # Incoming presence subscription request
218 if subscription
in ('to', 'both', 'from'):
219 self
.roster
.Authorize(jid
)
222 if subscription
not in ('to', 'both'):
223 self
.roster
.Subscribe(jid
)
225 if subscription
in (None, 'none'):
226 self
.send(jid
, self
.MSG_AUTHORIZE_ME
)
227 elif type_
== 'subscribed':
228 # Authorize any pending requests for that JID
229 self
.roster
.Authorize(jid
)
230 elif type_
== 'unsubscribed':
231 # Authorization was not granted
232 self
.send(jid
, self
.MSG_NOT_AUTHORIZED
)
233 self
.roster
.Unauthorize(jid
)
235 def callback_message( self
, conn
, mess
):
236 """Messages sent to the bot will arrive here. Command handling + routing is done in this function."""
237 jid
, text
= mess
.getFrom(), mess
.getBody()
239 # If a message format is not supported (eg. encrypted), txt will be None
243 # Ignore messages from users not seen by this bot
244 if jid
not in self
.__seen
:
245 self
.log('Ignoring message from unseen guest: %s' % jid
)
248 # Remember the last-talked-in thread for replies
249 self
.__threads
[jid
] = mess
.getThread()
252 command
, args
= text
.split(' ', 1)
254 command
, args
= text
, ''
256 cmd
= command
.lower()
258 if self
.commands
.has_key(cmd
):
260 reply
= self
.commands
[cmd
](mess
, args
)
262 reply
= traceback
.format_exc(e
)
263 self
.log('An error happened while processing a message ("%s") from %s: %s"' % (text
, jid
, reply
))
266 unk_str
= 'Unknown command: "%s". Type "help" for available commands.<b>blubb!</b>' % cmd
267 reply
= self
.unknown_command( mess
, cmd
, args
) or unk_str
269 self
.send(jid
, reply
, mess
)
271 def unknown_command(self
, mess
, cmd
, args
):
272 """Default handler for unknown commands
274 Override this method in derived class if you
275 want to trap some unrecognized commands. If
276 'cmd' is handled, you must return some non-false
277 value, else some helpful text will be sent back
283 def help( self
, mess
, args
):
284 """Returns a help string listing available options. Automatically assigned to the "help" command."""
285 usage
= '\n'.join(sorted(['%s: %s' % (name
, command
.__doc
__ or '(undocumented)') for (name
, command
) in self
.commands
.items() if name
!= 'help' and not command
._jabberbot
_hidden
]))
288 description
= self
.__doc
__.strip()
290 description
= 'Available commands:'
292 return '%s\n\n%s' % ( description
, usage
, )
294 def idle_proc( self
):
295 """This function will be called in the main loop."""
298 def serve_forever( self
, connect_callback
= None, disconnect_callback
= None):
299 """Connects to the server and handles messages."""
300 conn
= self
.connect()
302 self
.log('bot connected. serving forever.')
304 self
.log('could not connect to server - aborting.')
310 while not self
.__finished
:
314 except KeyboardInterrupt:
315 self
.log('bot stopped by user request. shutting down.')
318 if disconnect_callback
:
319 disconnect_callback()