changeset 2106:1577663f6354

Notify when a new attachment is added to a page. s/JabberBot/jabberbot/.
author Karol 'grzywacz' Nowak <grzywacz@sul.uni.lodz.pl>
date Thu, 07 Jun 2007 17:53:30 +0200
parents cc5067ea1235
children 645bbc7fe2b5
files JabberBot/__init__.py JabberBot/commands.py JabberBot/main.py JabberBot/xmlrpcbot.py JabberBot/xmppbot.py MoinMoin/action/AttachFile.py MoinMoin/events/JabberNotification.py MoinMoin/events/__init__.py MoinMoin/events/messages.py MoinMoin/events/notification_common.py jabberbot/__init__.py jabberbot/commands.py jabberbot/main.py jabberbot/xmlrpcbot.py jabberbot/xmppbot.py
diffstat 15 files changed, 767 insertions(+), 691 deletions(-) [+]
line wrap: on
line diff
--- a/JabberBot/__init__.py	Thu Jun 07 12:43:31 2007 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,11 +0,0 @@
-# -*- coding: iso-8859-1 -*-
-"""
-    MoinMoin - jabber bot
-
-    This is a bot for notification and simple editing
-    operations. Developed as a Google Summer of Code 
-    project.
-
-@copyright: 2007 by Karol Nowak <grywacz@gmail.com>
-@license: GNU GPL, see COPYING for details.
-"""
--- a/JabberBot/commands.py	Thu Jun 07 12:43:31 2007 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,27 +0,0 @@
-# -*- coding: iso-8859-1 -*-
-"""
-    MoinMoin - inter-thread communication commands
-
-    This file defines command objects used by notification
-    bot's threads to communicate among each other.
-
-    @copyright: 2007 by Karol Nowak <grywacz@gmail.com>
-    @license: GNU GPL, see COPYING for details.
-"""
-
-# First, XML RPC -> XMPP commands
-class NotificationCommand:
-    """Class representing a notification request"""
-    def __init__(self, jid, text):
-        self.jid = jid
-        self.text = text
-        
-class AddJIDToRosterCommand:
-    """Class representing a request to add a new jid to roster"""
-    def __init__(self, jid):
-        self.jid = jid
-        
-class RemoveJIDFromRosterCommand:
-    """Class representing a request to remove a jid from roster"""
-    def __init__(self, jid):
-        self.jid = jid
--- a/JabberBot/main.py	Thu Jun 07 12:43:31 2007 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,52 +0,0 @@
-# -*- coding: iso-8859-1 -*-
-"""
-    MoinMoin - jabber bot main file
-
-    This is a bot for notification and simple editing
-    operations. Developed as a Google Summer of Code 
-    project.
-
-    @copyright: 2007 by Karol Nowak <grywacz@gmail.com>
-    @license: GNU GPL, see COPYING for details.
-"""
-
-import sys
-import os
-
-from config import Config
-from xmppbot import XMPPBot
-from xmlrpcbot import XMLRPCServer, XMLRPCClient
-from Queue import Queue
-
-
-def main():
-    args = sys.argv
-    
-    if "--help" in args:
-        print """MoinMoin notification bot
-        
-        Usage: %(myname)s [--server server] [--xmpp_port port] [--user user] [--resource resource] [--password pass] [--xmlrpc_host host] [--xmlrpc_port port]
-        """ % { "myname": os.path.basename(args[0]) }
-        
-        raise SystemExit
-    
-    # TODO: actually accept options from the help string
-
-    commands_from_xmpp = Queue()
-    commands_to_xmpp = Queue()
-    
-    try:
-        xmpp_bot = XMPPBot(Config, commands_from_xmpp, commands_to_xmpp)
-        xmlrpc_client = XMLRPCClient(Config, commands_from_xmpp)
-        xmlrpc_server = XMLRPCServer(Config, commands_to_xmpp)
-        
-        xmpp_bot.start()
-        xmlrpc_client.start()
-        xmlrpc_server.start()
-    
-    except KeyboardInterrupt, i:
-        print i
-        sys.exit(0)
-
-        
-if __name__ == "__main__": main()
--- a/JabberBot/xmlrpcbot.py	Thu Jun 07 12:43:31 2007 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,111 +0,0 @@
-# -*- coding: iso-8859-1 -*-
-"""
-    MoinMoin - a xmlrpc server and client for the notification bot
-
-    This is a bot for notification and simple editing
-    operations. Developed as a Google Summer of Code 
-    project.
-
-    @copyright: 2007 by Karol Nowak <grywacz@gmail.com>
-    @license: GNU GPL, see COPYING for details.
-"""
-
-import time, xmlrpclib
-from threading import Thread
-from SimpleXMLRPCServer import SimpleXMLRPCServer
-
-from commands import *
-
-
-class XMLRPCClient(Thread):
-    """XMLRPC Client
-    
-    It's responsible for performing XMLRPC operations on
-    a wiki, as inctructed by command objects received from
-    the XMPP component"""
-    
-    def __init__(self, config, commands):
-        """
-        @param commands: an output command queue
-        """
-        Thread.__init__(self)
-        self.commands = commands
-        self.config = config
-        
-    def run(self):
-        """Starts the server / thread"""
-        pass
-
-
-class XMLRPCServer(Thread):
-    """XMLRPC Server
-    
-    It waits for notifications requests coming from wiki,
-    creates command objects and puts them on a queue for
-    later processing by the XMPP component
-    
-    @param commands: an input command queue
-    """
-    
-    def __init__(self, config, commands):
-        Thread.__init__(self)
-        self.commands = commands
-        self.verbose = config.verbose
-        self.secret = config.secret
-        self.server = SimpleXMLRPCServer((config.xmlrpc_host, config.xmlrpc_port))
-        
-    def run(self):
-        """Starts the server / thread"""
-        
-        # Register methods having an "export" attribute as XML RPC functions and 
-        # decorate them with a check for a shared (wiki-bot) secret.
-        items = self.__class__.__dict__.items()
-        methods = [(name, func) for (name, func) in items if callable(func) 
-                   and "export" in func.__dict__]
-
-        for name, func in methods:
-            self.server.register_function(self.secret_check(func), name)
-        
-        self.server.serve_forever()
-        
-    def log(self, message):
-        """Logs a message and its timestamp"""
-        
-        t = time.localtime( time.time() )
-        print time.strftime("%H:%M:%S", t), message
-
-    def secret_check(self, function):
-        """Adds a check for a secret to a given function
-        
-        Using this one does not have to worry about checking for the secret
-        in every XML RPC function.
-        """
-        def protected_func(secret, *args):
-            if secret != self.secret:
-                raise xmlrpclib.Fault(1, "You are not allowed to use this bot!")
-            else:
-                return function(self, *args)
-            
-        return protected_func
-    
-    
-    def send_notification(self, jid, text):
-        """Instructs the XMPP component to send a notification"""           
-        c = NotificationCommand(jid, text)
-        self.commands.put_nowait(c)
-        return True
-    send_notification.export = True
-    
-    def addJIDToRoster(self, jid):
-        """Instructs the XMPP component to add a new JID to its roster"""  
-        c = AddJIDToRosterCommand(jid)
-        self.commands.put_nowait(c)
-        return True
-    addJIDToRoster.export = True
-    
-    def removeJIDFromRoster(self, jid):
-        """Instructs the XMPP component to remove a JID from its roster"""      
-        c = RemoveJIDFromRosterCommand(jid)
-        self.commands.put_nowait(c)
-        return True
-    removeJIDFromRoster.export = True
--- a/JabberBot/xmppbot.py	Thu Jun 07 12:43:31 2007 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,416 +0,0 @@
-# -*- coding: iso-8859-1 -*-
-"""
-    MoinMoin - a XMPP bot
-
-    This is a bot for notification and simple editing
-    operations. Developed as a Google Summer of Code 
-    project.
-
-    @copyright: 2007 by Karol Nowak <grywacz@gmail.com>
-    @license: GNU GPL, see COPYING for details.
-"""
-
-import time
-import Queue
-from threading import Thread
-
-from pyxmpp.client import Client
-from pyxmpp.jid import JID
-from pyxmpp.streamtls import TLSSettings
-from pyxmpp.message import Message
-from pyxmpp.presence import Presence
-
-from commands import *
-
-
-class Contact:
-    """Abstraction of a roster item / contact
- 
-    This class handles some logic related to keeping track of
-    contact availability, status, etc."""
-    
-    def __init__(self, jid, resource, priority, show):
-        self.jid = jid
-        self.resources = { resource: {'show': show, 'priority': priority} }
-
-        # Queued messages, waiting for contact to change its "show"
-        # status to something different than "dnd". The messages should
-        # also be sent when contact becomes "unavailable" directly from
-        # "dnd", as we can't guarantee, that the bot will be up and running
-        # the next time she becomes "available".
-        self.messages = []
-        
-    def add_resource(self, resource, show, priority):
-        """Adds information about a connected resource"""
-        self.resources[resource] = {'show': show, 'priority': priority}
-    
-    def remove_resource(self, resource):
-        """Removes information about a connected resource"""
-        
-        if self.resources.has_key(resource):
-            del self.resources[resource]
-        else:
-            raise ValueError("No such resource!")
-        
-    def is_dnd(self):
-        """Checks if contact is DoNotDisturb
-        
-        The contact is DND if its resource with the highest priority is DND
-        """
-        
-        # Priority can't be lower than -128
-        max_prio = -129
-        max_prio_show = u"dnd"
-        
-        for resource in self.resources.itervalues():
-            # TODO: check RFC for behaviour of 2 resources with the same priority
-            if resource['priority'] > max_prio:
-                max_prio = resource['priority']
-                max_prio_show = resource['show']
-                
-        if max_prio_show == u'dnd':
-            return True
-        else:
-            return False
-        
-    def set_show(self, resource, show):
-        """Sets show property for a given resource
-        
-        @param resource: resource to alter
-        @param show: new value of the show property
-        @raise ValueError: no resource with given name has been found
-        """
-        if self.resources.has_key(resource):
-            self.resources[resource]['show'] = show
-        else:
-            raise ValueError("There's no such resource")
-    
-    def uses_resource(self, resource):
-        """Checks if contact uses a given resource"""
-        return self.resources.has_key(resource)
-        
-    def __str__(self):
-        retval = "%s (%s) has %d queued messages"
-        res = ", ".join([name + " is " + res['show'] for name, res in self.resources.items()])
-        return retval % (self.jid.as_utf8(), res, len(self.messages))
-
-
-class XMPPBot(Client, Thread):
-    """A simple XMPP bot"""
-       
-    def __init__(self, config, from_commands, to_commands):
-        """A constructor.
-        
-        @param from_commands: a Queue object used to send commands to other (xmlrpc) threads
-        @param to_commands: a Queue object used to receive commands from other threads
-        """
-        
-        Thread.__init__(self)
-        
-        self.from_commands = from_commands
-        self.to_commands = to_commands   
-        jid = u"%s@%s/%s" % (config.xmpp_node, config.xmpp_server, config.xmpp_resource)
-        
-        self.config = config
-        self.jid = JID(node_or_jid=jid, domain=config.xmpp_server, resource=config.xmpp_resource)
-        self.tlsconfig = TLSSettings(require = True, verify_peer=False)
-        
-        # A dictionary of contact objects, ordered by bare JID
-        self.contacts = { }
-        
-        Client.__init__(self, self.jid, config.xmpp_password, config.xmpp_server, tls_settings=self.tlsconfig)
-            
-    def run(self):
-        """Start the bot - enter the event loop"""
-        
-        if self.config.verbose:
-            self.log("Starting the jabber bot.")
-            
-        self.connect()
-        self.loop()
-        
-    def loop(self, timeout=1):
-        """Main event loop - stream and command handling"""
-        
-        while 1:
-            stream = self.get_stream()
-            if not stream:
-                break
-            
-            act = stream.loop_iter(timeout)
-            if not act:
-                # Process all available commands
-                while self.poll_commands(): pass
-                self.idle()
-        
-    def poll_commands(self):
-        """Checks for new commands in the input queue and executes them
-        
-        @return: True if any command has been executed, False otherwise."""
-        
-        try:
-            command = self.to_commands.get_nowait()
-            self.handle_command(command)
-            return True
-        except Queue.Empty:
-            return False
-        
-    def handle_command(self, command, ignore_dnd=False):
-        """Excecutes commands from other components"""
-        
-        # Handle normal notifications
-        if isinstance(command, NotificationCommand):
-            jid = JID(node_or_jid=command.jid)
-            jid_text = jid.bare().as_utf8()
-            text = command.text
-            
-            # Check if contact is DoNotDisturb. 
-            # If so, queue the message for delayed delivery.
-            try:
-                contact = self.contacts[jid_text]
-                if contact.is_dnd() and not ignore_dnd:
-                    contact.messages.append(command)
-                    return
-            except KeyError:
-                pass
-            
-            self.send_message(jid, text)
-            
-        # Handle subscribtion management commands
-        if isinstance(command, AddJIDToRosterCommand):
-            jid = JID(node_or_jid=command.jid)
-            self.ask_for_subscription(jid)
-            
-        if isinstance(command, RemoveJIDFromRosterCommand):
-            jid = JID(node_or_jid=command.jid)
-            self.remove_subscription(jid)
-            
-    def ask_for_subscription(self, jid):
-        """Sends a <presence/> stanza with type="subscribe"
-        
-        Bot tries to subscribe to every contact's presence, so that
-        it can honor special cases, like DoNotDisturb setting.
-        
-        @param jid: Jabber ID of entity we're subscribing to
-        @type jid: pyxmpp.jid.JID
-        
-        """
-        stanza = Presence(to_jid=jid, stanza_type="subscribe")
-        self.get_stream().send(stanza)
-        
-    def remove_subscription(self, jid):
-        """Sends a <presence/> stanza with type="unsubscribed
-        
-        @param jid: Jabber ID of entity whose subscription we cancel
-        @type jid: JID
-        
-        """
-        stanza = Presence(to_jid=jid, stanza_type="unsubscribed")
-        self.get_stream().send(stanza)
-        
-    def send_message(self, jid, text, msg_type=u"chat"):
-        """Sends a message
-        
-        @param jid: JID to send the message to
-        @param text: message's body
-        @param type: message type, as defined in RFC"""
-        
-        message = Message(to_jid=jid, body=text, stanza_type=msg_type)
-        self.get_stream().send(message)
-    
-    def handle_message(self, message):
-        """Handles incoming messages"""
-        
-        if self.config.verbose:
-            msg = "Message from %s." % (message.get_from_jid().as_utf8(),)
-            self.log(msg)
-            
-        text = message.get_body()
-        sender = message.get_from_jid()
-        command = text.split()
-        
-        # Ignore empty commands
-        if len(command) == 0:
-            return
-        
-        response = self.reply_help()
-        
-        if not response == u"":
-            self.send_message(sender, response)
-            
-    def handle_unsubscribed_presence(self, stanza):
-        """Handles unsubscribed presence stanzas"""
-        
-        # FiXME: what policy should we adopt in this case?
-        pass
-    
-    def handle_subscribe_presence(self, stanza):
-        """Handles subscribe presence stanzas (requests)"""
-        
-        # FIXME: Let's just accept all subscribtion requests for now
-        response = stanza.make_accept_response()
-        self.get_stream().send(response)
-        
-    def handle_unavailable_presence(self, stanza):
-        """Handles unavailable presence stanzas"""
-        
-        if self.config.verbose:
-            self.log("Handling unavailable presence.")
-        
-        jid = stanza.get_from_jid()
-        bare_jid = jid.bare().as_utf8()
-        
-        # If we get presence, this contact should already be known
-        if bare_jid in self.contacts:    
-            contact = self.contacts[bare_jid]
-            
-            if self.config.verbose:
-                self.log("%s, going OFFLINE." % contact)
-            
-            try:
-                # Send queued messages now, as we can't guarantee to be 
-                # alive the next time this contact becomes available.
-                if len(contact.resources) == 1:    
-                    self.send_queued_messages(contact, ignore_dnd=True)
-                    del self.contacts[bare_jid]
-                else:
-                    contact.remove_resource(jid.resource)
-                    
-                    # The highest-priority resource, which used to be DnD might
-                    # have gone offline. If so, try to deliver messages now.
-                    if not contact.is_dnd():
-                        self.send_queued_messages(contact)
-                    
-            except ValueError:
-                self.log("Unknown contact (resource) going offline...")
-            
-        else:
-            self.log("Unavailable presence from unknown contact.")
-                
-        # Confirm that we've handled this stanza
-        return True
-    
-    def handle_available_presence(self, presence):
-        """Handles available presence stanzas"""
-        if self.config.verbose:
-            self.log("Handling available presence.")
-        
-        show = presence.get_show()
-        if show is None:
-            show = u'available'
-            
-        priority = presence.get_priority()
-        jid = presence.get_from_jid()
-        bare_jid = jid.bare().as_utf8()
-               
-        if bare_jid in self.contacts:    
-            contact = self.contacts[bare_jid]              
-                
-            # The resource is already known, so update it
-            if contact.uses_resource(jid.resource):
-                contact.set_show(jid.resource, show)
-            
-            # Unknown resource, add it to the list
-            else:
-                contact.add_resource(jid.resource, show, priority)
-
-            if self.config.verbose:
-                self.log(contact)
-
-            # Either way check, if we can deliver queued messages now
-            if not contact.is_dnd():
-                self.send_queued_messages(contact)
-                
-        else:
-            self.contacts[bare_jid] = Contact(jid, jid.resource, priority, show)
-            
-            if self.config.verbose:
-                self.log(self.contacts[bare_jid])
-        
-        # Confirm that we've handled this stanza
-        return True
-    
-    def send_queued_messages(self, contact, ignore_dnd=False):
-        """Sends messages queued for the contact"""
-        for command in contact.messages:
-            self.handle_command(command, ignore_dnd)
-                    
-    def reply_help(self):
-        """Constructs a generic help message
-        
-        It's sent in response to an uknown message or the "help" command."""
-        
-        return u"""Hello there! I'm a MoinMoin Notification Bot. Too bad I can't say anything more (yet!)."""
-    
-    def log(self, message):
-        """Logs a message and its timestamp"""
-        
-        t = time.localtime( time.time() )
-        print time.strftime("%H:%M:%S", t), message
-    
-    def authenticated(self):
-        """Called when authentication succeedes"""
-        
-        if self.config.verbose:
-            self.log("Authenticated.")
-            
-    def authorized(self):
-        """Called when authorization succeedes"""
-        
-        if self.config.verbose:
-            self.log("Authorized.")
-        
-        stream = self.get_stream()
-        stream.set_message_handler("normal", self.handle_message)
-        stream.set_presence_handler("available", self.handle_available_presence)
-        stream.set_presence_handler("unavailable", self.handle_unavailable_presence)
-        stream.set_presence_handler("unsubscribed", self.handle_unsubscribed_presence)
-        stream.set_presence_handler("subscribe", self.handle_subscribe_presence)
-        
-        self.request_session()
-            
-    def connected(self):
-        """Called when connections has been established"""
-        
-        if self.config.verbose:
-            self.log("Connected.")
-            
-    def disconnected(self):
-        """Called when disconnection occurs"""
-        
-        if self.config.verbose:
-            self.log("Disconnected.")
-            
-    def roster_updated(self, item=None):
-        """Called when roster gets updated"""
-        
-        if self.config.verbose:
-            self.log("Updating roster.")
-            
-            contacts = [ str(c) for c in self.roster.get_items() ]
-            print "Groups:", self.roster.get_groups()
-            print "Contacts:", " ".join(contacts)
-            
- #   def session_started(self):
- #       """Called when session has been successfully started"""
- #       
- #       if self.config.verbose:
- #           self.log("Session started.")
-            
-    def stream_closed(self, stream):
-        """Called when stream closes"""
-        
-        if self.config.verbose:
-            self.log("Stream closed.")
-            
-    def stream_created(self, stream):
-        """Called when stream gets created"""
-        
-        if self.config.verbose:
-            self.log("Stream created.")
-            
-    def stream_error(self, error):
-        """Called when stream error gets received"""
-        
-        if self.config.verbose:
-            self.log("Received a stream error.")
--- a/MoinMoin/action/AttachFile.py	Thu Jun 07 12:43:31 2007 +0200
+++ b/MoinMoin/action/AttachFile.py	Thu Jun 07 17:53:30 2007 +0200
@@ -30,6 +30,7 @@
 from MoinMoin import config, wikiutil, packages
 from MoinMoin.Page import Page
 from MoinMoin.util import filesys, timefuncs
