improvements on MUC set role/affiliation
[jabberbot/examples.git] / jabberbot.py
blob25a112d76ea21c75e4234f00baf4038d92efe329
1 #!/usr/bin/python
2 # -*- coding: utf-8 -*-
4 # JabberBot: A simple jabber/xmpp bot framework
5 # Copyright (c) 2007-2012 Thomas Perl <thp.io/about>
6 # $Id$
8 # This program is free software; you can redistribute it and/or modify
9 # it under the terms of the GNU General Public License as published by
10 # the Free Software Foundation; either version 3 of the License, or
11 # (at your option) any later version.
13 # This program is distributed in the hope that it will be useful,
14 # but WITHOUT ANY WARRANTY; without even the implied warranty of
15 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 # GNU General Public License for more details.
18 # You should have received a copy of the GNU General Public License
19 # along with this program. If not, see <http://www.gnu.org/licenses/>.
22 """
23 A framework for writing Jabber/XMPP bots and services
25 The JabberBot framework allows you to easily write bots
26 that use the XMPP protocol. You can create commands by
27 decorating functions in your subclass or customize the
28 bot's operation completely. MUCs are also supported.
29 """
31 import os
32 import re
33 import sys
34 import thread
36 try:
37 import xmpp
38 except ImportError:
39 print >> sys.stderr, """
40 You need to install xmpppy from http://xmpppy.sf.net/.
41 On Debian-based systems, install the python-xmpp package.
42 """
43 sys.exit(-1)
45 import time
46 import inspect
47 import logging
48 import traceback
50 # Will be parsed by setup.py to determine package metadata
51 __author__ = 'Thomas Perl <m@thp.io>'
52 __version__ = '0.15'
53 __website__ = 'http://thp.io/2007/python-jabberbot/'
54 __license__ = 'GNU General Public License version 3 or later'
57 def botcmd(*args, **kwargs):
58 """Decorator for bot command functions"""
60 def decorate(func, hidden=False, name=None, thread=False):
61 setattr(func, '_jabberbot_command', True)
62 setattr(func, '_jabberbot_command_hidden', hidden)
63 setattr(func, '_jabberbot_command_name', name or func.__name__)
64 setattr(func, '_jabberbot_command_thread', thread) # Experimental!
65 return func
67 if len(args):
68 return decorate(args[0], **kwargs)
69 else:
70 return lambda func: decorate(func, **kwargs)
73 class JabberBot(object):
74 # Show types for presence
75 AVAILABLE, AWAY, CHAT = None, 'away', 'chat'
76 DND, XA, OFFLINE = 'dnd', 'xa', 'unavailable'
78 # UI-messages (overwrite to change content)
79 MSG_AUTHORIZE_ME = 'Hey there. You are not yet on my roster. ' \
80 'Authorize my request and I will do the same.'
81 MSG_NOT_AUTHORIZED = 'You did not authorize my subscription request. '\
82 'Access denied.'
83 MSG_UNKNOWN_COMMAND = 'Unknown command: "%(command)s". '\
84 'Type "%(helpcommand)s" for available commands.'
85 MSG_HELP_TAIL = 'Type %(helpcommand)s <command name> to get more info '\
86 'about that specific command.'
87 MSG_HELP_UNDEFINED_COMMAND = 'That command is not defined.'
88 MSG_ERROR_OCCURRED = 'Sorry for your inconvenience. '\
89 'An unexpected error occurred.'
91 PING_FREQUENCY = 0 # Set to the number of seconds, e.g. 60.
92 PING_TIMEOUT = 2 # Seconds to wait for a response.
94 def __init__(self, username, password, res=None, debug=False,
95 privatedomain=False, acceptownmsgs=False, handlers=None,
96 command_prefix=''):
97 """Initializes the jabber bot and sets up commands.
99 username and password should be clear ;)
101 If res provided, res will be ressourcename,
102 otherwise it defaults to classname of childclass
104 If debug is True log messages of xmpppy will be printed to console.
105 Logging of Jabberbot itself is NOT affected.
107 If privatedomain is provided, it should be either
108 True to only allow subscriptions from the same domain
109 as the bot or a string that describes the domain for
110 which subscriptions are accepted (e.g. 'jabber.org').
112 If acceptownmsgs it set to True, this bot will accept
113 messages from the same JID that the bot itself has. This
114 is useful when using JabberBot with a single Jabber account
115 and multiple instances that want to talk to each other.
117 If handlers are provided, default handlers won't be enabled.
118 Usage like: [('stanzatype1', function1), ('stanzatype2', function2)]
119 Signature of function should be callback_xx(self, conn, stanza),
120 where conn is the connection and stanza the current stanza in process.
121 First handler in list will be served first.
122 Don't forget to raise exception xmpp.NodeProcessed to stop
123 processing in other handlers (see callback_presence)
125 If command_prefix is set to a string different from '' (the empty
126 string), it will require the commands to be prefixed with this text,
127 e.g. command_prefix = '!' means: Type "!info" for the "info" command.
129 # TODO sort this initialisation thematically
130 self.__debug = debug
131 self.log = logging.getLogger(__name__)
132 self.__username = username
133 self.__password = password
134 self.jid = xmpp.JID(self.__username)
135 self.res = (res or self.__class__.__name__)
136 self.conn = None
137 self.__finished = False
138 self.__show = None
139 self.__status = None
140 self.__seen = {}
141 self.__threads = {}
142 self.__lastping = time.time()
143 self.__privatedomain = privatedomain
144 self.__acceptownmsgs = acceptownmsgs
145 self.__command_prefix = command_prefix
147 self.handlers = (handlers or [('message', self.callback_message),
148 ('presence', self.callback_presence)])
150 # Collect commands from source
151 self.commands = {}
152 for name, value in inspect.getmembers(self, inspect.ismethod):
153 if getattr(value, '_jabberbot_command', False):
154 name = getattr(value, '_jabberbot_command_name')
155 self.log.info('Registered command: %s' % name)
156 self.commands[self.__command_prefix + name] = value
158 self.roster = None
160 ################################
162 def _send_status(self):
163 """Send status to everyone"""
164 self.conn.send(xmpp.dispatcher.Presence(show=self.__show,
165 status=self.__status))
167 def __set_status(self, value):
168 """Set status message.
169 If value remains constant, no presence stanza will be send"""
170 if self.__status != value:
171 self.__status = value
172 self._send_status()
174 def __get_status(self):
175 """Get current status message"""
176 return self.__status
178 status_message = property(fget=__get_status, fset=__set_status)
180 def __set_show(self, value):
181 """Set show (status type like AWAY, DND etc.).
182 If value remains constant, no presence stanza will be send"""
183 if self.__show != value:
184 self.__show = value
185 self._send_status()
187 def __get_show(self):
188 """Get current show (status type like AWAY, DND etc.)."""
189 return self.__show
191 status_type = property(fget=__get_show, fset=__set_show)
193 ################################
195 def connect(self):
196 """Connects the bot to server or returns current connection,
197 send inital presence stanza
198 and registers handlers
200 if not self.conn:
201 # TODO improve debug
202 if self.__debug:
203 conn = xmpp.Client(self.jid.getDomain())
204 else:
205 conn = xmpp.Client(self.jid.getDomain(), debug=[])
207 #connection attempt
208 conres = conn.connect()
209 if not conres:
210 self.log.error('unable to connect to server %s.' %
211 self.jid.getDomain())
212 return None
213 if conres != 'tls':
214 self.log.warning('unable to establish secure connection '\
215 '- TLS failed!')
217 authres = conn.auth(self.jid.getNode(), self.__password, self.res)
218 if not authres:
219 self.log.error('unable to authorize with server.')
220 return None
221 if authres != 'sasl':
222 self.log.warning("unable to perform SASL auth on %s. "\
223 "Old authentication method used!" % self.jid.getDomain())
225 # Connection established - save connection
226 self.conn = conn
228 # Send initial presence stanza (say hello to everyone)
229 self.conn.sendInitPresence()
230 # Save roster and log Items
231 self.roster = self.conn.Roster.getRoster()
232 self.log.info('*** roster ***')
233 for contact in self.roster.getItems():
234 self.log.info(' %s' % contact)
235 self.log.info('*** roster ***')
237 # Register given handlers (TODO move to own function)
238 for (handler, callback) in self.handlers:
239 self.conn.RegisterHandler(handler, callback)
240 self.log.debug('Registered handler: %s' % handler)
242 return self.conn
244 ### XEP-0045 Multi User Chat # prefix: muc # START ###
246 def muc_join_room(self, room, username=None, password=None, prefix=""):
247 """Join the specified multi-user chat room or changes nickname
249 If username is NOT provided fallback to node part of JID"""
250 # TODO fix namespacestrings and history settings
251 NS_MUC = 'http://jabber.org/protocol/muc'
252 if username is None:
253 # TODO use xmpppy function getNode
254 username = self.__username.split('@')[0]
255 my_room_JID = '/'.join((room, username))
256 pres = xmpp.Presence(to=my_room_JID)
257 if password is not None:
258 pres.setTag('x', namespace=NS_MUC).setTagData('password', password)
259 self.connect().send(pres)
261 def muc_part_room(self, room, username=None, message=None):
262 """Parts the specified multi-user chat"""
263 if username is None:
264 # TODO use xmpppy function getNode
265 username = self.__username.split('@')[0]
266 my_room_JID = '/'.join((room, username))
267 pres = xmpp.Presence(to=my_room_JID)
268 pres.setAttr('type', 'unavailable')
269 if message is not None:
270 pres.setTagData('status', message)
271 self.connect().send(pres)
273 def muc_set_role(self, room, nick, role, reason=None):
274 """Set role to user from muc
275 reason works only if defined in protocol
276 Works only with sufficient rights."""
277 NS_MUCADMIN = 'http://jabber.org/protocol/muc#admin'
278 item = xmpp.simplexml.Node('item')
279 item.setAttr('jid', jid)
280 item.setAttr('role', role)
281 iq = xmpp.Iq(typ='set', queryNS=NS_MUCADMIN, xmlns=None, to=room,
282 payload=set([item]))
283 if reason is not None:
284 item.setTagData('reason', reason)
285 self.connect().send(iq)
287 def muc_kick(self, room, nick, reason=None):
288 """Kicks user from muc
289 Works only with sufficient rights."""
290 self.muc_set_role(room, nick, 'none', reason)
293 def muc_set_affiliation(self, room, jid, affiliation, reason=None):
294 """Set affiliation to user from muc
295 reason works only if defined in protocol
296 Works only with sufficient rights."""
297 NS_MUCADMIN = 'http://jabber.org/protocol/muc#admin'
298 item = xmpp.simplexml.Node('item')
299 item.setAttr('jid', jid)
300 item.setAttr('affiliation', affiliation)
301 iq = xmpp.Iq(typ='set', queryNS=NS_MUCADMIN, xmlns=None, to=room,
302 payload=set([item]))
303 if reason is not None:
304 item.setTagData('reason', reason)
305 self.connect().send(iq)
307 def muc_ban(self, room, jid, reason=None):
308 """Bans user from muc
309 Works only with sufficient rights."""
310 self.muc_set_affiliation(room, jid, 'outcast', reason)
312 def muc_unban(self, room, jid):
313 """Unbans user from muc
314 User will not regain old affiliation.
315 Works only with sufficient rights."""
316 self.muc_set_affiliation(room, jid, 'none')
318 def muc_set_subject(self, room, text):
319 """Changes subject of muc
320 Works only with sufficient rights."""
321 mess = xmpp.Message(to=room)
322 mess.setAttr('type', 'groupchat')
323 mess.setTagData('subject', text)
324 self.connect().send(mess)
326 def muc_get_subject(self, room):
327 """Get subject of muc"""
328 pass
330 def muc_room_participants(self, room):
331 """Get list of participants """
332 pass
334 def muc_get_role(self, room, nick=None):
335 """Get role of nick
336 If nick is None our own role will be returned"""
337 pass
339 def muc_invite(self, room, jid, reason=None):
340 """Invites user to muc.
341 Works only if user has permission to invite to muc"""
342 NS_MUCUSER = 'http://jabber.org/protocol/muc#user'
343 invite = xmpp.simplexml.Node('invite')
344 invite.setAttr('to', jid)
345 if reason is not None:
346 invite.setTagData('reason', reason)
347 mess = xmpp.Message(to=room)
348 mess.setTag('x', namespace=NS_MUCUSER).addChild(node=invite)
349 self.log.error(mess)
350 self.connect().send(mess)
352 ### XEP-0045 Multi User Chat # END ###
354 def quit(self):
355 """Stop serving messages and exit.
357 I find it is handy for development to run the
358 jabberbot in a 'while true' loop in the shell, so
359 whenever I make a code change to the bot, I send
360 the 'reload' command, which I have mapped to call
361 self.quit(), and my shell script relaunches the
362 new version.
364 self.__finished = True
366 def send_message(self, mess):
367 """Send an XMPP message"""
368 self.connect().send(mess)
370 def send_tune(self, song, debug=False):
371 """Set information about the currently played tune
373 Song is a dictionary with keys: file, title, artist, album, pos, track,
374 length, uri. For details see <http://xmpp.org/protocols/tune/>.
376 NS_TUNE = 'http://jabber.org/protocol/tune'
377 iq = xmpp.Iq(typ='set')
378 iq.setFrom(self.jid)
379 iq.pubsub = iq.addChild('pubsub', namespace=xmpp.NS_PUBSUB)
380 iq.pubsub.publish = iq.pubsub.addChild('publish',
381 attrs={'node': NS_TUNE})
382 iq.pubsub.publish.item = iq.pubsub.publish.addChild('item',
383 attrs={'id': 'current'})
384 tune = iq.pubsub.publish.item.addChild('tune')
385 tune.setNamespace(NS_TUNE)
387 title = None
388 if 'title' in song:
389 title = song['title']
390 elif 'file' in song:
391 title = os.path.splitext(os.path.basename(song['file']))[0]
392 if title is not None:
393 tune.addChild('title').addData(title)
394 if 'artist' in song:
395 tune.addChild('artist').addData(song['artist'])
396 if 'album' in song:
397 tune.addChild('source').addData(song['album'])
398 if 'pos' in song and song['pos'] > 0:
399 tune.addChild('track').addData(str(song['pos']))
400 if 'time' in song:
401 tune.addChild('length').addData(str(song['time']))
402 if 'uri' in song:
403 tune.addChild('uri').addData(song['uri'])
405 if debug:
406 self.log.info('Sending tune: %s' % iq.__str__().encode('utf8'))
407 self.conn.send(iq)
409 def send(self, user, text, in_reply_to=None, message_type='chat'):
410 """Sends a simple message to the specified user."""
411 mess = self.build_message(text)
412 mess.setTo(user)
414 if in_reply_to:
415 mess.setThread(in_reply_to.getThread())
416 mess.setType(in_reply_to.getType())
417 else:
418 mess.setThread(self.__threads.get(user, None))
419 mess.setType(message_type)
421 self.send_message(mess)
423 def send_simple_reply(self, mess, text, private=False):
424 """Send a simple response to a message"""
425 self.send_message(self.build_reply(mess, text, private))
427 def build_reply(self, mess, text=None, private=False):
428 """Build a message for responding to another message.
429 Message is NOT sent"""
430 response = self.build_message(text)
431 if private:
432 response.setTo(mess.getFrom())
433 response.setType('chat')
434 else:
435 response.setTo(mess.getFrom().getStripped())
436 response.setType(mess.getType())
437 response.setThread(mess.getThread())
438 return response
440 def build_message(self, text):
441 """Builds an xhtml message without attributes.
442 If input is not valid xhtml-im fallback to normal."""
443 message = None # init message variable
444 # Try to determine if text has xhtml-tags - TODO needs improvement
445 text_plain = re.sub(r'<[^>]+>', '', text)
446 if text_plain != text:
447 # Create body w stripped tags for reciptiens w/o xhtml-abilities
448 # FIXME unescape &quot; etc.
449 message = xmpp.protocol.Message(body=text_plain)
450 # Start creating a xhtml body
451 html = xmpp.Node('html', \
452 {'xmlns': 'http://jabber.org/protocol/xhtml-im'})
453 try:
454 html.addChild(node=xmpp.simplexml.XML2Node( \
455 "<body xmlns='http://www.w3.org/1999/xhtml'>" + \
456 text.encode('utf-8') + "</body>"))
457 message.addChild(node=html)
458 except Exception, e:
459 # Didn't work, incorrect markup or something.
460 self.log.debug('An error while building a xhtml message. '\
461 'Fallback to normal messagebody')
462 # Fallback - don't sanitize invalid input. User is responsible!
463 message = None
464 if message is None:
465 # Normal body
466 message = xmpp.protocol.Message(body=text)
467 return message
469 def get_sender_username(self, mess):
470 """Extract the sender's user name from a message"""
471 type = mess.getType()
472 jid = mess.getFrom()
473 if type == "groupchat":
474 username = jid.getResource()
475 elif type == "chat":
476 username = jid.getNode()
477 else:
478 username = ""
479 return username
481 def get_full_jids(self, jid):
482 """Returns all full jids, which belong to a bare jid
484 Example: A bare jid is bob@jabber.org, with two clients connected,
485 which
486 have the full jids bob@jabber.org/home and bob@jabber.org/work."""
487 for res in self.roster.getResources(jid):
488 full_jid = "%s/%s" % (jid, res)
489 yield full_jid
491 def status_type_changed(self, jid, new_status_type):
492 """Callback for tracking status types (dnd, away, offline, ...)"""
493 self.log.debug('user %s changed status to %s' % (jid, new_status_type))
495 def status_message_changed(self, jid, new_status_message):
496 """Callback for tracking status messages (the free-form status text)"""
497 self.log.debug('user %s updated text to %s' %
498 (jid, new_status_message))
500 def broadcast(self, message, only_available=False):
501 """Broadcast a message to all users 'seen' by this bot.
503 If the parameter 'only_available' is True, the broadcast
504 will not go to users whose status is not 'Available'."""
505 for jid, (show, status) in self.__seen.items():
506 if not only_available or show is self.AVAILABLE:
507 self.send(jid, message)
509 def callback_presence(self, conn, presence):
510 jid, type_, show, status = presence.getFrom(), \
511 presence.getType(), presence.getShow(), \
512 presence.getStatus()
514 if self.jid.bareMatch(jid):
515 # update internal status
516 if type_ != self.OFFLINE:
517 self.__status = status
518 self.__show = show
519 else:
520 self.__status = ""
521 self.__show = self.OFFLINE
522 if not self.__acceptownmsgs:
523 # Ignore our own presence messages
524 return
526 if type_ is None:
527 # Keep track of status message and type changes
528 old_show, old_status = self.__seen.get(jid, (self.OFFLINE, None))
529 if old_show != show:
530 self.status_type_changed(jid, show)
532 if old_status != status:
533 self.status_message_changed(jid, status)
535 self.__seen[jid] = (show, status)
536 elif type_ == self.OFFLINE and jid in self.__seen:
537 # Notify of user offline status change
538 del self.__seen[jid]
539 self.status_type_changed(jid, self.OFFLINE)
541 try:
542 subscription = self.roster.getSubscription(unicode(jid.__str__()))
543 except KeyError, e:
544 # User not on our roster
545 subscription = None
546 except AttributeError, e:
547 # Recieved presence update before roster built
548 return
550 if type_ == 'error':
551 self.log.error(presence.getError())
553 self.log.debug('Got presence: %s (type: %s, show: %s, status: %s, '\
554 'subscription: %s)' % (jid, type_, show, status, subscription))
556 # If subscription is private,
557 # disregard anything not from the private domain
558 if self.__privatedomain and type_ in ('subscribe', 'subscribed', \
559 'unsubscribe'):
560 if self.__privatedomain == True:
561 # Use the bot's domain
562 domain = self.jid.getDomain()
563 else:
564 # Use the specified domain
565 domain = self.__privatedomain
567 # Check if the sender is in the private domain
568 user_domain = jid.getDomain()
569 if domain != user_domain:
570 self.log.info('Ignoring subscribe request: %s does not '\
571 'match private domain (%s)' % (user_domain, domain))
572 return
574 if type_ == 'subscribe':
575 # Incoming presence subscription request
576 if subscription in ('to', 'both', 'from'):
577 self.roster.Authorize(jid)
578 self._send_status()
580 if subscription not in ('to', 'both'):
581 self.roster.Subscribe(jid)
583 if subscription in (None, 'none'):
584 self.send(jid, self.MSG_AUTHORIZE_ME)
585 elif type_ == 'subscribed':
586 # Authorize any pending requests for that JID
587 self.roster.Authorize(jid)
588 elif type_ == 'unsubscribed':
589 # Authorization was not granted
590 self.send(jid, self.MSG_NOT_AUTHORIZED)
591 self.roster.Unauthorize(jid)
593 def callback_message(self, conn, mess):
594 """Messages sent to the bot will arrive here.
595 Command handling + routing is done in this function."""
597 # Prepare to handle either private chats or group chats
598 type = mess.getType()
599 jid = mess.getFrom()
600 props = mess.getProperties()
601 text = mess.getBody()
602 username = self.get_sender_username(mess)
604 if type not in ("groupchat", "chat"):
605 self.log.debug("unhandled message type: %s" % type)
606 return
608 # Ignore messages from before we joined
609 if xmpp.NS_DELAY in props:
610 return
612 # Ignore messages from myself
613 if self.jid.bareMatch(jid):
614 return
616 self.log.debug("*** props = %s" % props)
617 self.log.debug("*** jid = %s" % jid)
618 self.log.debug("*** username = %s" % username)
619 self.log.debug("*** type = %s" % type)
620 self.log.debug("*** text = %s" % text)
622 # If a message format is not supported (eg. encrypted),
623 # txt will be None
624 if not text:
625 return
627 # Ignore messages from users not seen by this bot
628 if jid not in self.__seen:
629 self.log.info('Ignoring message from unseen guest: %s' % jid)
630 self.log.debug("I've seen: %s" %
631 ["%s" % x for x in self.__seen.keys()])
632 return
634 # Remember the last-talked-in message thread for replies
635 # FIXME i am not threadsafe
636 self.__threads[jid] = mess.getThread()
638 if ' ' in text:
639 command, args = text.split(' ', 1)
640 else:
641 command, args = text, ''
642 cmd = command.lower()
643 self.log.debug("*** cmd = %s" % cmd)
645 if cmd in self.commands:
646 def execute_and_send():
647 try:
648 reply = self.commands[cmd](mess, args)
649 except Exception, e:
650 self.log.exception('An error happened while processing '\
651 'a message ("%s") from %s: %s"' %
652 (text, jid, traceback.format_exc(e)))
653 reply = self.MSG_ERROR_OCCURRED
654 if reply:
655 self.send_simple_reply(mess, reply)
656 # Experimental!
657 # if command should be executed in a seperate thread do it
658 if self.commands[cmd]._jabberbot_command_thread:
659 thread.start_new_thread(execute_and_send, ())
660 else:
661 execute_and_send()
662 else:
663 # In private chat, it's okay for the bot to always respond.
664 # In group chat, the bot should silently ignore commands it
665 # doesn't understand or aren't handled by unknown_command().
666 if type == 'groupchat':
667 default_reply = None
668 else:
669 default_reply = self.MSG_UNKNOWN_COMMAND % {
670 'command': cmd,
671 'helpcommand': self.__command_prefix + 'help',
673 reply = self.unknown_command(mess, cmd, args)
674 if reply is None:
675 reply = default_reply
676 if reply:
677 self.send_simple_reply(mess, reply)
679 def unknown_command(self, mess, cmd, args):
680 """Default handler for unknown commands
682 Override this method in derived class if you
683 want to trap some unrecognized commands. If
684 'cmd' is handled, you must return some non-false
685 value, else some helpful text will be sent back
686 to the sender.
688 return None
690 def top_of_help_message(self):
691 """Returns a string that forms the top of the help message
693 Override this method in derived class if you
694 want to add additional help text at the
695 beginning of the help message.
697 return ""
699 def bottom_of_help_message(self):
700 """Returns a string that forms the bottom of the help message
702 Override this method in derived class if you
703 want to add additional help text at the end
704 of the help message.
706 return ""
708 @botcmd
709 def help(self, mess, args):
710 """ Returns a help string listing available options.
712 Automatically assigned to the "help" command."""
713 if not args:
714 if self.__doc__:
715 description = self.__doc__.strip()
716 else:
717 description = 'Available commands:'
719 usage = '\n'.join(sorted([
720 '%s: %s' % (name, (command.__doc__ or \
721 '(undocumented)').strip().split('\n', 1)[0])
722 for (name, command) in self.commands.iteritems() \
723 if name != (self.__command_prefix + 'help') \
724 and not command._jabberbot_command_hidden
726 usage = '\n\n' + '\n\n'.join(filter(None,
727 [usage, self.MSG_HELP_TAIL % {'helpcommand':
728 self.__command_prefix + 'help'}]))
729 else:
730 description = ''
731 if (args not in self.commands and
732 (self.__command_prefix + args) in self.commands):
733 # Automatically add prefix if it's missing
734 args = self.__command_prefix + args
735 if args in self.commands:
736 usage = (self.commands[args].__doc__ or \
737 'undocumented').strip()
738 else:
739 usage = self.MSG_HELP_UNDEFINED_COMMAND
741 top = self.top_of_help_message()
742 bottom = self.bottom_of_help_message()
743 return ''.join(filter(None, [top, description, usage, bottom]))
745 def idle_proc(self):
746 """This function will be called in the main loop."""
747 self._idle_ping()
749 def _idle_ping(self):
750 """Pings the server, calls on_ping_timeout() on no response.
752 To enable set self.PING_FREQUENCY to a value higher than zero.
754 if self.PING_FREQUENCY \
755 and time.time() - self.__lastping > self.PING_FREQUENCY:
756 self.__lastping = time.time()
757 #logging.debug('Pinging the server.')
758 ping = xmpp.Protocol('iq', typ='get', \
759 payload=[xmpp.Node('ping', attrs={'xmlns':'urn:xmpp:ping'})])
760 try:
761 res = self.conn.SendAndWaitForResponse(ping, self.PING_TIMEOUT)
762 #logging.debug('Got response: ' + str(res))
763 if res is None:
764 self.on_ping_timeout()
765 except IOError, e:
766 logging.error('Error pinging the server: %s, '\
767 'treating as ping timeout.' % e)
768 self.on_ping_timeout()
770 def on_ping_timeout(self):
771 logging.info('Terminating due to PING timeout.')
772 self.quit()
774 def shutdown(self):
775 """This function will be called when we're done serving
777 Override this method in derived class if you
778 want to do anything special at shutdown.
780 pass
782 def serve_forever(self, connect_callback=None, disconnect_callback=None):
783 """Connects to the server and handles messages."""
784 conn = self.connect()
785 if conn:
786 self.log.info('bot connected. serving forever.')
787 else:
788 self.log.warn('could not connect to server - aborting.')
789 return
791 if connect_callback:
792 connect_callback()
793 self.__lastping = time.time()
795 while not self.__finished:
796 try:
797 conn.Process(1)
798 self.idle_proc()
799 except KeyboardInterrupt:
800 self.log.info('bot stopped by user request. '\
801 'shutting down.')
802 break
804 self.shutdown()
806 if disconnect_callback:
807 disconnect_callback()
809 # vim: expandtab tabstop=4 shiftwidth=4 softtabstop=4