changeset 2139:3accd05a36f3

Merge with my branch
author Reimar Bauer <rb.proj AT googlemail DOT com>
date Sun, 17 Jun 2007 00:01:13 +0200
parents 0fe9e533f00e (current diff) 2941aad83666 (diff)
children ff428fa12e3c
files
diffstat 22 files changed, 1793 insertions(+), 137 deletions(-) [+]
line wrap: on
line diff
--- a/.hgignore	Sat Jun 16 23:57:54 2007 +0200
+++ b/.hgignore	Sun Jun 17 00:01:13 2007 +0200
@@ -8,4 +8,3 @@
 wiki/data/edit-log
 wiki/data/event-log
 wiki/data/cache
-
--- a/MoinMoin/Page.py	Sat Jun 16 23:57:54 2007 +0200
+++ b/MoinMoin/Page.py	Sun Jun 17 00:01:13 2007 +0200
@@ -224,7 +224,8 @@
                 self.__body = text
             finally:
                 f.close()
-        return self.__body    
+        return self.__body
+      
     def set_body(self, newbody):
         self.__body = newbody
         self.__meta = None
@@ -884,13 +885,16 @@
         """
         from MoinMoin import i18n
         from MoinMoin import security
+        request = self.request
         pi = {} # we collect the processing instructions here
-    
+
         body = self.body
+        # TODO: remove this hack once we have separate metadata and can use mimetype there
         if body.startswith('<?xml'): # check for XML content
             pi['lines'] = 0
             pi['format'] = "xslt"
             pi['formatargs'] = ''
+            pi['acl'] = security.AccessControlList(request.cfg, []) # avoid KeyError on acl check
             return pi
 
         meta = self.meta
@@ -899,7 +903,6 @@
         pi['format'] = self.cfg.default_markup or "wiki"
         pi['formatargs'] = ''
         pi['lines'] = len(meta)
-        request = self.request
         acl = []
         
         for verb, args in meta:
--- a/MoinMoin/PageEditor.py	Sat Jun 16 23:57:54 2007 +0200
+++ b/MoinMoin/PageEditor.py	Sun Jun 17 00:01:13 2007 +0200
@@ -25,7 +25,7 @@
 from MoinMoin.logfile import editlog, eventlog
 from MoinMoin.util import filesys, timefuncs, web
 from MoinMoin.mail import sendmail
-
+from MoinMoin.events import PageDeletedEvent, send_event
 
 # used for merging
 conflict_markers = ("\n---- /!\\ '''Edit conflict - other version:''' ----\n",
@@ -646,6 +646,9 @@
             raise self.AccessDenied, msg
 
         try:
+            event = PageDeletedEvent(request, self, comment)
+            send_event(event)
+            
             msg = self.saveText(u"deleted\n", 0, comment=comment or u'', index=1, deleted=True)
             msg = msg.replace(
                 _("Thank you for your changes. Your attention to detail is appreciated."),
@@ -671,95 +674,6 @@
             cache.remove()
         return success, msg
 
-    def _sendNotification(self, comment, emails, email_lang, revisions, trivial):
-        """ Send notification email for a single language.
-
-        @param comment: editor's comment given when saving the page
-        @param emails: list of email addresses
-        @param email_lang: language of emails
-        @param revisions: revisions of this page (newest first!)
-        @param trivial: the change is marked as trivial
-        @rtype: int
-        @return: sendmail result
-        """
-        request = self.request
-        _ = lambda s, formatted=True, r=request, l=email_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(self.url(request, querystr, relative=False))
-
-        mailBody = _("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': self.uid_override or user.getUserIdentification(request),
-                'pagelink': pagelink,
-                'sitename': self.cfg.sitename or request.getBaseURL(),
-        }
-
-        if comment:
-            mailBody = mailBody + \
-                _("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:
-            mailBody = mailBody + \
-                _("New page:\n", formatted=False) + \
-                self.get_raw_body()
-        else:
-            lines = wikiutil.pagediff(request, self.page_name, revisions[1],
-                                      self.page_name, revisions[0])
-            if lines:
-                mailBody = mailBody + "%s\n%s\n" % (("-" * 78), '\n'.join(lines))
-            else:
-                mailBody = mailBody + _("No differences found!\n", formatted=False)
-
-        return sendmail.sendmail(request, emails,
-            _('[%(sitename)s] %(trivial)sUpdate of "%(pagename)s" by %(username)s', formatted=False) % {
-                'trivial': (trivial and _("Trivial ", formatted=False)) or "",
-                'sitename': self.cfg.sitename or "Wiki",
-                'pagename': self.page_name,
-                'username': self.uid_override or user.getUserIdentification(request),
-            },
-            mailBody, mail_from=self.cfg.mail_from)
-
-
-    def _notifySubscribers(self, comment, trivial):
-        """ Send email to all subscribers of this page.
-
-        @param comment: editor's comment given when saving the page
-        @param trivial: editor's suggestion that the change is trivial (Subscribers may ignore this)
-        @rtype: string
-        @return: message, indicating success or errors.
-        """
-        _ = self._
-        subscribers = self.getSubscribers(self.request, return_users=1, trivial=trivial)
-        if subscribers:
-            # get a list of old revisions, and append a diff
-            revisions = self.getRevList()
-
-            # send email to all subscribers
-            results = [_('Status of sending notification mails:')]
-            for lang in subscribers:
-                emails = [u.email for u in subscribers[lang]]
-                names = [u.name for u in subscribers[lang]]
-                mailok, status = self._sendNotification(comment, emails, lang, revisions, trivial)
-                recipients = ", ".join(names)
-                results.append(_('[%(lang)s] %(recipients)s: %(status)s') % {
-                    'lang': lang, 'recipients': recipients, 'status': status})
-
-            # Return mail sent results. Ignore trivial - we don't have
-            # to lie. If mail was sent, just tell about it.
-            return '<p>\n%s\n</p> ' % '<br>'.join(results)
-
-        # No mail sent, no message.
-        return ''
-
     def _get_local_timestamp(self):
         """ Returns the string that can be used by the TIME substitution.
 
@@ -1126,9 +1040,11 @@
             self.clean_acl_cache()
             self._save_draft(None, None) # everything fine, kill the draft for this page
 
-            # send notification mails
-            if request.cfg.mail_enabled:
-                msg = msg + self._notifySubscribers(comment, trivial)
+            # send notifications
+            from MoinMoin import events
+            e = events.PageChangedEvent(self.request, self, comment, trivial)
+            messages = events.send_event(e)
+            msg = msg + "".join(messages)
 
             if kw.get('index', 1) and request.cfg.xapian_search:
                 from MoinMoin.search.Xapian import Index
--- a/MoinMoin/action/AttachFile.py	Sat Jun 16 23:57:54 2007 +0200
+++ b/MoinMoin/action/AttachFile.py	Sun Jun 17 00:01:13 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/action/revert.py	Sat Jun 16 23:57:54 2007 +0200
+++ b/MoinMoin/action/revert.py	Sun Jun 17 00:01:13 2007 +0200
@@ -40,5 +40,10 @@
         from MoinMoin import caching
         caching.CacheEntry(request, pg, key, scope='item').remove()
         caching.CacheEntry(request, pg, "pagelinks", scope='item').remove()