+from MoinMoin.events import FileAttachedEvent, send_event
 
 action_name = __name__.split('.')[-1]
 
@@ -219,6 +220,10 @@
             stream.close()
 
         _addLogEntry(request, 'ATTNEW', pagename, target)
+        
+        event = FileAttachedEvent(request, pagename, target, len(filecontent))
+        messages = send_event(event)
+        msg = "".join(messages)
 
         if request.cfg.xapian_search:
             from MoinMoin.search.Xapian import Index
--- a/MoinMoin/events/JabberNotification.py	Thu Jun 07 12:43:31 2007 +0200
+++ b/MoinMoin/events/JabberNotification.py	Thu Jun 07 17:53:30 2007 +0200
@@ -11,9 +11,11 @@
 import xmlrpclib
 
 from MoinMoin.user import User, getUserList
+from MoinMoin.Page import Page
 
 from MoinMoin.events import *
-from MoinMoin.events.notification_common import page_changed_notification
+from MoinMoin.events.messages import page_changed_notification
+from MoinMoin.events.messages import file_attached_notification
 
 
 # XML RPC Server object used to communicate with notification bot
@@ -33,14 +35,16 @@
     if server is None:
         server = xmlrpclib.Server("http://" + cfg.bot_host)
     
-    if isinstance(event, PageEvent):
+    if isinstance(event, PageChangedEvent):
         return handle_page_changed(event)
     elif isinstance(event, JabberIDSetEvent) or isinstance(event, JabberIDUnsetEvent):
         return handle_jid_changed(event)
