Register handlers before sendInitPresence()
[jabberbot.git] / jabberbot.py
blobd1c7090edd754ff0da8ef4eb10d4b46883f34b9f
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='', server=None, port=5222):
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 if server is not None:
133 self.__server = (server, port)
134 else:
135 self.__server = None
136 self.__username = username
137 self.__password = password
138 self.jid = xmpp.JID(self.__username)
139 self.res = (res or self.__class__.__name__)
140 self.conn = None
141 self.__finished = False
142 self.__show = None
143 self.__status = None
144 self.__seen = {}
145 self.__threads = {}
146 self.__lastping = time.time()
147 self.__privatedomain = privatedomain
148 self.__acceptownmsgs = acceptownmsgs
149 self.__command_prefix = command_prefix
151 self.handlers = (handlers or [('message', self.callback_message),
152 ('presence', self.callback_presence)])
154 # Collect commands from source
155 self.commands = {}
156 for name, value in inspect.getmembers(self, inspect.ismethod):
157 if getattr(value, '_jabberbot_command', False):
158 name = getattr(value, '_jabberbot_command_name')
159 self.log.info('Registered command: %s' % name)
160 self.commands[self.__command_prefix + name] = value
162 self.roster = None
164 ################################
166 def _send_status(self):
167 """Send status to everyone"""
168 self.conn.send(xmpp.dispatcher.Presence(show=self.__show,
169 status=self.__status))
171 def __set_status(self, value):
172 """Set status message.
173 If value remains constant, no presence stanza will be send"""
174 if self.__status != value:
175 self.__status = value
176 self._send_status()
178 def __get_status(self):
179 """Get current status message"""
180 return self.__status
182 status_message = property(fget=__get_status, fset=__set_status)
184 def __set_show(self, value):
185 """Set show (status type like AWAY, DND etc.).
186 If value remains constant, no presence stanza will be send"""
187 if self.__show != value:
188 self.__show = value
189 self._send_status()
191 def __get_show(self):
192 """Get current show (status type like AWAY, DND etc.)."""
193 return self.__show
195 status_type = property(fget=__get_show, fset=__set_show)
197 ################################
199 def connect(self):
200 """Connects the bot to server or returns current connection,
201 send inital presence stanza
202 and registers handlers
204 if not self.conn:
205 # TODO improve debug
206 if self.__debug:
207 conn = xmpp.Client(self.jid.getDomain())
208 else:
209 conn = xmpp.Client(self.jid.getDomain(), debug=[])
211 #connection attempt
212 if self.__server:
213 conres = conn.connect(self.__server)
214 else:
215 conres = conn.connect()
216 if not conres:
217 self.log.error('unable to connect to server %s.' %
218 self.jid.getDomain())
219 return None
220 if conres != 'tls':
221 self.log.warning('unable to establish secure connection '\
222 '- TLS failed!')
224 authres = conn.auth(self.jid.getNode(), self.__password, self.res)
225 if not authres:
226 self.log.error('unable to authorize with server.')
227 return None
228 if authres != 'sasl':
229 self.log.warning("unable to perform SASL auth on %s. "\
230 "Old authentication method used!" % self.jid.getDomain())
232 # Connection established - save connection
233 self.conn = conn
235 # Register given handlers (TODO move to own function)
236 for (handler, callback) in self.handlers:
237 self.conn.RegisterHandler(handler, callback)
238 self.log.debug('Registered handler: %s' % handler)
240 # Send initial presence stanza (say hello to everyone)
241 self.conn.sendInitPresence()
242 # Save roster and log Items
243 self.roster = self.conn.Roster.getRoster()
244 self.log.info('*** roster ***')
245 for contact in self.roster.getItems():
246 self.log.info(' %s' % contact)
247 self.log.info('*** roster ***')
249 return self.conn
251 ### XEP-0045 Multi User Chat # prefix: muc # START ###
253 def muc_join_room(self, room, username=None, password=None, prefix=""):
254 """Join the specified multi-user chat room or changes nickname
256 If username is NOT provided fallback to node part of JID"""
257 # TODO fix namespacestrings and history settings
258 NS_MUC = 'http://jabber.org/protocol/muc'
259 if username is None:
260 # TODO use xmpppy function getNode
261 username = self.__username.split('@')[0]
262 my_room_JID = '/'.join((room, username))
263 pres = xmpp.Presence(to=my_room_JID)
264 if password is not None:
265 pres.setTag('x', namespace=NS_MUC).setTagData('password', password)
266 self.connect().send(pres)
268 def muc_part_room(self, room, username=None, message=None):
269 """Parts the specified multi-user chat"""
270 if username is None:
271 # TODO use xmpppy function getNode
272 username = self.__username.split('@')[0]
273 my_room_JID = '/'.join((room, username))
274 pres = xmpp.Presence(to=my_room_JID)
275 pres.setAttr('type', 'unavailable')
276 if message is not None:
277 pres.setTagData('status', message)
278 self.connect().send(pres)
280 def muc_set_role(self, room, nick, role, reason=None):
281 """Set role to user from muc
282 reason works only if defined in protocol
283 Works only with sufficient rights."""
284 NS_MUCADMIN = 'http://jabber.org/protocol/muc#admin'
285 item = xmpp.simplexml.Node('item')
286 item.setAttr('jid', jid)
287 item.setAttr('role', role)
288 iq = xmpp.Iq(typ='set', queryNS=NS_MUCADMIN, xmlns=None, to=room,
289 payload=set([item]))
290 if reason is not None:
291 item.setTagData('reason', reason)
292 self.connect().send(iq)
294 def muc_kick(self, room, nick, reason=None):
295 """Kicks user from muc
296 Works only with sufficient rights."""
297 self.muc_set_role(room, nick, 'none', reason)
300 def muc_set_affiliation(self, room, jid, affiliation, reason=None):
301 """Set affiliation to user from muc
302 reason works only if defined in protocol
303 Works only with sufficient rights."""
304 NS_MUCADMIN = 'http://jabber.org/protocol/muc#admin'
305 item = xmpp.simplexml.Node('item')
306 item.setAttr('jid', jid)
307 item.setAttr('affiliation', affiliation)
308 iq = xmpp.Iq(typ='set', queryNS=NS_MUCADMIN, xmlns=None, to=room,
309 payload=set([item]))
310 if reason is not None:
311 item.setTagData('reason', reason)
312 self.connect().send(iq)
314 def muc_ban(self, room, jid, reason=None):
315 """Bans user from muc
316 Works only with sufficient rights."""
317 self.muc_set_affiliation(room, jid, 'outcast', reason)
319 def muc_unban(self, room, jid):
320 """Unbans user from muc
321 User will not regain old affiliation.
322 Works only with sufficient rights."""
323 self.muc_set_affiliation(room, jid, 'none')
325 def muc_set_subject(self, room, text):
326 """Changes subject of muc
327 Works only with sufficient rights."""
328 mess = xmpp.Message(to=room)
329 mess.setAttr('type', 'groupchat')
330 mess.setTagData('subject', text)
331 self.connect().send(mess)
333 def muc_get_subject(self, room):
334 """Get subject of muc"""
335 pass
337 def muc_room_participants(self, room):
338 """Get list of participants """
339 pass
341 def muc_get_role(self, room, nick=None):
342 """Get role of nick
343 If nick is None our own role will be returned"""
344 pass
346 def muc_invite(self, room, jid, reason=None):
347 """Invites user to muc.
348 Works only if user has permission to invite to muc"""
349 NS_MUCUSER = 'http://jabber.org/protocol/muc#user'
350 invite = xmpp.simplexml.Node('invite')
351 invite.setAttr('to', jid)
352 if reason is not None:
353 invite.setTagData('reason', reason)
354 mess = xmpp.Message(to=room)
355 mess.setTag('x', namespace=NS_MUCUSER).addChild(node=invite)
356 self.log.error(mess)
357 self.connect().send(mess)
359 ### XEP-0045 Multi User Chat # END ###
361 def quit(self):
362 """Stop serving messages and exit.
364 I find it is handy for development to run the
365 jabberbot in a 'while true' loop in the shell, so
366 whenever I make a code change to the bot, I send
367 the 'reload' command, which I have mapped to call
368 self.quit(), and my shell script relaunches the
369 new version.
371 self.__finished = True
373 def send_message(self, mess):
374 """Send an XMPP message"""
375 self.connect().send(mess)
377 def send_tune(self, song, debug=False):
378 """Set information about the currently played tune
380 Song is a dictionary with keys: file, title, artist, album, pos, track,
381 length, uri. For details see <http://xmpp.org/protocols/tune/>.
383 NS_TUNE = 'http://jabber.org/protocol/tune'
384 iq = xmpp.Iq(typ='set')
385 iq.setFrom(self.jid)
386 iq.pubsub = iq.addChild('pubsub', namespace=xmpp.NS_PUBSUB)
387 iq.pubsub.publish = iq.pubsub.addChild('publish',
388 attrs={'node': NS_TUNE})
389 iq.pubsub.publish.item = iq.pubsub.publish.addChild('item',
390 attrs={'id': 'current'})
391 tune = iq.pubsub.publish.item.addChild('tune')
392 tune.setNamespace(NS_TUNE)
394 title = None
395 if 'title' in song:
396 title = song['title']
397 elif 'file' in song:
398 title = os.path.splitext(os.path.basename(song['file']))[0]
399 if title is not None:
400 tune.addChild('title').addData(title)
401 if 'artist' in song:
402 tune.addChild('artist').addData(song['artist'])
403 if 'album' in song:
404 tune.addChild('source').addData(song['album'])
405 if 'pos' in song and song['pos'] > 0:
406 tune.addChild('track').addData(str(song['pos']))
407 if 'time' in song:
408 tune.addChild('length').addData(str(song['time']))
409 if 'uri' in song:
410 tune.addChild('uri').addData(song['uri'])
412 if debug:
413 self.log.info('Sending tune: %s' % iq.__str__().encode('utf8'))
414 self.conn.send(iq)
416 def send(self, user, text, in_reply_to=None, message_type='chat'):
417 """Sends a simple message to the specified user."""
418 mess = self.build_message(text)
419 mess.setTo(user)
421 if in_reply_to:
422 mess.setThread(in_reply_to.getThread())
423 mess.setType(in_reply_to.getType())
424 else:
425 mess.setThread(self.__threads.get(user, None))
426 mess.setType(message_type)
428 self.send_message(mess)
430 def send_simple_reply(self, mess, text, private=False):
431 """Send a simple response to a message"""
432 self.send_message(self.build_reply(mess, text, private))
434 def build_reply(self, mess, text=None, private=False):
435 """Build a message for responding to another message.
436 Message is NOT sent"""
437 response = self.build_message(text)
438 if private:
439 response.setTo(mess.getFrom())
440 response.setType('chat')
441 else:
442 response.setTo(mess.getFrom().getStripped())
443 response.setType(mess.getType())
444 response.setThread(mess.getThread())
445 return response
447 def build_message(self, text):
448 """Builds an xhtml message without attributes.
449 If input is not valid xhtml-im fallback to normal."""
450 message = None # init message variable
451 # Try to determine if text has xhtml-tags - TODO needs improvement
452 text_plain = re.sub(r'<[^>]+>', '', text)
453 if text_plain != text:
454 # Create body w stripped tags for reciptiens w/o xhtml-abilities
455 # FIXME unescape &quot; etc.
456 message = xmpp.protocol.Message(body=text_plain)
457 # Start creating a xhtml body
458 html = xmpp.Node('html', \
459 {'xmlns': 'http://jabber.org/protocol/xhtml-im'})
460 try:
461 html.addChild(node=xmpp.simplexml.XML2Node( \
462 "<body xmlns='http://www.w3.org/1999/xhtml'>" + \
463 text.encode('utf-8') + "</body>"))
464 message.addChild(node=html)
465 except Exception, e:
466 # Didn't work, incorrect markup or something.
467 self.log.debug('An error while building a xhtml message. '\
468 'Fallback to normal messagebody')
469 # Fallback - don't sanitize invalid input. User is responsible!
470 message = None
471 if message is None:
472 # Normal body
473 message = xmpp.protocol.Message(body=text)
474 return message
476 def get_sender_username(self, mess):
477 """Extract the sender's user name from a message"""
478 type = mess.getType()
479 jid = mess.getFrom()
480 if type == "groupchat":
481 username = jid.getResource()
482 elif type == "chat":
483 username = jid.getNode()
484 else:
485 username = ""
486 return username
488 def get_full_jids(self, jid):
489 """Returns all full jids, which belong to a bare jid
491 Example: A bare jid is bob@jabber.org, with two clients connected,
492 which
493 have the full jids bob@jabber.org/home and bob@jabber.org/work."""
494 for res in self.roster.getResources(jid):
495 full_jid = "%s/%s" % (jid, res)
496 yield full_jid
498 def status_type_changed(self, jid, new_status_type):
499 """Callback for tracking status types (dnd, away, offline, ...)"""
500 self.log.debug('user %s changed status to %s' % (jid, new_status_type))
502 def status_message_changed(self, jid, new_status_message):
503 """Callback for tracking status messages (the free-form status text)"""
504 self.log.debug('user %s updated text to %s' %
505 (jid, new_status_message))
507 def broadcast(self, message, only_available=False):
508 """Broadcast a message to all users 'seen' by this bot.
510 If the parameter 'only_available' is True, the broadcast
511 will not go to users whose status is not 'Available'."""
512 for jid, (show, status) in self.__seen.items():
513 if not only_available or show is self.AVAILABLE:
514 self.send(jid, message)
516 def callback_presence(self, conn, presence):
517 jid, type_, show, status = presence.getFrom(), \
518 presence.getType(), presence.getShow(), \
519 presence.getStatus()
521 if self.jid.bareMatch(jid):
522 # update internal status
523 if type_ != self.OFFLINE:
524 self.__status = status
525 self.__show = show
526 else:
527 self.__status = ""
528 self.__show = self.OFFLINE
529 if not self.__acceptownmsgs:
530 # Ignore our own presence messages
531 return
533 if type_ is None:
534 # Keep track of status message and type changes
535 old_show, old_status = self.__seen.get(jid, (self.OFFLINE, None))
536 if old_show != show:
537 self.status_type_changed(jid, show)
539 if old_status != status:
540 self.status_message_changed(jid, status)
542 self.__seen[jid] = (show, status)
543 elif type_ == self.OFFLINE and jid in self.__seen:
544 # Notify of user offline status change
545 del self.__seen[jid]
546 self.status_type_changed(jid, self.OFFLINE)
548 try:
549 subscription = self.roster.getSubscription(unicode(jid.__str__()))
550 except KeyError, e:
551 # User not on our roster
552 subscription = None
553 except AttributeError, e:
554 # Recieved presence update before roster built
555 return
557 if type_ == 'error':
558 self.log.error(presence.getError())
560 self.log.debug('Got presence: %s (type: %s, show: %s, status: %s, '\
561 'subscription: %s)' % (jid, type_, show, status, subscription))
563 # If subscription is private,
564 # disregard anything not from the private domain
565 if self.__privatedomain and type_ in ('subscribe', 'subscribed', \
566 'unsubscribe'):
567 if self.__privatedomain == True:
568 # Use the bot's domain
569 domain = self.jid.getDomain()
570 else:
571 # Use the specified domain
572 domain = self.__privatedomain
574 # Check if the sender is in the private domain
575 user_domain = jid.getDomain()
576 if domain != user_domain:
577 self.log.info('Ignoring subscribe request: %s does not '\
578 'match private domain (%s)' % (user_domain, domain))
579 return
581 if type_ == 'subscribe':
582 # Incoming presence subscription request
583 if subscription in ('to', 'both', 'from'):
584 self.roster.Authorize(jid)
585 self._send_status()
587 if subscription not in ('to', 'both'):
588 self.roster.Subscribe(jid)
590 if subscription in (None, 'none'):
591 self.send(jid, self.MSG_AUTHORIZE_ME)
592 elif type_ == 'subscribed':
593 # Authorize any pending requests for that JID
594 self.roster.Authorize(jid)
595 elif type_ == 'unsubscribed':
596 # Authorization was not granted
597 self.send(jid, self.MSG_NOT_AUTHORIZED)
598 self.roster.Unauthorize(jid)
600 def callback_message(self, conn, mess):
601 """Messages sent to the bot will arrive here.
602 Command handling + routing is done in this function."""
604 # Prepare to handle either private chats or group chats
605 type = mess.getType()
606 jid = mess.getFrom()
607 props = mess.getProperties()
608 text = mess.getBody()
609 username = self.get_sender_username(mess)
611 if type not in ("groupchat", "chat"):
612 self.log.debug("unhandled message type: %s" % type)
613 return
615 # Ignore messages from before we joined
616 if xmpp.NS_DELAY in props:
617 return
619 # Ignore messages from myself
620 if self.jid.bareMatch(jid):
621 return
623 self.log.debug("*** props = %s" % props)
624 self.log.debug("*** jid = %s" % jid)
625 self.log.debug("*** username = %s" % username)
626 self.log.debug("*** type = %s" % type)
627 self.log.debug("*** text = %s" % text)
629 # If a message format is not supported (eg. encrypted),
630 # txt will be None
631 if not text:
632 return
634 # Ignore messages from users not seen by this bot
635 if jid not in self.__seen:
636 self.log.info('Ignoring message from unseen guest: %s' % jid)
637 self.log.debug("I've seen: %s" %
638 ["%s" % x for x in self.__seen.keys()])
639 return
641 # Remember the last-talked-in message thread for replies
642 # FIXME i am not threadsafe
643 self.__threads[jid] = mess.getThread()
645 if ' ' in text:
646 command, args = text.split(' ', 1)
647 else:
648 command, args = text, ''
649 cmd = command.lower()
650 self.log.debug("*** cmd = %s" % cmd)
652 if cmd in self.commands:
653 def execute_and_send():
654 try:
655 reply = self.commands[cmd](mess, args)
656 except Exception, e:
657 self.log.exception('An error happened while processing '\
658 'a message ("%s") from %s: %s"' %
659 (text, jid, traceback.format_exc(e)))
660 reply = self.MSG_ERROR_OCCURRED
661 if reply:
662 self.send_simple_reply(mess, reply)
663 # Experimental!
664 # if command should be executed in a seperate thread do it
665 if self.commands[cmd]._jabberbot_command_thread:
666 thread.start_new_thread(execute_and_send, ())
667 else:
668 execute_and_send()
669 else:
670 # In private chat, it's okay for the bot to always respond.
671 # In group chat, the bot should silently ignore commands it
672 # doesn't understand or aren't handled by unknown_command().
673 if type == 'groupchat':
674 default_reply = None
675 else:
676 default_reply = self.MSG_UNKNOWN_COMMAND % {
677 'command': cmd,
678 'helpcommand': self.__command_prefix + 'help',
680 reply = self.unknown_command(mess, cmd, args)
681 if reply is None:
682 reply = default_reply
683 if reply:
684 self.send_simple_reply(mess, reply)
686 def unknown_command(self, mess, cmd, args):
687 """Default handler for unknown commands
689 Override this method in derived class if you
690 want to trap some unrecognized commands. If
691 'cmd' is handled, you must return some non-false
692 value, else some helpful text will be sent back
693 to the sender.
695 return None
697 def top_of_help_message(self):
698 """Returns a string that forms the top of the help message
700 Override this method in derived class if you
701 want to add additional help text at the
702 beginning of the help message.
704 return ""
706 def bottom_of_help_message(self):
707 """Returns a string that forms the bottom of the help message
709 Override this method in derived class if you
710 want to add additional help text at the end
711 of the help message.
713 return ""
715 @botcmd
716 def help(self, mess, args):
717 """ Returns a help string listing available options.
719 Automatically assigned to the "help" command."""
720 if not args:
721 if self.__doc__:
722 description = self.__doc__.strip()
723 else:
724 description = 'Available commands:'
726 usage = '\n'.join(sorted([
727 '%s: %s' % (name, (command.__doc__ or \
728 '(undocumented)').strip().split('\n', 1)[0])
729 for (name, command) in self.commands.iteritems() \
730 if name != (self.__command_prefix + 'help') \
731 and not command._jabberbot_command_hidden
733 usage = '\n\n' + '\n\n'.join(filter(None,
734 [usage, self.MSG_HELP_TAIL % {'helpcommand':
735 self.__command_prefix + 'help'}]))
736 else:
737 description = ''
738 if (args not in self.commands and
739 (self.__command_prefix + args) in self.commands):
740 # Automatically add prefix if it's missing
741 args = self.__command_prefix + args
742 if args in self.commands:
743 usage = (self.commands[args].__doc__ or \
744 'undocumented').strip()
745 else:
746 usage = self.MSG_HELP_UNDEFINED_COMMAND
748 top = self.top_of_help_message()
749 bottom = self.bottom_of_help_message()
750 return ''.join(filter(None, [top, description, usage, bottom]))
752 def idle_proc(self):
753 """This function will be called in the main loop."""
754 self._idle_ping()
756 def _idle_ping(self):
757 """Pings the server, calls on_ping_timeout() on no response.
759 To enable set self.PING_FREQUENCY to a value higher than zero.
761 if self.PING_FREQUENCY \
762 and time.time() - self.__lastping > self.PING_FREQUENCY:
763 self.__lastping = time.time()
764 #logging.debug('Pinging the server.')
765 ping = xmpp.Protocol('iq', typ='get', \
766 payload=[xmpp.Node('ping', attrs={'xmlns':'urn:xmpp:ping'})])
767 try:
768 res = self.conn.SendAndWaitForResponse(ping, self.PING_TIMEOUT)
769 #logging.debug('Got response: ' + str(res))
770 if res is None:
771 self.on_ping_timeout()
772 except IOError, e:
773 logging.error('Error pinging the server: %s, '\
774 'treating as ping timeout.' % e)
775 self.on_ping_timeout()
777 def on_ping_timeout(self):
778 logging.info('Terminating due to PING timeout.')
779 self.quit()
781 def shutdown(self):
782 """This function will be called when we're done serving
784 Override this method in derived class if you
785 want to do anything special at shutdown.
787 pass
789 def serve_forever(self, connect_callback=None, disconnect_callback=None):
790 """Connects to the server and handles messages."""
791 conn = self.connect()
792 if conn:
793 self.log.info('bot connected. serving forever.')
794 else:
795 self.log.warn('could not connect to server - aborting.')
796 return
798 if connect_callback:
799 connect_callback()
800 self.__lastping = time.time()
802 while not self.__finished:
803 try:
804 conn.Process(1)
805 self.idle_proc()
806 except KeyboardInterrupt:
807 self.log.info('bot stopped by user request. '\
808 'shutting down.')
809 break
811 self.shutdown()
813 if disconnect_callback:
814 disconnect_callback()
816 # vim: expandtab tabstop=4 shiftwidth=4 softtabstop=4