+        
+        # Notify observers
+        from MoinMoin.events import PageRevertedEvent, send_event
+        e = PageRevertedEvent(request, pagename, request.rev, revstr)
+        send_event(e)
 
     pg.send_page(msg=msg)
--- a/MoinMoin/config/multiconfig.py	Sat Jun 16 23:57:54 2007 +0200
+++ b/MoinMoin/config/multiconfig.py	Sun Jun 17 00:01:13 2007 +0200
@@ -462,6 +462,7 @@
 
     user_autocreate = False # do we auto-create user profiles
     user_email_unique = True # do we check whether a user's email is unique?
+    user_jid_unique = True # do we check whether a user's email is unique?
 
     # a regex of HTTP_USER_AGENTS that should be excluded from logging
     # and receive a FORBIDDEN for anything except viewing a page
@@ -546,6 +547,7 @@
         ('password', _('Password'), "password", "36", ''),
         ('password2', _('Password repeat'), "password", "36", _('(Only for password change or new account)')),
         ('email', _('Email'), "text", "36", ''),
+        ('jid', _('Jabber ID'), "text", "36", ''),
         ('css_url', _('User CSS URL'), "text", "40", _('(Leave it empty for disabling user CSS)')),
         ('edit_rows', _('Editor size'), "text", "3", ''),
     ]
@@ -556,6 +558,7 @@
         'password': '',
         'password2': '',
         'email': '',
+        'jid': '',
         'css_url': '',
         'edit_rows': "20",
     }
@@ -656,6 +659,9 @@
 
         # check if mail is possible and set flag:
         self.mail_enabled = (self.mail_smarthost is not None or self.mail_sendmail is not None) and self.mail_from