+    elif isinstance(event, FileAttachedEvent):
+        return handle_file_attached(event)
     
 
 def handle_jid_changed(event):
-    """ Handle events sent when user's JID changes """
+    """ Handles events sent when user's JID changes """
     
     request = event.request
     _ = request.getText
@@ -58,9 +62,41 @@
         print _("Low-level communication error: "), str(err)
         return (0, _("Notifications not sent"))
 
-def handle_jid_unset(event):
-    pass
 
+def handle_file_attached(event):
+    """Handles event sent when a file is attached to a page"""
+    
+    request = event.request
+    pagename = event.pagename
+    size = event.size
+    attach_name = event.attachment_name
+    page = Page(request, pagename)
+    
+    _ = request.getText
+    
+    subscribers = page.getSubscribers(request, return_users=1)
+    if subscribers:
+        # send notifications to all subscribers
+        results = [_('Status of sending notifications:')]
+        
+        for lang in subscribers:
+            jids = [u.jid for u in subscribers[lang]]
+            names = [u.name for u in subscribers[lang]]
+            msg = file_attached_notification(request, pagename, lang, attach_name, size)
+            print msg
+            jabberok, status = send_notification(request, jids, msg)
+            recipients = ", ".join(names)
+            results.append(_('[%(lang)s] %(recipients)s: %(status)s') % {
+                'lang': lang, 'recipients': recipients, 'status': status})
+
+        # Return notifications sent results. Ignore trivial - we don't have
+        # to lie. If notification was sent, just tell about it.
+        return '<p>\n%s\n</p> ' % '<br>'.join(results)
+
+    # No notifications sent, no message.
+    return ''
+        
+        
 def handle_page_changed(event):
     """ Handles events related to page changes """
     
