[Python-modules-commits] r19610 - in packages/jabberbot/branches/squeeze (2 files)
jcristau at users.alioth.debian.org
jcristau at users.alioth.debian.org
Mon Dec 12 20:55:39 UTC 2011
Date: Monday, December 12, 2011 @ 20:55:36
Author: jcristau
Revision: 19610
Bind callbacks after the roster has been initialised
Cherry-picked from upstream, closes: 651621
Added:
packages/jabberbot/branches/squeeze/jabberbot.py
Modified:
packages/jabberbot/branches/squeeze/debian/changelog
Modified: packages/jabberbot/branches/squeeze/debian/changelog
===================================================================
--- packages/jabberbot/branches/squeeze/debian/changelog 2011-12-12 20:31:08 UTC (rev 19609)
+++ packages/jabberbot/branches/squeeze/debian/changelog 2011-12-12 20:55:36 UTC (rev 19610)
@@ -1,3 +1,14 @@
+jabberbot (0.9-1+squeeze1) squeeze; urgency=low
+
+ * Team upload.
+ * Cherry-pick a change from upstream (included in 0.11):
+ "Bind callbacks after the roster has been initialised. It is possible on
+ busy servers that the callback can receive roster events before the roster
+ is initalised, this moves the binding to later in the process."
+ Closes: #651621
+
+ -- Julien Cristau <jcristau at debian.org> Mon, 12 Dec 2011 21:39:28 +0100
+
jabberbot (0.9-1) unstable; urgency=low
* New upstream release
Added: packages/jabberbot/branches/squeeze/jabberbot.py
===================================================================
--- packages/jabberbot/branches/squeeze/jabberbot.py (rev 0)
+++ packages/jabberbot/branches/squeeze/jabberbot.py 2011-12-12 20:55:36 UTC (rev 19610)
@@ -0,0 +1,434 @@
+#!/usr/bin/python
+
+# JabberBot: A simple jabber/xmpp bot framework
+# Copyright (c) 2007-2009 Thomas Perl <thpinfo.com>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+
+
+import sys
+
+try:
+ import xmpp
+except ImportError:
+ print >>sys.stderr, 'You need to install xmpppy from http://xmpppy.sf.net/.'
+ sys.exit(-1)
+import inspect
+import traceback
+
+"""A simple jabber/xmpp bot framework"""
+
+__author__ = 'Thomas Perl <thp at thpinfo.com>'
+__version__ = '0.9'
+__website__ = 'http://thpinfo.com/2007/python-jabberbot/'
+__license__ = 'GPLv3 or later'
+
+def botcmd(*args, **kwargs):
+ """Decorator for bot command functions"""
+
+ def decorate(func, hidden=False):
+ setattr(func, '_jabberbot_command', True)
+ setattr(func, '_jabberbot_hidden', hidden)
+ return func
+
+ if len(args):
+ return decorate(args[0], **kwargs)
+ else:
+ return lambda func: decorate(func, **kwargs)
+
+
+class JabberBot(object):
+ AVAILABLE, AWAY, CHAT, DND, XA, OFFLINE = None, 'away', 'chat', 'dnd', 'xa', 'unavailable'
+
+ MSG_AUTHORIZE_ME = 'Hey there. You are not yet on my roster. Authorize my request and I will do the same.'
+ MSG_NOT_AUTHORIZED = 'You did not authorize my subscription request. Access denied.'
+
+ def __init__(self, username, password, res=None, debug=False):
+ """Initializes the jabber bot and sets up commands."""
+ self.__debug = debug
+ self.__username = username
+ self.__password = password
+ self.jid = xmpp.JID(self.__username)
+ self.res = (res or self.__class__.__name__)
+ self.conn = None
+ self.__finished = False
+ self.__show = None
+ self.__status = None
+ self.__seen = {}
+ self.__threads = {}
+
+ self.commands = {}
+ for name, value in inspect.getmembers(self):
+ if inspect.ismethod(value) and getattr(value, '_jabberbot_command', False):
+ self.debug('Registered command: %s' % name)
+ self.commands[name] = value
+
+################################
+
+ def _send_status(self):
+ self.conn.send(xmpp.dispatcher.Presence(show=self.__show, status=self.__status))
+
+ def __set_status(self, value):
+ if self.__status != value:
+ self.__status = value
+ self._send_status()
+
+ def __get_status(self):
+ return self.__status
+
+ status_message = property(fget=__get_status, fset=__set_status)
+
+ def __set_show(self, value):
+ if self.__show != value:
+ self.__show = value
+ self._send_status()
+
+ def __get_show(self):
+ return self.__show
+
+ status_type = property(fget=__get_show, fset=__set_show)
+
+################################
+
+ def debug(self, s):
+ if self.__debug: self.log(s)
+
+ def log( self, s):
+ """Logging facility, can be overridden in subclasses to log to file, etc.."""
+ print self.__class__.__name__, ':', s
+
+ def connect( self):
+ if not self.conn:
+ if self.__debug:
+ conn = xmpp.Client(self.jid.getDomain())
+ else:
+ conn = xmpp.Client(self.jid.getDomain(), debug = [])
+
+ conres = conn.connect()
+ if not conres:
+ self.log( 'unable to connect to server %s.' % self.jid.getDomain())
+ return None
+ if conres<>'tls':
+ self.log("Warning: unable to establish secure connection - TLS failed!")
+
+ authres = conn.auth(self.jid.getNode(), self.__password, self.res)
+ if not authres:
+ self.log('unable to authorize with server.')
+ return None
+ if authres<>'sasl':
+ self.log("Warning: unable to perform SASL auth os %s. Old authentication method used!" % self.jid.getDomain())
+
+ conn.sendInitPresence()
+ self.conn = conn
+ self.roster = self.conn.Roster.getRoster()
+ self.log('*** roster ***')
+ for contact in self.roster.getItems():
+ self.log(' ' + str(contact))
+ self.log('*** roster ***')
+ self.conn.RegisterHandler('message', self.callback_message)
+ self.conn.RegisterHandler('presence', self.callback_presence)
+
+ return self.conn
+
+ def join_room(self, room):
+ """Join the specified multi-user chat room"""
+ my_room_JID = "%s/%s" % (room,self.__username)
+ self.connect().send(xmpp.Presence(to=my_room_JID))
+
+ def quit( self):
+ """Stop serving messages and exit.
+
+ I find it is handy for development to run the
+ jabberbot in a 'while true' loop in the shell, so
+ whenever I make a code change to the bot, I send
+ the 'reload' command, which I have mapped to call
+ self.quit(), and my shell script relaunches the
+ new version.
+ """
+ self.__finished = True
+
+ def send_message(self, mess):
+ """Send an XMPP message"""
+ self.connect().send(mess)
+
+ def send(self, user, text, in_reply_to=None, message_type='chat'):
+ """Sends a simple message to the specified user."""
+ mess = xmpp.Message(user, text)
+
+ if in_reply_to:
+ mess.setThread(in_reply_to.getThread())
+ mess.setType(in_reply_to.getType())
+ else:
+ mess.setThread(self.__threads.get(user, None))
+ mess.setType(message_type)
+
+ self.send_message(mess)
+
+ def send_simple_reply(self, mess, text, private=False):
+ """Send a simple response to a message"""
+ self.send_message( self.build_reply(mess,text, private) )
+
+ def build_reply(self, mess, text=None, private=False):
+ """Build a message for responding to another message. Message is NOT sent"""
+ if private:
+ to_user = mess.getFrom()
+ type = "chat"
+ else:
+ to_user = mess.getFrom().getStripped()
+ type = mess.getType()
+ response = xmpp.Message(to_user, text)
+ response.setThread(mess.getThread())
+ response.setType(type)
+ return response
+
+ def get_sender_username(self, mess):
+ """Extract the sender's user name from a message"""
+ type = mess.getType()
+ jid = mess.getFrom()
+ if type == "groupchat":
+ username = jid.getResource()
+ elif type == "chat":
+ username = jid.getNode()
+ else:
+ username = ""
+ return username
+
+ def status_type_changed(self, jid, new_status_type):
+ """Callback for tracking status types (available, away, offline, ...)"""
+ self.debug('user %s changed status to %s' % (jid, new_status_type))
+
+ def status_message_changed(self, jid, new_status_message):
+ """Callback for tracking status messages (the free-form status text)"""
+ self.debug('user %s updated text to %s' % (jid, new_status_message))
+
+ def broadcast(self, message, only_available=False):
+ """Broadcast a message to all users 'seen' by this bot.
+
+ If the parameter 'only_available' is True, the broadcast
+ will not go to users whose status is not 'Available'."""
+ for jid, (show, status) in self.__seen.items():
+ if not only_available or show is self.AVAILABLE:
+ self.send(jid, message)
+
+ def callback_presence(self, conn, presence):
+ jid, type_, show, status = presence.getFrom(), \
+ presence.getType(), presence.getShow(), \
+ presence.getStatus()
+
+ if self.jid.bareMatch(jid):
+ # Ignore our own presence messages
+ return
+
+ if type_ is None:
+ # Keep track of status message and type changes
+ old_show, old_status = self.__seen.get(jid, (self.OFFLINE, None))
+ if old_show != show:
+ self.status_type_changed(jid, show)
+
+ if old_status != status:
+ self.status_message_changed(jid, status)
+
+ self.__seen[jid] = (show, status)
+ elif type_ == self.OFFLINE and jid in self.__seen:
+ # Notify of user offline status change
+ del self.__seen[jid]
+ self.status_type_changed(jid, self.OFFLINE)
+
+ try:
+ subscription = self.roster.getSubscription(str(jid))
+ except KeyError, ke:
+ # User not on our roster
+ subscription = None
+
+ if type_ == 'error':
+ self.log(presence.getError())
+
+ self.debug('Got presence: %s (type: %s, show: %s, status: %s, subscription: %s)' % (jid, type_, show, status, subscription))
+
+ if type_ == 'subscribe':
+ # Incoming presence subscription request
+ if subscription in ('to', 'both', 'from'):
+ self.roster.Authorize(jid)
+ self._send_status()
+
+ if subscription not in ('to', 'both'):
+ self.roster.Subscribe(jid)
+
+ if subscription in (None, 'none'):
+ self.send(jid, self.MSG_AUTHORIZE_ME)
+ elif type_ == 'subscribed':
+ # Authorize any pending requests for that JID
+ self.roster.Authorize(jid)
+ elif type_ == 'unsubscribed':
+ # Authorization was not granted
+ self.send(jid, self.MSG_NOT_AUTHORIZED)
+ self.roster.Unauthorize(jid)
+
+ def callback_message( self, conn, mess):
+ """Messages sent to the bot will arrive here. Command handling + routing is done in this function."""
+
+ # Prepare to handle either private chats or group chats
+ type = mess.getType()
+ jid = mess.getFrom()
+ props = mess.getProperties()
+ text = mess.getBody()
+ username = self.get_sender_username(mess)
+
+ if type not in ("groupchat", "chat"):
+ self.debug("unhandled message type: %s" % type)
+ return
+
+ self.debug("*** props = %s" % props)
+ self.debug("*** jid = %s" % jid)
+ self.debug("*** username = %s" % username)
+ self.debug("*** type = %s" % type)
+ self.debug("*** text = %s" % text)
+
+ # Ignore messages from before we joined
+ if xmpp.NS_DELAY in props: return
+
+ # Ignore messages from myself
+ if username == self.__username: return
+
+ # If a message format is not supported (eg. encrypted), txt will be None
+ if not text: return
+
+ # Ignore messages from users not seen by this bot
+ if jid not in self.__seen:
+ self.log('Ignoring message from unseen guest: %s' % jid)
+ self.debug("I've seen: %s" % ["%s" % x for x in self.__seen.keys()])
+ return
+
+ # Remember the last-talked-in thread for replies
+ self.__threads[jid] = mess.getThread()
+
+ if ' ' in text:
+ command, args = text.split(' ', 1)
+ else:
+ command, args = text, ''
+ cmd = command.lower()
+ self.debug("*** cmd = %s" % cmd)
+
+ if self.commands.has_key(cmd):
+ try:
+ reply = self.commands[cmd](mess, args)
+ except Exception, e:
+ reply = traceback.format_exc(e)
+ self.log('An error happened while processing a message ("%s") from %s: %s"' % (text, jid, reply))
+ print reply
+ else:
+ # In private chat, it's okay for the bot to always respond.
+ # In group chat, the bot should silently ignore commands it
+ # doesn't understand or aren't handled by unknown_command().
+ default_reply = 'Unknown command: "%s". Type "help" for available commands.<b>blubb!</b>' % cmd
+ if type == "groupchat": default_reply = None
+ reply = self.unknown_command( mess, cmd, args) or default_reply
+ if reply:
+ self.send_simple_reply(mess,reply)
+
+ def unknown_command(self, mess, cmd, args):
+ """Default handler for unknown commands
+
+ Override this method in derived class if you
+ want to trap some unrecognized commands. If
+ 'cmd' is handled, you must return some non-false
+ value, else some helpful text will be sent back
+ to the sender.
+ """
+ return None
+
+ def top_of_help_message(self):
+ """Returns a string that forms the top of the help message
+
+ Override this method in derived class if you
+ want to add additional help text at the
+ beginning of the help message.
+ """
+ return ""
+
+ def bottom_of_help_message(self):
+ """Returns a string that forms the bottom of the help message
+
+ Override this method in derived class if you
+ want to add additional help text at the end
+ of the help message.
+ """
+ return ""
+
+ @botcmd
+ def help(self, mess, args):
+ """Returns a help string listing available options.
+
+ Automatically assigned to the "help" command."""
+ if not args:
+ if self.__doc__:
+ description = self.__doc__.strip()
+ else:
+ description = 'Available commands:'
+
+ usage = '\n'.join(sorted(['%s: %s' % (name, (command.__doc__ or '(undocumented)').split('\n', 1)[0]) for (name, command) in self.commands.items() if name != 'help' and not command._jabberbot_hidden]))
+ usage = usage + '\n\nType help <command name> to get more info about that specific command.'
+ else:
+ description = ''
+ if args in self.commands:
+ usage = self.commands[args].__doc__ or 'undocumented'
+ else:
+ usage = 'That command is not defined.'
+
+ top = self.top_of_help_message()
+ bottom = self.bottom_of_help_message()
+ if top : top = "%s\n\n" % top
+ if bottom: bottom = "\n\n%s" % bottom
+
+ return '%s%s\n\n%s%s' % ( top, description, usage, bottom )
+
+ def idle_proc( self):
+ """This function will be called in the main loop."""
+ pass
+
+ def shutdown(self):
+ """This function will be called when we're done serving
+
+ Override this method in derived class if you
+ want to do anything special at shutdown.
+ """
+ pass
+
+ def serve_forever( self, connect_callback = None, disconnect_callback = None):
+ """Connects to the server and handles messages."""
+ conn = self.connect()
+ if conn:
+ self.log('bot connected. serving forever.')
+ else:
+ self.log('could not connect to server - aborting.')
+ return
+
+ if connect_callback:
+ connect_callback()
+
+ while not self.__finished:
+ try:
+ conn.Process(1)
+ self.idle_proc()
+ except KeyboardInterrupt:
+ self.log('bot stopped by user request. shutting down.')
+ break
+
+ self.shutdown()
+
+ if disconnect_callback:
+ disconnect_callback()
+
+
More information about the Python-modules-commits
mailing list