+        
+        # check if jabber bot is available and set flag:
+        self.jabber_enabled = self.bot_host is not None
 
         # Cache variables for the properties below
         self._iwid = self._iwid_full = self._meta_dict = None
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MoinMoin/events/__init__.py	Sun Jun 17 00:01:13 2007 +0200
@@ -0,0 +1,135 @@
+# -*- coding: iso-8859-1 -*-
+"""
+    MoinMoin - event (notification) framework
+
+    This code abstracts event handling in MoinMoin,
+    currently for notifications. It implements the observer pattern.
+
+    @copyright: 2007 by Karol Nowak <grywacz@gmail.com>
+    @license: GNU GPL, see COPYING for details.
+"""
+
+from MoinMoin import wikiutil
+from MoinMoin.util import pysupport
+from MoinMoin.wikiutil import PluginAttributeError
+
+
+# A list of available event handlers
+_event_handlers = None
+
+# Create a list of extension actions from the package directory
+modules = pysupport.getPackageModules(__file__)
+
+
+class Event:
+    """A class handling information common to all events."""
+    def __init__(self, request):
+        self.request = request
+
+        
+class PageEvent(Event):
+    """An event related to a page change"""
+    def __init__(self, request):
+        Event.__init__(self, request)
+
+        
+class PageChangedEvent(PageEvent):
+    def __init__(self, request, page, comment, trivial):
+        PageEvent.__init__(self, request)
+        self.page = page
+        self.comment = comment
+        self.trivial = trivial
+
+        
+class PageRenamedEvent(PageEvent):
+    pass
+
+
+class PageDeletedEvent(PageEvent):
+    def __init__(self, request, page, comment):
+        PageEvent.__init__(self, request)
+        self.request = request
+        self.page = page
+        self.comment = comment
+
+
+class FileAttachedEvent(PageEvent):
+    def __init__(self, request, pagename, attachment_name, size):
+        PageEvent.__init__(self, request)
+        self.request = request
+        self.pagename = pagename
+        self.attachment_name = attachment_name
+        self.size = size
+
+
+class PageRevertedEvent(PageEvent):
+    def __init__(self, request, pagename, previous, current):
+        PageEvent.__init__(self, request)
+        self.pagename = pagename
+        self.previous = previous
+        self.current = current    
+
+
+class SubscribedToPageEvent(PageEvent):
+    def __init__(self, request, pagename, username):
+        PageEvent.__init__(self, request)    
+        self.pagename = pagename
+        self.username = username
+
+
+class JabberIDSetEvent(Event):
+    """ Sent when user changes her Jabber ID """
+    def __init__(self, request, jid):
+        Event.__init__(self, request)
+        self.jid = jid
+        
+class JabberIDUnsetEvent(Event):
+    """ Sent when Jabber ID is no longer used
+    
+    Obviously this will be usually sent along with JabberIDSetEvent,
+    because we require user's jabber id to be unique by default.
+    """
+    def __init__(self, request, jid):
+        Event.__init__(self, request)
+        self.jid = jid
+        
+def _register_handlers(cfg):
+    """Create a list of available event handlers.
+    
+    Each handler is a handle() function defined in an plugin,
+    pretty much like in case of actions.
+    
+    TODO: maybe make it less dumb? ;-)"""
+    
+    global _event_handlers
+
+    _event_handlers = []
+    names = wikiutil.getPlugins("events", cfg)
+
+    for name in names:
+        try:
+            handler = wikiutil.importPlugin(cfg, "events", name, "handle")
+        except PluginAttributeError:
+            handler = None
+        
+        if handler is not None:
+            _event_handlers.append(handler)
+
+
+def send_event(event):
+    """Function called from outside to process an event"""
+   
+    # A list of messages generated by event handlers, passed back to caller
+    msg = []
+    
+    # Find all available event handlers
+    if _event_handlers is None:
+        _register_handlers(event.request.cfg)
+    
+    # Try to handle the event with each available handler (for now)
+    for handle in _event_handlers:
+        retval = handle(event)
+        if isinstance(retval, unicode):
+            msg.append(retval)
+            
+    return msg
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MoinMoin/events/emailnotify.py	Sun Jun 17 00:01:13 2007 +0200
@@ -0,0 +1,83 @@
+# -*- coding: iso-8859-1 -*-
+"""
+    MoinMoin - email notification plugin from event system
+
+    This code sends email notifications about page changes.
+    TODO: refactor it to handle separate events for page changes, creations, etc
+
+    @copyright: 2007 by Karol Nowak <grywacz@gmail.com>
+    @license: GNU GPL, see COPYING for details.
+"""
+
+from MoinMoin import user
+from MoinMoin.Page import Page
+from MoinMoin.mail import sendmail
+from MoinMoin.events import *
+from MoinMoin.events.notification_common import page_changed_notification
+
+
+def sendNotification(request, page, comment, emails, email_lang, revisions, trivial):
+    """ Send notification email for a single language.
+
+    @param comment: editor's comment given when saving the page
+    @param emails: list of email addresses
+    @param email_lang: language of email
+    @param revisions: revisions of this page (newest first!)
+    @param trivial: the change is marked as trivial
+    @rtype: int
+    @return: sendmail result
+    """
+    _ = request.getText
+    mailBody = page_changed_notification(request, page, comment, email_lang, revisions, trivial)
+
+    return sendmail.sendmail(request, emails,
+        _('[%(sitename)s] %(trivial)sUpdate of "%(pagename)s" by %(username)s', formatted=False) % {
+            'trivial': (trivial and _("Trivial ", formatted=False)) or "",
+            'sitename': page.cfg.sitename or "Wiki",
+            'pagename': page.page_name,
+            'username': page.uid_override or user.getUserIdentification(request),
+        },
+        mailBody, mail_from=page.cfg.mail_from)
+
+
+def notifySubscribers(request, page, comment, trivial):
+    """ Send email to all subscribers of given page.
+
+    @param comment: editor's comment given when saving the page
+    @param trivial: editor's suggestion that the change is trivial (Subscribers may ignore this)
+    @rtype: string
+    @return: message, indicating success or errors.
+    """
+    _ = request.getText
+    subscribers = page.getSubscribers(request, return_users=1, trivial=trivial)
+    
+    if subscribers:
+        # get a list of old revisions, and append a diff
+        revisions = page.getRevList()
+
+        # send email to all subscribers
+        results = [_('Status of sending notification mails:')]
+        for lang in subscribers:
+            emails = [u.email for u in subscribers[lang]]
+            names = [u.name for u in subscribers[lang]]
+            mailok, status = sendNotification(request, page, comment, emails, lang, revisions, trivial)
+            recipients = ", ".join(names)
+            results.append(_('[%(lang)s] %(recipients)s: %(status)s') % {
+                'lang': lang, 'recipients': recipients, 'status': status})
+
+        # Return mail sent results. Ignore trivial - we don't have
+        # to lie. If mail was sent, just tell about it.
+        return '<p>\n%s\n</p> ' % '<br>'.join(results)
+
+    # No mail sent, no message.
+    return ''
+
+
+def handle(event):
+    if not isinstance(event, PageChangedEvent):
+        return
+
+    if not event.request.cfg.mail_enabled:
+        return
+    
+    return notifySubscribers(event.request, event.page, event.comment, event.trivial)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MoinMoin/events/jabbernotify.py	Sun Jun 17 00:01:13 2007 +0200
@@ -0,0 +1,141 @@
+# -*- coding: iso-8859-1 -*-
+"""
+    MoinMoin - jabber notification plugin for event system
+
+    This code sends notifications using a separate daemon.
+
+    @copyright: 2007 by Karol Nowak <grywacz@gmail.com>
+    @license: GNU GPL, see COPYING for details.
+"""
+
+import xmlrpclib
+
+from MoinMoin.user import User, getUserList
+from MoinMoin.Page import Page
+
+import MoinMoin.events as ev
+from MoinMoin.events.messages import page_change_message
+
+# XML RPC Server object used to communicate with notification bot
+server = None
+
+
+def handle(event):
+    global server
+
+    cfg = event.request.cfg
+
+    # Check for desired event type and if notification bot is configured
+    if not cfg.jabber_enabled:
+        return
+    
+    # Create an XML RPC server object only if it doesn't exist
+    if server is None:
+        server = xmlrpclib.Server("http://" + cfg.bot_host)
+    
+    if isinstance(event, ev.PageChangedEvent):
+        return handle_page_changed(event)
+    elif isinstance(event, ev.JabberIDSetEvent) or isinstance(event, ev.JabberIDUnsetEvent):
+        return handle_jid_changed(event)
+    elif isinstance(event, ev.FileAttachedEvent):
+        return handle_file_attached(event)
+    elif isinstance(event, ev.PageDeletedEvent):
+        return handle_page_deleted(event)
+    
+
+def handle_jid_changed(event):
+    """ Handles events sent when user's JID changes """
+    
+    request = event.request
+    _ = request.getText
+    
+    try:
+        if isinstance(event, JabberIDSetEvent):
+            server.addJIDToRoster(request.cfg.secret, event.jid)
+        else:
+            server.removeJIDFromRoster(request.cfg.secret, event.jid)        
+                
+    except xmlrpclib.Error, err:
+        print _("XML RPC error: "), str(err)
+        return (0, _("Notifications not sent"))
+    except Exception, err:
+        print _("Low-level communication error: "), str(err)
+        return (0, _("Notifications not sent"))
+
+
+def handle_file_attached(event):
+    """Handles event sent when a file is attached to a page"""
+    
+    request = event.request
+    page = Page(request, event.pagename) 
+    
+    subscribers = page.getSubscribers(request, return_users=1)
+    page_change("attachment_added", request, page, subscribers, attach_name=event.attachment_name, attach_size=event.size)
+
+        
+def handle_page_changed(event):
+    """ Handles events related to page changes """
+    
+    request = event.request
+    page = event.page
+    
+    subscribers = page.getSubscribers(request, return_users=1, trivial=event.trivial)
+    page_change("page_changed", request, page, subscribers, revisions=page.getRevList(), comment=event.comment)
+    
+
+def handle_page_deleted(event):
+    """Handles event sent when a page is deleted"""
+    
+    request = event.request
+    page = event.page
+    
+    subscribers = page.getSubscribers(request, return_users=1)
+    page_change("page_deleted", request, page, subscribers)
+    
+
+def page_change(type, request, page, subscribers, **kwargs):
+    
+    _ = request.getText
+    
+    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 = page_change_message(type, request, page, lang, **kwargs)
+            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 send_notification(request, jids, message):
+    """ Send notifications for a single language.
+
+    @param comment: editor's comment given when saving the page
+    @param jids: list of Jabber IDs
+    @param message_lang: language of notification
+    @param revisions: revisions of this page (newest first!)
+    @param trivial: the change is marked as trivial
+    """
+    _ = request.getText
+    
+    for jid in jids:
+        # FIXME: stops sending notifications on first error
+        try:
+            server.send_notification(request.cfg.secret, jid, message)
+        except xmlrpclib.Error, err:
+            print _("XML RPC error: "), str(err)
+            return (0, _("Notifications not sent"))
+        except Exception, err:
+            print _("Low-level communication error: "), str(err)
+            return (0, _("Notifications not sent"))
+        
+    return (1, _("Notifications sent OK"))
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MoinMoin/events/messages.py	Sun Jun 17 00:01:13 2007 +0200
@@ -0,0 +1,104 @@
+# -*- 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
+
+class UnknownChangeType:
+    pass
+
+def page_change_message(msgtype, request, page, lang, **kwargs):
+    """Prepare a notification text for a page change of given type
+    
+    @param msgtype: a type of message to send (page_changed, attachment_added, ...)
+    @type msgtype: str or unicode
+    @param **kwargs: a dictionary of additional parameters, which depend on msgtype
+    
+    @return: a formatted, ready to send message
+    @rtype: unicode
+    
+    """
+    
+    _ = request.getText
+    page._ = lambda s, formatted=True, r=request, l=lang: r.getText(s, formatted=formatted, lang=l)
+    querystr = {}
+    
+    if msgtype == "page_changed": 
+        revisions = kwargs['revisions']
+        if len(kwargs['revisions']) >= 2:
+            querystr = {'action': 'diff',
+                    'rev2': str(revisions[0]),
+                    'rev1': str(revisions[1])}
+        
+    if msgtype == "attachment_added":
+        attachlink = request.getBaseURL() + \
+                        getAttachUrl(page.page_name, kwargs['attach_name'], request)
+        
+    pagelink = request.getQualifiedURL(page.url(request, querystr, relative=False))
+    
+    if msgtype == "page_changed":
+        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(),
+        }
+            
+        # 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)
+                
+    elif msgtype == "attachment_added":
+        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"
+        "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': kwargs['attach_name'],
+            'attach_size': kwargs['attach_size'],
+            'attach_get': attachlink,
+        }
+        
+    elif msgtype == "page_deleted":
+        messageBody = _("Dear wiki user,\n\n"
+            'You have subscribed to a wiki page "%(sitename)s" for change notification.\n\n'
+            "The following page has been deleted 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(),
+        }
+    else:
+        raise UnknownChangeType()
+    
+    if 'comment' in kwargs and kwargs['comment'] is not None:
+        messageBody = messageBody + \
+            _("The comment on the change is:\n%(comment)s", formatted=False) % {'comment': kwargs['comment']}
+            
+    return messageBody
+    
--- a/MoinMoin/request/__init__.py	Sat Jun 16 23:57:54 2007 +0200
+++ b/MoinMoin/request/__init__.py	Sun Jun 17 00:01:13 2007 +0200
@@ -661,6 +661,9 @@
 
         self._login_messages = login_msgs
         return user_obj