@@ -82,7 +118,8 @@
         for lang in subscribers:
             jids = [u.jid for u in subscribers[lang]]
             names = [u.name for u in subscribers[lang]]
-            jabberok, status = send_notification(request, page, comment, jids, lang, revisions, trivial)
+            msg = page_changed_notification(request, page, comment, lang, revisions, trivial)
+            jabberok, status = send_notification(request, jids, msg)
             recipients = ", ".join(names)
             results.append(_('[%(lang)s] %(recipients)s: %(status)s') % {
                 'lang': lang, 'recipients': recipients, 'status': status})
@@ -94,7 +131,7 @@
     # No notifications sent, no message.
     return ''
 
-def send_notification(request, page, comment, jids, message_lang, revisions, trivial):
+def send_notification(request, jids, message):
     """ Send notifications for a single language.
 
     @param comment: editor's comment given when saving the page
@@ -104,12 +141,11 @@
     @param trivial: the change is marked as trivial
     """
     _ = request.getText
-    msg = page_changed_notification(request, page, comment, message_lang, revisions, trivial)
     
     for jid in jids:
         # FIXME: stops sending notifications on first error
         try:
-            server.send_notification(request.cfg.secret, jid, msg)
+            server.send_notification(request.cfg.secret, jid, message)
         except xmlrpclib.Error, err:
             print _("XML RPC error: "), str(err)
             return (0, _("Notifications not sent"))
--- a/MoinMoin/events/__init__.py	Thu Jun 07 12:43:31 2007 +0200
+++ b/MoinMoin/events/__init__.py	Thu Jun 07 17:53:30 2007 +0200
@@ -45,8 +45,14 @@
     pass
 class PageDeletedEvent(PageEvent):
     pass
+
+
 class FileAttachedEvent(PageEvent):
-    pass
+    def __init__(self, request, pagename, attachment_name, size):
+        self.request = request
+        self.pagename = pagename
+        self.attachment_name = attachment_name
+        self.size = size
 
 
 class PageRevertedEvent(PageEvent):
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MoinMoin/events/messages.py	Thu Jun 07 17:53:30 2007 +0200
@@ -0,0 +1,91 @@
+# -*- coding: iso-8859-1 -*-
+"""
+    MoinMoin - common functions for notification framework
+
+    Code for building messages informing about events (changes)
+    happening in the wiki.
+
+    @copyright: 2007 by Karol Nowak <grywacz@gmail.com>
+    @license: GNU GPL, see COPYING for details.
+"""
+
+from MoinMoin import user, wikiutil
+from MoinMoin.Page import Page
+from MoinMoin.action.AttachFile import getAttachUrl
+
+
+def page_changed_notification(request, page, comment, lang, revisions, trivial):
+    """ Prepare a notification text for a single language
+
+    @param comment: editor's comment given when saving the page
+    @param lang: language of notifications
+    @param revisions: revisions of this page (newest first!)
+    @param trivial: the change is marked as trivial
+    @rtype: int
+    @return: composed string message
+    """
+    _ = request.getText
+    page._ = lambda s, formatted=True, r=request, l=lang: r.getText(s, formatted=formatted, lang=l)
+
+    if len(revisions) >= 2:
+        querystr = {'action': 'diff',
+                    'rev2': str(revisions[0]),
+                    'rev1': str(revisions[1])}
+    else:
+        querystr = {}
+        
+    pagelink = request.getQualifiedURL(page.url(request, querystr, relative=False))
+
+    messageBody = _("Dear Wiki user,\n\n"
+        'You have subscribed to a wiki page or wiki category on "%(sitename)s" for change notification.\n\n'
+        "The following page has been changed by %(editor)s:\n"
+        "%(pagelink)s\n\n", formatted=False) % {
+            'editor': page.uid_override or user.getUserIdentification(request),
+            'pagelink': pagelink,
+            'sitename': page.cfg.sitename or request.getBaseURL(),
+    }
+
+    if comment:
+        messageBody = messageBody + \
+            _("The comment on the change is:\n%(comment)s\n\n", formatted=False) % {'comment': comment}
+
+    # append a diff (or append full page text if there is no diff)
+    if len(revisions) < 2:
+        messageBody = messageBody + \
+            _("New page:\n", formatted=False) + \
+            page.get_raw_body()
+    else:
+        lines = wikiutil.pagediff(request, page.page_name, revisions[1],
+                                  page.page_name, revisions[0])
+        if lines:
+            messageBody = messageBody + "%s\n%s\n" % (("-" * 78), '\n'.join(lines))
+        else:
+            messageBody = messageBody + _("No differences found!\n", formatted=False)
+            
+    return messageBody
+
+def file_attached_notification(request, pagename, lang, attach_name, attach_size):
+    
+    _ = request.getText
+    page = Page(request, pagename)
+    pagelink = request.getQualifiedURL(page.url(request, {}, relative=False))
+    attachlink = request.getBaseURL() + getAttachUrl(pagename, attach_name, request)
+    
+    messageBody = _("Dear Wiki user.\n\n"
+        'You have subscribed to a wiki page "%(sitename)s" for change notification.\n\n'
+        "An attachment has been added to the following page by %(editor)s:\n"
+        "%(pagelink)s\n\n"
+        "Following detailed information is available:\n"
+        "Attachment name: %(attach_name)s\n"
+        "Attachment size: %(attach_size)s\n"
+        "Download link: %(attach_get)s", formatted=False) % {
+            'editor': user.getUserIdentification(request),
+            'pagelink': pagelink,
+            'sitename': page.cfg.sitename or request.getBaseURL(),
+            'attach_name': attach_name,
+            'attach_size': attach_size,
+            'attach_get': attachlink,
+    }
+        
+    return messageBody
+
--- a/MoinMoin/events/notification_common.py	Thu Jun 07 12:43:31 2007 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,64 +0,0 @@
-# -*- coding: iso-8859-1 -*-
-"""
-    MoinMoin - common functions for notification framework
-
-    Code for building messages informing about events (changes)
-    happening in the wiki.
-
-    @copyright: 2007 by Karol Nowak <grywacz@gmail.com>
-    @license: GNU GPL, see COPYING for details.
-"""
-
-from MoinMoin import user, wikiutil
-from MoinMoin.Page import Page
-
-
-def page_changed_notification(request, page, comment, lang, revisions, trivial):
-    """ Prepare a notification text for a single language
-
-    @param comment: editor's comment given when saving the page
-    @param lang: language of notifications
-    @param revisions: revisions of this page (newest first!)
-    @param trivial: the change is marked as trivial
-    @rtype: int
-    @return: composed string message
-    """
-    _ = request.getText
-    page._ = lambda s, formatted=True, r=request, l=lang: r.getText(s, formatted=formatted, lang=l)
-
-    if len(revisions) >= 2:
-        querystr = {'action': 'diff',
-                    'rev2': str(revisions[0]),
-                    'rev1': str(revisions[1])}
-    else:
-        querystr = {}
-        
-    pagelink = request.getQualifiedURL(page.url(request, querystr, relative=False))
-
-    messageBody = _("Dear Wiki user,\n\n"
-        'You have subscribed to a wiki page or wiki category on "%(sitename)s" for change notification.\n\n'
-        "The following page has been changed by %(editor)s:\n"
-        "%(pagelink)s\n\n", formatted=False) % {
-            'editor': page.uid_override or user.getUserIdentification(request),
-            'pagelink': pagelink,
-            'sitename': page.cfg.sitename or request.getBaseURL(),
-    }
-
-    if comment:
-        messageBody = messageBody + \
-            _("The comment on the change is:\n%(comment)s\n\n", formatted=False) % {'comment': comment}
-
-    # append a diff (or append full page text if there is no diff)
-    if len(revisions) < 2:
-        messageBody = messageBody + \
-            _("New page:\n", formatted=False) + \
-            page.get_raw_body()
-    else:
-        lines = wikiutil.pagediff(request, page.page_name, revisions[1],
-                                  page.page_name, revisions[0])
-        if lines:
-            messageBody = messageBody + "%s\n%s\n" % (("-" * 78), '\n'.join(lines))
-        else:
-            messageBody = messageBody + _("No differences found!\n", formatted=False)
-            
-    return messageBody
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/jabberbot/__init__.py	Thu Jun 07 17:53:30 2007 +0200
@@ -0,0 +1,11 @@
+# -*- coding: iso-8859-1 -*-
+"""
+    MoinMoin - jabber bot
+
+    This is a bot for notification and simple editing
+    operations. Developed as a Google Summer of Code 
+    project.
+
+@copyright: 2007 by Karol Nowak <grywacz@gmail.com>
+@license: GNU GPL, see COPYING for details.
+"""
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/jabberbot/commands.py	Thu Jun 07 17:53:30 2007 +0200
@@ -0,0 +1,27 @@
+# -*- coding: iso-8859-1 -*-
+"""
+    MoinMoin - inter-thread communication commands
+
+    This file defines command objects used by notification
+    bot's threads to communicate among each other.
+
+    @copyright: 2007 by Karol Nowak <grywacz@gmail.com>
+    @license: GNU GPL, see COPYING for details.
+"""
+
+# First, XML RPC -> XMPP commands
+class NotificationCommand:
+    """Class representing a notification request"""
+    def __init__(self, jid, text):
+        self.jid = jid
+        self.text = text
+        
+class AddJIDToRosterCommand:
+    """Class representing a request to add a new jid to roster"""
+    def __init__(self, jid):
+        self.jid = jid
+        
+class RemoveJIDFromRosterCommand:
+    """Class representing a request to remove a jid from roster"""
+    def __init__(self, jid):
+        self.jid = jid
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/jabberbot/main.py	Thu Jun 07 17:53:30 2007 +0200
@@ -0,0 +1,52 @@
+# -*- coding: iso-8859-1 -*-
+"""
+    MoinMoin - jabber bot main file
+
+    This is a bot for notification and simple editing
+    operations. Developed as a Google Summer of Code 
+    project.
+
+    @copyright: 2007 by Karol Nowak <grywacz@gmail.com>
+    @license: GNU GPL, see COPYING for details.
+"""
+
+import sys
+import os
+
+from config import Config
+from xmppbot import XMPPBot
+from xmlrpcbot import XMLRPCServer, XMLRPCClient
+from Queue import Queue
+
+
+def main():
+    args = sys.argv
+    
+    if "--help" in args:
+        print """MoinMoin notification bot
+        
+        Usage: %(myname)s [--server server] [--xmpp_port port] [--user user] [--resource resource] [--password pass] [--xmlrpc_host host] [--xmlrpc_port port]
+        """ % { "myname": os.path.basename(args[0]) }
+        
+        raise SystemExit
+    
+    # TODO: actually accept options from the help string
+
+    commands_from_xmpp = Queue()
+    commands_to_xmpp = Queue()
+    
+    try:
+        xmpp_bot = XMPPBot(Config, commands_from_xmpp, commands_to_xmpp)
+        xmlrpc_client = XMLRPCClient(Config, commands_from_xmpp)
+        xmlrpc_server = XMLRPCServer(Config, commands_to_xmpp)
+        
+        xmpp_bot.start()
+        xmlrpc_client.start()
+        xmlrpc_server.start()
+    
+    except KeyboardInterrupt, i:
+        print i
+        sys.exit(0)
+
+        
+if __name__ == "__main__": main()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/jabberbot/xmlrpcbot.py	Thu Jun 07 17:53:30 2007 +0200
@@ -0,0 +1,112 @@
+# -*- coding: iso-8859-1 -*-
+"""
+    MoinMoin - a xmlrpc server and client for the notification bot
+
+    This is a bot for notification and simple editing
+    operations. Developed as a Google Summer of Code 
+    project.
+
+    @copyright: 2007 by Karol Nowak <grywacz@gmail.com>
+    @license: GNU GPL, see COPYING for details.
+"""
+
+import time, xmlrpclib
+from threading import Thread
+from SimpleXMLRPCServer import SimpleXMLRPCServer
+
+from jabberbot.commands import NotificationCommand, AddJIDToRosterCommand
+from jabberbot.commands import RemoveJIDFromRosterCommand
+
+
+class XMLRPCClient(Thread):
+    """XMLRPC Client
+    
+    It's responsible for performing XMLRPC operations on
+    a wiki, as inctructed by command objects received from
+    the XMPP component"""
+    
+    def __init__(self, config, commands):
+        """
+        @param commands: an output command queue
+        """
+        Thread.__init__(self)
+        self.commands = commands
+        self.config = config
+        
+    def run(self):
+        """Starts the server / thread"""
+        pass
+
+
+class XMLRPCServer(Thread):
+    """XMLRPC Server
+    
+    It waits for notifications requests coming from wiki,
+    creates command objects and puts them on a queue for
+    later processing by the XMPP component
+    
+    @param commands: an input command queue
+    """
+    
+    def __init__(self, config, commands):
+        Thread.__init__(self)
+        self.commands = commands
+        self.verbose = config.verbose
+        self.secret = config.secret
+        self.server = SimpleXMLRPCServer((config.xmlrpc_host, config.xmlrpc_port))
+        
+    def run(self):
+        """Starts the server / thread"""
+        
+        # Register methods having an "export" attribute as XML RPC functions and 
+        # decorate them with a check for a shared (wiki-bot) secret.
+        items = self.__class__.__dict__.items()
+        methods = [(name, func) for (name, func) in items if callable(func) 
+                   and "export" in func.__dict__]
+
+        for name, func in methods:
+            self.server.register_function(self.secret_check(func), name)
+        
+        self.server.serve_forever()
+        
+    def log(self, message):
+        """Logs a message and its timestamp"""
+        
+        t = time.localtime( time.time() )
+        print time.strftime("%H:%M:%S", t), message
+
+    def secret_check(self, function):
+        """Adds a check for a secret to a given function
+        
+        Using this one does not have to worry about checking for the secret
+        in every XML RPC function.
+        """
+        def protected_func(secret, *args):
+            if secret != self.secret:
+                raise xmlrpclib.Fault(1, "You are not allowed to use this bot!")
+            else:
+                return function(self, *args)
+            
+        return protected_func
+    
+    
+    def send_notification(self, jid, text):
+        """Instructs the XMPP component to send a notification"""           
+        c = NotificationCommand(jid, text)
+        self.commands.put_nowait(c)
+        return True
+    send_notification.export = True
+    
+    def addJIDToRoster(self, jid):
+        """Instructs the XMPP component to add a new JID to its roster"""  
+        c = AddJIDToRosterCommand(jid)
+        self.commands.put_nowait(c)
+        return True
+    addJIDToRoster.export = True
+    
+    def removeJIDFromRoster(self, jid):
+        """Instructs the XMPP component to remove a JID from its roster"""      
+        c = RemoveJIDFromRosterCommand(jid)
+        self.commands.put_nowait(c)
+        return True
+    removeJIDFromRoster.export = True
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/jabberbot/xmppbot.py	Thu Jun 07 17:53:30 2007 +0200
@@ -0,0 +1,417 @@
+# -*- coding: iso-8859-1 -*-
+"""
+    MoinMoin - a XMPP bot
+
+    This is a bot for notification and simple editing
+    operations. Developed as a Google Summer of Code 
+    project.
+
+    @copyright: 2007 by Karol Nowak <grywacz@gmail.com>
+    @license: GNU GPL, see COPYING for details.
+"""
+
+import time
+import Queue
+from threading import Thread
+
+from pyxmpp.client import Client
+from pyxmpp.jid import JID
+from pyxmpp.streamtls import TLSSettings
+from pyxmpp.message import Message
+from pyxmpp.presence import Presence
+
+from jabberbot.commands import NotificationCommand, AddJIDToRosterCommand
+from jabberbot.commands import RemoveJIDFromRosterCommand
+
+
+class Contact:
+    """Abstraction of a roster item / contact
+ 
+    This class handles some logic related to keeping track of
+    contact availability, status, etc."""
+    
+    def __init__(self, jid, resource, priority, show):
+        self.jid = jid
+        self.resources = { resource: {'show': show, 'priority': priority} }
+
+        # Queued messages, waiting for contact to change its "show"
+        # status to something different than "dnd". The messages should
+        # also be sent when contact becomes "unavailable" directly from
+        # "dnd", as we can't guarantee, that the bot will be up and running
+        # the next time she becomes "available".
+        self.messages = []
+        
+    def add_resource(self, resource, show, priority):
+        """Adds information about a connected resource"""
+        self.resources[resource] = {'show': show, 'priority': priority}
+    
+    def remove_resource(self, resource):
+        """Removes information about a connected resource"""
+        
+        if self.resources.has_key(resource):
+            del self.resources[resource]
+        else:
+            raise ValueError("No such resource!")
+        
+    def is_dnd(self):
+        """Checks if contact is DoNotDisturb
+        
+        The contact is DND if its resource with the highest priority is DND
+        """
+        
+        # Priority can't be lower than -128
+        max_prio = -129
+        max_prio_show = u"dnd"
+        
+        for resource in self.resources.itervalues():
+            # TODO: check RFC for behaviour of 2 resources with the same priority
+            if resource['priority'] > max_prio:
+                max_prio = resource['priority']
+                max_prio_show = resource['show']
+                
+        if max_prio_show == u'dnd':
+            return True
+        else:
+            return False
+        
+    def set_show(self, resource, show):
+        """Sets show property for a given resource
+        
+        @param resource: resource to alter
+        @param show: new value of the show property
+        @raise ValueError: no resource with given name has been found
+        """
+        if self.resources.has_key(resource):
+            self.resources[resource]['show'] = show
+        else:
+            raise ValueError("There's no such resource")
+    
+    def uses_resource(self, resource):
+        """Checks if contact uses a given resource"""
+        return self.resources.has_key(resource)
+        
+    def __str__(self):
+        retval = "%s (%s) has %d queued messages"
+        res = ", ".join([name + " is " + res['show'] for name, res in self.resources.items()])
+        return retval % (self.jid.as_utf8(), res, len(self.messages))
+
+
+class XMPPBot(Client, Thread):
+    """A simple XMPP bot"""
+       
+    def __init__(self, config, from_commands, to_commands):
+        """A constructor.
+        
+        @param from_commands: a Queue object used to send commands to other (xmlrpc) threads
+        @param to_commands: a Queue object used to receive commands from other threads
+        """
+        
+        Thread.__init__(self)
+        
+        self.from_commands = from_commands
+        self.to_commands = to_commands   
+        jid = u"%s@%s/%s" % (config.xmpp_node, config.xmpp_server, config.xmpp_resource)
+        
+        self.config = config
+        self.jid = JID(node_or_jid=jid, domain=config.xmpp_server, resource=config.xmpp_resource)
+        self.tlsconfig = TLSSettings(require = True, verify_peer=False)
+        
+        # A dictionary of contact objects, ordered by bare JID
+        self.contacts = { }
+        
+        Client.__init__(self, self.jid, config.xmpp_password, config.xmpp_server, tls_settings=self.tlsconfig)
+            
+    def run(self):
+        """Start the bot - enter the event loop"""
+        
+        if self.config.verbose:
+            self.log("Starting the jabber bot.")
+            
+        self.connect()
+        self.loop()
+        
+    def loop(self, timeout=1):
+        """Main event loop - stream and command handling"""
+        
+        while 1:
+            stream = self.get_stream()
+            if not stream:
+                break
+            
+            act = stream.loop_iter(timeout)
+            if not act:
+                # Process all available commands
+                while self.poll_commands(): pass
+                self.idle()
+        
+    def poll_commands(self):
+        """Checks for new commands in the input queue and executes them
+        
+        @return: True if any command has been executed, False otherwise."""
+        
+        try:
+            command = self.to_commands.get_nowait()
+            self.handle_command(command)
+            return True
+        except Queue.Empty:
+            return False
+        
+    def handle_command(self, command, ignore_dnd=False):
+        """Excecutes commands from other components"""
+        
+        # Handle normal notifications
+        if isinstance(command, NotificationCommand):
+            jid = JID(node_or_jid=command.jid)
+            jid_text = jid.bare().as_utf8()
+            text = command.text
+            
+            # Check if contact is DoNotDisturb. 
+            # If so, queue the message for delayed delivery.
+            try:
+                contact = self.contacts[jid_text]
+                if contact.is_dnd() and not ignore_dnd:
+                    contact.messages.append(command)
+                    return
+            except KeyError:
+                pass
+            
+            self.send_message(jid, text)
+            
+        # Handle subscribtion management commands
+        if isinstance(command, AddJIDToRosterCommand):
+            jid = JID(node_or_jid=command.jid)
+            self.ask_for_subscription(jid)
+            
+        if isinstance(command, RemoveJIDFromRosterCommand):
+            jid = JID(node_or_jid=command.jid)
+            self.remove_subscription(jid)
+            
+    def ask_for_subscription(self, jid):
+        """Sends a <presence/> stanza with type="subscribe"
+        
+        Bot tries to subscribe to every contact's presence, so that
+        it can honor special cases, like DoNotDisturb setting.
+        
+        @param jid: Jabber ID of entity we're subscribing to
+        @type jid: pyxmpp.jid.JID
+        
+        """
+        stanza = Presence(to_jid=jid, stanza_type="subscribe")
+        self.get_stream().send(stanza)
+        
+    def remove_subscription(self, jid):
+        """Sends a <presence/> stanza with type="unsubscribed
+        
+        @param jid: Jabber ID of entity whose subscription we cancel
+        @type jid: JID
+        
+        """
+        stanza = Presence(to_jid=jid, stanza_type="unsubscribed")
+        self.get_stream().send(stanza)
+        
+    def send_message(self, jid, text, msg_type=u"chat"):
+        """Sends a message
+        
+        @param jid: JID to send the message to
+        @param text: message's body
+        @param type: message type, as defined in RFC"""
+        
+        message = Message(to_jid=jid, body=text, stanza_type=msg_type)
+        self.get_stream().send(message)
+    
+    def handle_message(self, message):
+        """Handles incoming messages"""
+        
+        if self.config.verbose:
+            msg = "Message from %s." % (message.get_from_jid().as_utf8(),)
+            self.log(msg)
+            
+        text = message.get_body()
+        sender = message.get_from_jid()
+        command = text.split()
+        
+        # Ignore empty commands
+        if len(command) == 0:
+            return
+        
+        response = self.reply_help()
+        
+        if not response == u"":
+            self.send_message(sender, response)
+            
+    def handle_unsubscribed_presence(self, stanza):
+        """Handles unsubscribed presence stanzas"""
+        
+        # FiXME: what policy should we adopt in this case?
+        pass
+    
+    def handle_subscribe_presence(self, stanza):
+        """Handles subscribe presence stanzas (requests)"""
+        
+        # FIXME: Let's just accept all subscribtion requests for now
+        response = stanza.make_accept_response()
+        self.get_stream().send(response)
+        
+    def handle_unavailable_presence(self, stanza):
+        """Handles unavailable presence stanzas"""
+        
+        if self.config.verbose:
+            self.log("Handling unavailable presence.")
+        
+        jid = stanza.get_from_jid()
+        bare_jid = jid.bare().as_utf8()
+        
+        # If we get presence, this contact should already be known
+        if bare_jid in self.contacts:    
+            contact = self.contacts[bare_jid]
+            
+            if self.config.verbose:
+                self.log("%s, going OFFLINE." % contact)
+            
+            try:
+                # Send queued messages now, as we can't guarantee to be 
+                # alive the next time this contact becomes available.
+                if len(contact.resources) == 1:    
+                    self.send_queued_messages(contact, ignore_dnd=True)
+                    del self.contacts[bare_jid]
+                else:
+                    contact.remove_resource(jid.resource)
+                    
+                    # The highest-priority resource, which used to be DnD might
+                    # have gone offline. If so, try to deliver messages now.
+                    if not contact.is_dnd():
+                        self.send_queued_messages(contact)
+                    
+            except ValueError:
+                self.log("Unknown contact (resource) going offline...")
+            
+        else:
+            self.log("Unavailable presence from unknown contact.")
+                
+        # Confirm that we've handled this stanza
+        return True
+    
+    def handle_available_presence(self, presence):
+        """Handles available presence stanzas"""
+        if self.config.verbose:
+            self.log("Handling available presence.")
+        
+        show = presence.get_show()
+        if show is None:
+            show = u'available'
+            
+        priority = presence.get_priority()
+        jid = presence.get_from_jid()
+        bare_jid = jid.bare().as_utf8()
+               
+        if bare_jid in self.contacts:    
+            contact = self.contacts[bare_jid]              
+                
+            # The resource is already known, so update it
+            if contact.uses_resource(jid.resource):
+                contact.set_show(jid.resource, show)
+            
+            # Unknown resource, add it to the list
+            else:
+                contact.add_resource(jid.resource, show, priority)
+
+            if self.config.verbose:
+                self.log(contact)
+
+            # Either way check, if we can deliver queued messages now
+            if not contact.is_dnd():
+                self.send_queued_messages(contact)
+                
+        else:
+            self.contacts[bare_jid] = Contact(jid, jid.resource, priority, show)
+            
+            if self.config.verbose:
+                self.log(self.contacts[bare_jid])
+        
+        # Confirm that we've handled this stanza
+        return True
+    
+    def send_queued_messages(self, contact, ignore_dnd=False):
+        """Sends messages queued for the contact"""
+        for command in contact.messages:
+            self.handle_command(command, ignore_dnd)
+                    
+    def reply_help(self):
+        """Constructs a generic help message
+        
+        It's sent in response to an uknown message or the "help" command."""
+        
+        return u"""Hello there! I'm a MoinMoin Notification Bot. Too bad I can't say anything more (yet!)."""
+    
+    def log(self, message):
+        """Logs a message and its timestamp"""
+        
+        t = time.localtime( time.time() )
+        print time.strftime("%H:%M:%S", t), message
+    
+    def authenticated(self):
+        """Called when authentication succeedes"""
+        
+        if self.config.verbose:
+            self.log("Authenticated.")
+            
+    def authorized(self):
+        """Called when authorization succeedes"""
+        
+        if self.config.verbose:
+            self.log("Authorized.")
+        
+        stream = self.get_stream()
+        stream.set_message_handler("normal", self.handle_message)
+        stream.set_presence_handler("available", self.handle_available_presence)
+        stream.set_presence_handler("unavailable", self.handle_unavailable_presence)
+        stream.set_presence_handler("unsubscribed", self.handle_unsubscribed_presence)
+        stream.set_presence_handler("subscribe", self.handle_subscribe_presence)
+        
+        self.request_session()
+            
+    def connected(self):
+        """Called when connections has been established"""
+        
+        if self.config.verbose:
+            self.log("Connected.")
+            
+    def disconnected(self):
+        """Called when disconnection occurs"""
+        
+        if self.config.verbose:
+            self.log("Disconnected.")
+            
+    def roster_updated(self, item=None):
+        """Called when roster gets updated"""
+        
+        if self.config.verbose:
+            self.log("Updating roster.")
+            
+            contacts = [ str(c) for c in self.roster.get_items() ]
+            print "Groups:", self.roster.get_groups()
+            print "Contacts:", " ".join(contacts)
+            
+ #   def session_started(self):
+ #       """Called when session has been successfully started"""
+ #       
+ #       if self.config.verbose:
+ #           self.log("Session started.")
+            
+    def stream_closed(self, stream):
+        """Called when stream closes"""
+        
+        if self.config.verbose:
+            self.log("Stream closed.")
+            
+    def stream_created(self, stream):
+        """Called when stream gets created"""
+        
+        if self.config.verbose:
+            self.log("Stream created.")
+            
+    def stream_error(self, error):
+        """Called when stream error gets received"""
+        
+        if self.config.verbose:
+            self.log("Received a stream error.")