+    
+    def handle_jid_auth(self, jid):
+        return user.get_by_jabber_id(self, jid)
 
     def parse_cookie(self):
         try:
@@ -1127,6 +1130,8 @@
         self.html_formatter = Formatter(self)
         self.formatter = self.html_formatter
 
+        self.initTheme()
+
         action_name = self.action
         if action_name == 'xmlrpc':
             from MoinMoin import xmlrpc
@@ -1138,8 +1143,6 @@
 
         # parse request data
         try:
-            self.initTheme()
-
             # The last component in path_info is the page name, if any
             path = self.getPathinfo()
 
--- a/MoinMoin/user.py	Sat Jun 16 23:57:54 2007 +0200
+++ b/MoinMoin/user.py	Sun Jun 17 00:01:13 2007 +0200
@@ -41,13 +41,23 @@
     userlist = [f for f in files if user_re.match(f)]
     return userlist
 
-def get_by_email_address(request, email_address):
-    """ Searches for a user with a particular e-mail address and returns it. """
+def get_by_filter(request, filter_func):
+    """ Searches for an user with a given filter function """
     for uid in getUserList(request):
         theuser = User(request, uid)
-        if theuser.valid and theuser.email.lower() == email_address.lower():
+        if filter_func(theuser):
             return theuser
 
+def get_by_email_address(request, email_address):
+    """ Searches for an user with a particular e-mail address and returns it. """
+    filter_func = lambda user: user.valid and user.email.lower() == email_address.lower()
+    return get_by_filter(request, filter_func)
+        
+def get_by_jabber_id(request, jabber_id):
+    """ Searches for an user with a perticular jabber id and returns it. """
+    filter_func = lambda user: user.valid and user.jid.lower() == jabber_id.lower()
+    return get_by_filter(request, filter_func)
+
 def _getUserIdByKey(request, key, search):
     """ Get the user ID for a specified key/value pair.
 
@@ -733,6 +743,11 @@
         if pagename not in self.subscribed_pages:
             self.subscribed_pages.append(pagename)
             self.save()
+            
+            # Send a notification
+            from MoinMoin.events import SubscribedToPageEvent, send_event
+            e = SubscribedToPageEvent(self._request, pagename, self.name)
+            send_event(e)
             return True
 
         return False
--- a/MoinMoin/userform.py	Sat Jun 16 23:57:54 2007 +0200
+++ b/MoinMoin/userform.py	Sun Jun 17 00:01:13 2007 +0200
@@ -9,6 +9,7 @@
 
 import time
 from MoinMoin import user, util, wikiutil
+from MoinMoin.events import send_event, JabberIDSetEvent, JabberIDUnsetEvent
 from MoinMoin.widget import html
 
 _debug = 0
@@ -226,13 +227,28 @@
 
             # Email should be unique - see also MoinMoin/script/accounts/moin_usercheck.py
             if theuser.email and self.request.cfg.user_email_unique:
-                users = user.getUserList(self.request)
-                for uid in users:
-                    if uid == theuser.id:
-                        continue
-                    thisuser = user.User(self.request, uid, auth_method='userform:283')
-                    if thisuser.email == theuser.email:
-                        return _("This email already belongs to somebody else.")
+                other = user.get_by_email_address(self.request, theuser.email)
+                if other is not None and other.id != theuser.id:
+                    return _("This email already belongs to somebody else.")
+                    
+        if not 'jid' in theuser.auth_attribs:
+            # try to get the jid
+            jid = wikiutil.clean_input(form.get('jid', [theuser.jid])[0]).strip()
+            
+            jid_changed = theuser.jid != jid
+            previous_jid = theuser.jid           
+            theuser.jid = jid
+            
+            if theuser.jid and self.request.cfg.user_jid_unique:
+                other = user.get_by_jabber_id(self.request, theuser.jid)
+                if other is not None and other.id != theuser.id:
+                    return _("This jabber id already belongs to somebody else.")
+            
+            if jid_changed:
+                set_event = JabberIDSetEvent(self.request, theuser.jid)
+                unset_event = JabberIDUnsetEvent(self.request, previous_jid)
+                send_event(set_event)
+                send_event(unset_event)
 
         if not 'aliasname' in theuser.auth_attribs:
             # aliasname
@@ -284,7 +300,7 @@
         already_handled = ['name', 'password', 'password2', 'email',
                            'aliasname', 'edit_rows', 'editor_default',
                            'editor_ui', 'tz_offset', 'datetime_fmt',
-                           'theme_name', 'language']
+                           'theme_name', 'language', 'jid']
         for field in self.cfg.user_form_fields:
             key = field[0]
             if ((key in self.cfg.user_form_disable)
@@ -621,6 +637,9 @@
 
         if self.cfg.mail_enabled:
             buttons.append(("account_sendmail", _('Mail me my account data')))
+            
+        if self.cfg.jabber_enabled:
+            buttons.append(("account_sendjabber", _('Send me my account data with Jabber')))
 
         if create_only:
             buttons = [("create_only", _('Create Profile'))]
@@ -731,6 +750,7 @@
         #Column('id', label=('ID'), align='right'),
         Column('name', label=('Username')),
         Column('email', label=('Email')),
+        Column('jabber', label=('Jabber')),
         Column('action', label=_('Action')),
     ]
 
@@ -750,12 +770,21 @@
             (request.formatter.url(1, 'mailto:' + account.email, css='mailto', do_escape=0) +
              request.formatter.text(account.email) +
              request.formatter.url(0)),
+            (request.formatter.url(1, 'xmpp:' + account.jid, css='mailto', do_escape=0) +
+             request.formatter.text(account.jid) +
+             request.formatter.url(0)),
             request.page.link_to(request, text=_('Mail me my account data'),
                                  querystr={"action":"userform",
                                            "email": account.email,
                                            "account_sendmail": "1",
                                            "sysadm": "users", },
-                                 rel='nofollow')
+                                 rel='nofollow'),
+            request.page.link_to(request, text=_('Send me my accound data with Jabber'),
+                                 querystr={"action":"userform",
+                                           "jid": account.jid,
+                                           "account_sendjabber": "1",
+                                           "sysadm": "users", },
+                                  rel='nofollow'),
         ))
 
     if data:
--- a/MoinMoin/xmlrpc/__init__.py	Sat Jun 16 23:57:54 2007 +0200
+++ b/MoinMoin/xmlrpc/__init__.py	Sun Jun 17 00:01:13 2007 +0200
@@ -201,17 +201,21 @@
             try:
                 # XXX A marshalling error in any response will fail the entire
                 # multicall. If someone cares they should fix this.
-                results.append([self.dispatch(method_name, params)])
-            except xmlrpclib.Fault, fault:
-                results.append(
-                    {'faultCode': fault.faultCode,
-                     'faultString': fault.faultString}
-                    )
+                result = self.dispatch(method_name, params)
+                
+                if not isinstance(result, xmlrpclib.Fault):
+                    results.append([result])
+                else:
+                    results.append(
+                        {'faultCode': result.faultCode,
+                         'faultString': result.faultString}
+                        )
             except:
                 results.append(
                     {'faultCode': 1,
                      'faultString': "%s:%s" % (sys.exc_type, sys.exc_value)}
                     )
+                
         return results
 
     #############################################################################
@@ -587,42 +591,70 @@
             return userdata
 
     # authorization methods
+    
+    def _cleanup_stale_tokens(request):
+        items = caching.get_cache_list(request, 'xmlrpc-session', 'farm')
+        tnow = time.time()
+        for item in items:
+            centry = caching.CacheEntry(self.request, 'xmlrpc-session', item,
+                                        scope='farm', use_pickle=True)
+            try:
+                expiry, uid = centry.content()
+                if expiry < tnow:
+                    centry.remove()
+            except caching.CacheError:
+                pass
+            
+    def _generate_auth_token(self):
+        token = random_string(32, 'abcdefghijklmnopqrstuvwxyz0123456789')
+        centry = caching.CacheEntry(self.request, 'xmlrpc-session', token,
+                                    scope='farm', use_pickle=True)
+        centry.update((time.time() + 15*3600, u.id))
+        return token
 
     def xmlrpc_getAuthToken(self, username, password, *args):
         """ Returns a token which can be used for authentication
             in other XMLRPC calls. If the token is empty, the username
             or the password were wrong. """
 
-        def _cleanup_stale_tokens(request):
-            items = caching.get_cache_list(request, 'xmlrpc-session', 'farm')
-            tnow = time.time()
-            for item in items:
-                centry = caching.CacheEntry(self.request, 'xmlrpc-session', item,
-                                            scope='farm', use_pickle=True)
-                try:
-                    expiry, uid = centry.content()
-                    if expiry < tnow:
-                        centry.remove()
-                except caching.CacheError:
-                    pass
-
         if randint(0, 99) == 0:
             _cleanup_stale_tokens(self.request)
 
         u = self.request.handle_auth(None, username=username,
                                      password=password, login=True)
+        
         if u and u.valid:
-            token = random_string(32, 'abcdefghijklmnopqrstuvwxyz0123456789')
-            centry = caching.CacheEntry(self.request, 'xmlrpc-session', token,
-                                        scope='farm', use_pickle=True)
-            centry.update((time.time() + 15*3600, u.id))
-            return token
+            return _generate_auth_token()
         else:
             return ""
+        
+    def xmlrpc_getJabberAuthToken(self, jid, secret):
+        """Returns a token which can be used for authentication.
+        
+        This token can be used in other XMLRPC calls. Generation of
+        token depends on user's JID and a secret shared between wiki
+        and Jabber bot.
+        
+        @param jid: a bare Jabber ID
+        
+        """
+        
+        if self.cfg.secret != secret:
+            return ""
+        
+        if randint(0, 99) == 0:
+            _cleanup_stale_tokens(self.request)
+            
+        u = self.request.handle_jid_auth(jid)
+        
+        if u and u.valid:
+            return _generate_auth_token()
+        else:
+            return ""            
 
     def xmlrpc_applyAuthToken(self, auth_token):
         """ Applies the auth token and thereby authenticates the user. """
-        centry = caching.CacheEntry(self.request, 'xmlrpc-session', token,
+        centry = caching.CacheEntry(self.request, 'xmlrpc-session', auth_token,
                                     scope='farm', use_pickle=True)
         try:
             expiry, uid = centry.content()
@@ -644,7 +676,7 @@
 
     def xmlrpc_deleteAuthToken(self, auth_token):
         """ Delete the given auth token. """
-        centry = caching.CacheEntry(self.request, 'xmlrpc-session', token,
+        centry = caching.CacheEntry(self.request, 'xmlrpc-session', auth_token,
                                     scope='farm', use_pickle=True)
         try:
             centry.remove()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/jabberbot/__init__.py	Sun Jun 17 00:01:13 2007 +0200
@@ -0,0 +1,7 @@
+# -*- coding: iso-8859-1 -*-
+"""
+    MoinMoin - jabber bot
+
+    @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	Sun Jun 17 00:01:13 2007 +0200
@@ -0,0 +1,84 @@
+# -*- 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
+
+# XMPP <-> XML RPC commands
+# These commands are passed in both directions, with added data
+# payload when they return to the XMPP code. Naming convention
+# follows method names defined by the Wiki RPC Interface v2.
+
+class BaseDataCommand(object):
+    """Base class for all commands used by the XMPP component.
+    
+    It has to support an optional data payload and store JID the
+    request has come from and provide a help string for its parameters.
+    """
+    
+    # Description of what the command does
+    description = u""
+    
+    # Parameter list in a human-readable format
+    parameter_list = u""
+    
+    def __init__(self, jid):
+        self.jid = jid
+        self.data = None
+        
+class GetPage(BaseDataCommand):
+    
+    description = u"retrieve raw content of a named page"
+    parameter_list = u"pagename"
+    
+    def __init__(self, jid, pagename):
+        BaseDataCommand.__init__(self, jid)
+        self.pagename = pagename
+        
+class GetPageHTML(BaseDataCommand):
+    
+    description = u"retrieve HTML-formatted content of a named page"
+    parameter_list = u"pagename"
+    
+    def __init__(self, jid, pagename):
+        BaseDataCommand.__init__(self, jid)
+        self.pagename = pagename
+        
+class GetPageList(BaseDataCommand):
+    
+    description = u"get a list of accesible pages"
+    parameter_list = u""
+    
+    def __init__(self, jid):
+        BaseDataCommand.__init__(self, jid)
+
+class GetPageInfo(BaseDataCommand):
+    
+    description = u"show detailed information about a page"
+    parameter_list = u"pagename"
+    
+    def __init__(self, jid, pagename):
+        BaseDataCommand.__init__(self, jid)
+        self.pagename = pagename
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/jabberbot/main.py	Sun Jun 17 00:01:13 2007 +0200
@@ -0,0 +1,48 @@
+# -*- coding: iso-8859-1 -*-
+"""
+    MoinMoin - jabber bot main file
+
+    @copyright: 2007 by Karol Nowak <grywacz@gmail.com>
+    @license: GNU GPL, see COPYING for details.
+"""
+
+import sys
+import os
+from Queue import Queue
+
+from jabberbot.config import BotConfig
+from jabberbot.xmppbot import XMPPBot
+from jabberbot.xmlrpcbot import XMLRPCServer, XMLRPCClient
+
+
+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(BotConfig, commands_from_xmpp, commands_to_xmpp)
+        xmlrpc_client = XMLRPCClient(BotConfig, commands_from_xmpp, commands_to_xmpp)
+        xmlrpc_server = XMLRPCServer(BotConfig, 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/multicall.py	Sun Jun 17 00:01:13 2007 +0200
@@ -0,0 +1,70 @@
+""" XMLRPC MultiCall support for Python 2.3. Copied from xmlrpclib.py of Python 2.4.3. """
+
+try:
+    from xmlrpclib import MultiCall
+except ImportError: 
+    from xmlrpclib import Fault
+    
+    class _MultiCallMethod:
+        # some lesser magic to store calls made to a MultiCall object
+        # for batch execution
+        def __init__(self, call_list, name):
+            self.__call_list = call_list
+            self.__name = name
+        def __getattr__(self, name):
+            return _MultiCallMethod(self.__call_list, "%s.%s" % (self.__name, name))
+        def __call__(self, *args):
+            self.__call_list.append((self.__name, args))
+    
+    class MultiCallIterator:
+        """Iterates over the results of a multicall. Exceptions are
+        thrown in response to xmlrpc faults."""
+    
+        def __init__(self, results):
+            self.results = results
+    
+        def __getitem__(self, i):
+            item = self.results[i]
+            if type(item) == type({}):
+                raise Fault(item['faultCode'], item['faultString'])
+            elif type(item) == type([]):
+                return item[0]
+            else:
+                raise ValueError,\
+                      "unexpected type in multicall result"
+    
+    class MultiCall:
+        """server -> a object used to boxcar method calls
+    
+        server should be a ServerProxy object.
+    
+        Methods can be added to the MultiCall using normal
+        method call syntax e.g.:
+    
+        multicall = MultiCall(server_proxy)
+        multicall.add(2,3)
+        multicall.get_address("Guido")
+    
+        To execute the multicall, call the MultiCall object e.g.:
+    
+        add_result, address = multicall()
+        """
+    
+        def __init__(self, server):
+            self.__server = server
+            self.__call_list = []
+    
+        def __repr__(self):
+            return "<MultiCall at %x>" % id(self)
+    
+        __str__ = __repr__
+    
+        def __getattr__(self, name):
+            return _MultiCallMethod(self.__call_list, name)
+    
+        def __call__(self):
+            marshalled_list = []
+            for name, args in self.__call_list:
+                marshalled_list.append({'methodName' : name, 'params' : args})
+    
+            return MultiCallIterator(self.__server.system.multicall(marshalled_list))
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/jabberbot/xmlrpcbot.py	Sun Jun 17 00:01:13 2007 +0200
@@ -0,0 +1,257 @@
+# -*- coding: iso-8859-1 -*-
+"""
+    MoinMoin - a xmlrpc server and client for the notification bot
+
+    @copyright: 2007 by Karol Nowak <grywacz@gmail.com>
+    @license: GNU GPL, see COPYING for details.
+"""
+
+import Queue
+import time, xmlrpclib
+from threading import Thread
+from SimpleXMLRPCServer import SimpleXMLRPCServer
+
+import jabberbot.commands as cmd
+from jabberbot.multicall import MultiCall
+
+
+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_in, commands_out):
+        """
+        @param commands: an output command queue
+        """
+        Thread.__init__(self)
+        self.commands_in = commands_in
+        self.commands_out = commands_out
+        self.config = config
+        self.url = config.wiki_url + "?action=xmlrpc2"
+        self.connection = self.create_connection()
+        self.token = None
+        self.multicall = None
+
+    def run(self):
+        """Starts the server / thread"""
+        while True:
+            try:
+                command = self.commands_in.get(True, 2)
+                self.execute_command(command)
+            except Queue.Empty:
+                pass
+            
+    def create_connection(self):
+        return xmlrpclib.ServerProxy(self.url, allow_none=True, verbose=self.config.verbose)
+                
+    def execute_command(self, command):
+        """Execute commands coming from the XMPP component"""
+        
+        # FIXME: make this kind of automatic
+        if isinstance(command, cmd.GetPage):
+            self.get_page(command)
+        elif isinstance(command, cmd.GetPageHTML):
+            self.get_page_html(command)
+        elif isinstance(command, cmd.GetPageList):
+            self.get_page_list(command)
+        elif isinstance(command, cmd.GetPageInfo):
+            self.get_page_info(command)
+    
+    def get_auth_token(self, jid):
+        token = self.connection.getAuthToken(jid, self.config.secret)
+        if token:
+            self.token = token
+    
+    def _xmlrpc_decorator(function):
+        """A decorator function, which adds some maintenance code
+        
+        This function takes care of preparing a MultiCall object and
+        an authentication token, and deleting them at the end.
+        
+        """
+        def wrapped_func(self, command):
+            self.token = None
+            self.multicall = MultiCall(self.connection)
+            self.get_auth_token(command.jid)
+            
+            if self.token:
+                self.multicall.applyAuthToken(self.token)
+   
+            try:
+                try:
+                    function(self, command)
+                    self.commands_out.put_nowait(command)
+                except xmlrpclib.Fault, fault:
+                    msg = u"""Your request has failed. The reason is:\n%s"""
+                    notification = cmd.NotificationCommand(command.jid, msg % (fault.faultString, ))
+                    self.commands_out.put_nowait(notification)
+            finally:
+                del self.token
+                del self.multicall
+                
+        return wrapped_func
+    
+    def get_page(self, command):
+        """Returns a raw page"""
+        
+        self.multicall.getPage(command.pagename)
+        
+        if not self.token:
+            # FIXME: notify the user that he may not have full rights on the wiki
+            getpage_result = self.multicall()
+        else:
+            getpage_result, token_result = self.multicall()
+
+        # FIXME: warn if token turned out being wrong
+        command.data = getpage_result[0]
+            
+    get_page = _xmlrpc_decorator(get_page)
+        
+        
+    def get_page_html(self, command):
+        """Returns a html-formatted page"""
+        
+        self.multicall.getPageHTML(command.pagename)
+        
+        if not self.token:
+            # FIXME: notify the user that he may not have full rights on the wiki    
+            getpagehtml_result = self.multicall()
+        else:
+            token_result, getpagehtml_result = self.multicall()
+
+        # FIXME: warn if token turned out being wrong
+        command.data = getpagehtml_result[0]
+            
+    get_page_html = _xmlrpc_decorator(get_page_html)
+        
+        
+    def get_page_list(self, command):
+        """Returns a list of all accesible pages"""
+        
+        txt = u"""This command may take a while to complete, please be patient..."""
+        info = cmd.NotificationCommand(command.jid, txt)
+        self.commands_out.put_nowait(info)
+        
+        self.multicall.getAllPages()
+        
+        if not self.token:
+            # FIXME: notify the user that he may not have full rights on the wiki
+            getpagelist_result = self.multicall()
+        else:
+            token_result, getpagelist_result = self.multicall()
+
+        # FIXME: warn if token turned out being wrong
+        command.data = getpagelist_result[0]
+            
+    get_page_list = _xmlrpc_decorator(get_page_list)
+    
+    
+    def get_page_info(self, command):
+        """Returns detailed information about a given page"""
+        
+        self.multicall.getPageInfo(command.pagename)
+        
+        if not self.token:
+            # FIXME: notify the user that he may not have full rights on the wiki
+            getpageinfo_result = self.multicall()
+        else:
+            token_result, getpageinfo_result = self.multicall()
+        
+        # FIXME: warn if token turned out being wrong
+        command.data = getpageinfo_result[0]
+        
+    get_page_info = _xmlrpc_decorator(get_page_info)
+
+
+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
+        
+        @param jid: a jid to send a message to (bare jid)
+        @type jid: str or unicode
+        @param text: a message body
+        @type text: unicode
+        
+        """
+        command = cmd.NotificationCommand(jid, text)
+        self.commands.put_nowait(command)
+        return True
+    send_notification.export = True
+    
+    def addJIDToRoster(self, jid):
+        """Instructs the XMPP component to add a new JID to its roster
+        
+        @param jid: a jid to add, this must be a bare jid
+        @type jid: str or unicode, 
+        
+        """  
+        command = cmd.AddJIDToRosterCommand(jid)
+        self.commands.put_nowait(command)
+        return True
+    addJIDToRoster.export = True
+    
+    def removeJIDFromRoster(self, jid):
+        """Instructs the XMPP component to remove a JID from its roster
+        
+        @param jid: a jid to remove, this must be a bare jid
+        @type jid: str or unicode
+        
+        """
+        command = cmd.RemoveJIDFromRosterCommand(jid)
+        self.commands.put_nowait(command)
+        return True
+    removeJIDFromRoster.export = True
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/jabberbot/xmppbot.py	Sun Jun 17 00:01:13 2007 +0200
@@ -0,0 +1,532 @@
+# -*- coding: iso-8859-1 -*-
+"""
+    MoinMoin - jabber bot
+
+    @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
+
+import jabberbot.commands as cmd
+
+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
+        
+        @param resource: resource name
+        @param show: a show presence property, as defined in XMPP
+        @param priority: priority of the given resource
+        
+        """
+        self.resources[resource] = {'show': show, 'priority': priority}
+    
+    def remove_resource(self, resource):
+        """Removes information about a connected resource
+        
+        @param resource: resource name
+        
+        """
+        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']
+                
+        return max_prio_show == u'dnd'
+        
+    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 = { }
+
+        self.known_xmlrpc_cmds = [cmd.GetPage, cmd.GetPageHTML, cmd.GetPageList, cmd.GetPageInfo] 
+        self.internal_commands = ["ping", "help"]
+        
+        self.xmlrpc_commands = {}
+        for command, name in [(command, command.__name__) for command in self.known_xmlrpc_cmds]:
+            self.xmlrpc_commands[name] = command
+        
+        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
+        
+        @param command: a command to execute
+        @type command: any class defined in commands.py (FIXME?)
+        @param ignore_dnd: if command results in user interaction, should DnD be ignored?
+        
+        """
+        # Handle normal notifications
+        if isinstance(command, cmd.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, cmd.AddJIDToRosterCommand):
+            jid = JID(node_or_jid=command.jid)
+            self.ask_for_subscription(jid)
+            
+        elif isinstance(command, cmd.RemoveJIDFromRosterCommand):
+            jid = JID(node_or_jid=command.jid)
+            self.remove_subscription(jid)
+            
+        elif isinstance(command, cmd.GetPage) or isinstance(command, cmd.GetPageHTML):
+            msg = u"""Here's the page "%s" that you've requested:\n\n%s"""
+            self.send_message(command.jid, msg % (command.pagename, command.data))
+        
+        elif isinstance(command, cmd.GetPageList):
+            msg = u"""That's the list of pages accesible to you:\n\n%s"""
+            pagelist = "\n".join(command.data)
+            self.send_message(command.jid, msg % (pagelist, ))
+            
+        elif isinstance(command, cmd.GetPageInfo):
+            msg = u"""Here's some more detailed information on page "%s":\n\n%s"""
+            self.send_message(command.jid, msg % (command.pagename, command.data))
+            
+    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
+        
+        @param message: a message stanza to parse
+        @type message: pyxmpp.message.Message
+        
+        """    
+        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 not command:
+            return
+        
+        if command[0] in self.internal_commands:
+            response = self.handle_internal_command(command)
+        elif command[0] in self.xmlrpc_commands.keys():
+            response = self.handle_xmlrpc_command(sender, command)
+        else:
+            response = self.reply_help()
+        
+        if not response == u"":
+            self.send_message(sender, response)
+            
+    def handle_internal_command(self, command):
+        """Handles internal commands, that can be completed by the XMPP bot itself
+        
+        @param command: list representing a command
+        
+        """
+        if command[0] == "ping":
+            return "pong"
+        elif command[0] == "help":
+            if len(command) == 1:
+                return self.reply_help()
+            else:
+                return self.help_on(command[1])
+        else:
+            # For unknown command return a generic help message
+            return self.reply_help()
+        
+    def help_on(self, command):
+        """Returns a help message on a given topic
+        
+        @param command: a command to describe in a help message
+        @type command: str or unicode
+        @return: a help message
+        
+        """
+        if command == "help":
+            return u"""The "help" command prints a short, helpful message about a given topic or function.\n\nUsage: help [topic_or_function]"""
+        
+        elif command == "ping":
+            return u"""The "ping" command returns a "pong" message as soon as it's received."""
+        
+        # Here we have to deal with help messages of external (xmlrpc) commands
+        else:
+            classobj = self.xmlrpc_commands[command]
+            help_str = u"%s - %s\n\nUsage: %s %s"
+            return help_str % (command, classobj.description, command, classobj.parameter_list)
+        
+        
+    def handle_xmlrpc_command(self, sender, command):
+        """Creates a command object, and puts it the command queue
+        
+        @param command: a valid name of available xmlrpc command
+        @type command: list representing a command, name and parameters
+        
+        """
+        command_class = self.xmlrpc_commands[command[0]]
+        
+        # Add sender's JID to the argument list
+        command.insert(1, sender.as_utf8())
+        
+        try:
+            instance = command_class.__new__(command_class)
+            instance.__init__(*command[1:])
+            self.from_commands.put_nowait(instance)
+            
+        # This happens when user specifies wrong parameters
+        except TypeError:
+            msg = u"You've specified a wrong parameter list. The call should look like:\n\n%s %s"
+            return msg % (command[0], command_class.parameter_list)
+            
+    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
+        
+        @type stanza: pyxmpp.presence.Presence
+        
+        """
+        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
+        
+        @type presence: pyxmpp.presence.Presence
+        
+        """
+        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
+        
+        @param contact: a contact whose queued messages are to be sent
+        @type contact: jabberbot.xmppbot.Contact
+        @param ignore_dnd: should contact's DnD status be ignored?
+        
+        """
+        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.
+        
+        """
+        msg = u"""Hello there! I'm a MoinMoin Notification Bot. Available commands:\n\n%s\n%s"""
+        internal = ", ".join(self.internal_commands)
+        xmlrpc = ", ".join(self.xmlrpc_commands.keys())
+        
+        return msg % (internal, xmlrpc)
+    
+    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.")
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/wiki/config/more_samples/jabber_wikiconfig.py	Sun Jun 17 00:01:13 2007 +0200
@@ -0,0 +1,177 @@
+# -*- coding: iso-8859-1 -*-
+# IMPORTANT! This encoding (charset) setting MUST be correct! If you live in a
+# western country and you don't know that you use utf-8, you probably want to
+# use iso-8859-1 (or some other iso charset). If you use utf-8 (a Unicode
+# encoding) you MUST use: coding: utf-8
+# That setting must match the encoding your editor uses when you modify the
+# settings below. If it does not, special non-ASCII chars will be wrong.
+
+"""
+    MoinMoin - Configuration for a single wiki
+
+    This is a sample configuration for a wiki which uses a Jabber
+    notification bot. Note that the bot has to be started separately
+    (as of the time of writing this) and has its own configuration. Make
+    sure that a shared secret is set to the same (long) string in both
+    configs! The notification bot specific options are at the bottom.
+
+    If you run a single wiki only, you can omit the farmconfig.py config
+    file and just use wikiconfig.py - it will be used for every request
+    we get in that case.
+
+    Note that there are more config options than you'll find in
+    the version of this file that is installed by default; see
+    the module MoinMoin.config.multiconfig for a full list of names and their
+    default values.
+
+    Also, the URL http://moinmoin.wikiwikiweb.de/HelpOnConfiguration has
+    a list of config options.
+
+    ** Please do not use this file for a wiki farm. Use the sample file 
+    from the wikifarm directory instead! **
+"""
+
+from MoinMoin.config.multiconfig import DefaultConfig
+
+
+class Config(DefaultConfig):
+
+    # Wiki identity ----------------------------------------------------
+
+    # Site name, used by default for wiki name-logo [Unicode]
+    sitename = u'Untitled Wiki'
+
+    # Wiki logo. You can use an image, text or both. [Unicode]
+    # For no logo or text, use '' - the default is to show the sitename.
+    # See also url_prefix setting below!
+    logo_string = u'<img src="/wiki/common/moinmoin.png" alt="MoinMoin Logo">'
+
+    # name of entry page / front page [Unicode], choose one of those:
+
+    # a) if most wiki content is in a single language
+    #page_front_page = u"MyStartingPage"
+
+    # b) if wiki content is maintained in many languages
+    #page_front_page = u"FrontPage"
+
+    # The interwiki name used in interwiki links
+    #interwikiname = 'UntitledWiki'
+    # Show the interwiki name (and link it to page_front_page) in the Theme,
+    # nice for farm setups or when your logo does not show the wiki's name.
+    #show_interwiki = 1
+
+
+    # Critical setup  ---------------------------------------------------
+
+    # Misconfiguration here will render your wiki unusable. Check that
+    # all directories are accessible by the web server or moin server.
+
+    # If you encounter problems, try to set data_dir and data_underlay_dir
+    # to absolute paths.
+
+    # Where your mutable wiki pages are. You want to make regular
+    # backups of this directory.
+    data_dir = './data/'
+
+    # Where read-only system and help page are. You might want to share
+    # this directory between several wikis. When you update MoinMoin,
+    # you can safely replace the underlay directory with a new one. This
+    # directory is part of MoinMoin distribution, you don't have to
+    # backup it.
+    data_underlay_dir = './underlay/'
+
+    # The URL prefix we use to access the static stuff (img, css, js).
+    # NOT touching this is maybe the best way to handle this setting as moin
+    # uses a good internal default (something like '/moin_static160' for moin
+    # version 1.6.0).
+    # For Twisted and standalone server, the default will automatically work.
+    # For others, you should make a matching server config (e.g. an Apache
+    # Alias definition pointing to the directory with the static stuff).
+    #url_prefix_static = '/moin_static160'
+
+
+    # Security ----------------------------------------------------------
+
+    # This is checked by some rather critical and potentially harmful actions,
+    # like despam or PackageInstaller action:
+    #superuser = [u"YourName", ]
+
+    # IMPORTANT: grant yourself admin rights! replace YourName with
+    # your user name. See HelpOnAccessControlLists for more help.
+    # All acl_rights_xxx options must use unicode [Unicode]
+    #acl_rights_before = u"YourName:read,write,delete,revert,admin"
+
+    # Link spam protection for public wikis (Uncomment to enable)
+    # Needs a reliable internet connection.
+    #from MoinMoin.security.antispam import SecurityPolicy
+
+
+    # Mail --------------------------------------------------------------
+
+    # Configure to enable subscribing to pages (disabled by default)
+    # or sending forgotten passwords.
+
+    # SMTP server, e.g. "mail.provider.com" (None to disable mail)
+    #mail_smarthost = ""
+
+    # The return address, e.g u"Jürgen Wiki <noreply@mywiki.org>" [Unicode]
+    #mail_from = u""
+
+    # "user pwd" if you need to use SMTP AUTH
+    #mail_login = ""
+
+
+    # User interface ----------------------------------------------------
+
+    # Add your wikis important pages at the end. It is not recommended to
+    # remove the default links.  Leave room for user links - don't use
+    # more than 6 short items.
+    # You MUST use Unicode strings here, but you need not use localized
+    # page names for system and help pages, those will be used automatically
+    # according to the user selected language. [Unicode]
+    navi_bar = [
+        # If you want to show your page_front_page here:
+        #u'%(page_front_page)s',
+        u'RecentChanges',
+        u'FindPage',
+        u'HelpContents',
+    ]
+
+    # The default theme anonymous or new users get
+    theme_default = 'modern'
+
+
+    # Language options --------------------------------------------------
+
+    # See http://moinmoin.wikiwikiweb.de/ConfigMarket for configuration in 
+    # YOUR language that other people contributed.
+
+    # The main wiki language, set the direction of the wiki pages
+    language_default = 'en'
+
+    # You must use Unicode strings here [Unicode]
+    page_category_regex = u'^Category[A-Z]'
+    page_dict_regex = u'[a-z]Dict$'
+    page_form_regex = u'[a-z]Form$'
+    page_group_regex = u'[a-z]Group$'
+    page_template_regex = u'[a-z]Template$'
+
+    # Content options ---------------------------------------------------
+
+    # Show users hostnames in RecentChanges
+    show_hosts = 1                  
+
+    # Enable graphical charts, requires gdchart.
+    #chart_options = {'width': 600, 'height': 300}
+
+    # Notification bot options ------------------------------------------
+    
+    # Host and port on which the notification bot runs
+    bot_host = u"localhost:8000"
+    
+    # A secret shared with notification bot, must be the same in both
+    # configs for communication to work.
+    #
+    # CHANGE IT TO A LONG RANDOM STRING, OR YOU WILL HAVE A SECURITY ISSUE!
+    secret = u"8yFAS(E-,.c-93adj'uff;3AW#(UDJ,.df3OA($HG"
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/wiki/data/plugin/events/__init__.py	Sun Jun 17 00:01:13 2007 +0200
@@ -0,0 +1,5 @@
+# -*- coding: iso-8859-1 -*-
+
+from MoinMoin.util import pysupport
+
+modules = pysupport.getPackageModules(__file__)