Back to index

moin  1.9.0~rc2
xmlrpcbot.py
Go to the documentation of this file.
00001 # -*- coding: iso-8859-1 -*-
00002 """
00003     MoinMoin - a xmlrpc server and client for the notification bot
00004 
00005     @copyright: 2007 by Karol Nowak <grywacz@gmail.com>
00006     @license: GNU GPL, see COPYING for details.
00007 """
00008 
00009 import logging, xmlrpclib, Queue
00010 from SimpleXMLRPCServer import SimpleXMLRPCServer
00011 from threading import Thread
00012 
00013 import jabberbot.commands as cmd
00014 
00015 
00016 class ConfigurationError(Exception):
00017 
00018     def __init__(self, message):
00019         Exception.__init__(self)
00020         self.message = message
00021 
00022 
00023 def _xmlrpc_decorator(function):
00024     """A decorator function, which adds some maintenance code
00025 
00026     This function takes care of preparing a MultiCall object and
00027     an authentication token, and deleting them at the end.
00028 
00029     """
00030     def wrapped_func(self, command):
00031         # Dummy function, so that the string appears in a .po file
00032         _ = lambda x: x
00033 
00034         self.token = None
00035         self.multicall = xmlrpclib.MultiCall(self.connection)
00036         jid = command.jid
00037         if type(jid) is not list:
00038             jid = [jid]
00039 
00040         try:
00041             try:
00042                 self.get_auth_token(command.jid)
00043                 if self.token:
00044                     self.multicall.applyAuthToken(self.token)
00045 
00046                 function(self, command)
00047                 self.commands_out.put_nowait(command)
00048 
00049             except xmlrpclib.Fault, fault:
00050                 msg = _("Your request has failed. The reason is:\n%(error)s")
00051                 self.log.error(str(fault))
00052                 self.report_error(jid, msg, {'error': fault.faultString})
00053             except xmlrpclib.Error, err:
00054                 msg = _("A serious error occured while processing your request:\n%(error)s")
00055                 self.log.error(str(err))
00056                 self.report_error(jid, msg, {'error': str(err)})
00057             except Exception, exc:
00058                 msg = _("An internal error has occured, please contact the administrator.")
00059                 self.log.critical(str(exc))
00060                 self.report_error(jid, msg)
00061 
00062         finally:
00063             del self.token
00064             del self.multicall
00065 
00066     return wrapped_func
00067 
00068 class XMLRPCClient(Thread):
00069     """XMLRPC Client
00070 
00071     It's responsible for performing XMLRPC operations on
00072     a wiki, as inctructed by command objects received from
00073     the XMPP component"""
00074 
00075     def __init__(self, config, commands_in, commands_out):
00076         """A constructor
00077 
00078         @param commands_out: an output command queue (to xmpp)
00079         @param commands_in: an input command queue (from xmpp)
00080 
00081         """
00082         Thread.__init__(self)
00083         self.log = logging.getLogger(__name__)
00084 
00085         if not config.secret:
00086             error = "You must set a (long) secret string!"
00087             self.log.critical(error)
00088             raise ConfigurationError(error)
00089 
00090         self.commands_in = commands_in
00091         self.commands_out = commands_out
00092         self.config = config
00093         self.url = config.wiki_url + "?action=xmlrpc2"
00094         self.connection = self.create_connection()
00095         self.token = None
00096         self.multicall = None
00097         self.stopping = False
00098 
00099         self._cmd_handlers = {cmd.GetPage: self.get_page,
00100                               cmd.GetPageHTML: self.get_page_html,
00101                               cmd.GetPageList: self.get_page_list,
00102                               cmd.GetPageInfo: self.get_page_info,
00103                               cmd.GetUserLanguage: self.get_language_by_jid,
00104                               cmd.Search: self.do_search,
00105                               cmd.RevertPage: self.do_revert}
00106 
00107     def run(self):
00108         """Starts the server / thread"""
00109         while True:
00110             if self.stopping:
00111                 break
00112 
00113             try:
00114                 command = self.commands_in.get(True, 2)
00115                 self.execute_command(command)
00116             except Queue.Empty:
00117                 pass
00118 
00119     def stop(self):
00120         """Stop the thread"""
00121         self.stopping = True
00122 
00123     def create_connection(self):
00124         return xmlrpclib.ServerProxy(self.url, allow_none=True, verbose=self.config.verbose)
00125 
00126     def execute_command(self, command):
00127         """Execute commands coming from the XMPP component"""
00128 
00129         cmd_name = command.__class__
00130 
00131         try:
00132             handler = self._cmd_handlers[cmd_name]
00133         except KeyError:
00134             self.log.debug("No such command: " + cmd_name.__name__)
00135             return
00136 
00137         handler(command)
00138 
00139     def report_error(self, jid, text, data={}):
00140         """Reports an internal error
00141 
00142         @param jid: Jabber ID that should be informed about the error condition
00143         @param text: description of the error
00144         @param data: dictionary used to substitute strings in translated message
00145         @type data: dict
00146 
00147         """
00148         # Dummy function, so that the string appears in a .po file
00149         _ = lambda x: x
00150 
00151         cmddata = {'text': text, 'data': data}
00152         report = cmd.NotificationCommandI18n(jid, cmddata, msg_type=u"chat", async=False)
00153         self.commands_out.put_nowait(report)
00154 
00155     def get_auth_token(self, jid):
00156         """Get an auth token using user's Jabber ID
00157 
00158         @type jid: unicode
00159         """
00160         # We have to use a bare JID
00161         jid = jid.split('/')[0]
00162         token = self.connection.getJabberAuthToken(jid, self.config.secret)
00163         if token:
00164             self.token = token
00165 
00166     def warn_no_credentials(self, jid):
00167         """Warn a given JID that credentials check failed
00168 
00169         @param jid: full JID to notify about failure
00170         @type jid: str
00171 
00172         """
00173         # Dummy function, so that the string appears in a .po file
00174         _ = lambda x: x
00175 
00176         cmddata = {'text': _("Credentials check failed, you might be unable to see all information.")}
00177         warning = cmd.NotificationCommandI18n([jid], cmddata, async=False)
00178         self.commands_out.put_nowait(warning)
00179 
00180     def _get_multicall_result(self, jid):
00181         """Returns multicall results and issues a warning if there's an auth error
00182 
00183         @param jid: a full JID to use if there's an error
00184         @type jid: str
00185 
00186         """
00187 
00188         if not self.token:
00189             result = self.multicall()[0]
00190             token_result = u"FAILURE"
00191         else:
00192             token_result, result = self.multicall()
00193 
00194         if token_result != u"SUCCESS":
00195             self.warn_no_credentials(jid)
00196 
00197         return result
00198 
00199 
00200     def get_page(self, command):
00201         """Returns a raw page"""
00202 
00203         self.multicall.getPage(command.pagename)
00204         command.data = self._get_multicall_result(command.jid)
00205 
00206     get_page = _xmlrpc_decorator(get_page)
00207 
00208 
00209     def get_page_html(self, command):
00210         """Returns a html-formatted page"""
00211 
00212         self.multicall.getPageHTML(command.pagename)
00213         command.data = self._get_multicall_result(command.jid)
00214 
00215     get_page_html = _xmlrpc_decorator(get_page_html)
00216 
00217 
00218     def get_page_list(self, command):
00219         """Returns a list of all accesible pages"""
00220 
00221         # Dummy function, so that the string appears in a .po file
00222         _ = lambda x: x
00223 
00224         cmd_data = {'text': _("This command may take a while to complete, please be patient...")}
00225         info = cmd.NotificationCommandI18n([command.jid], cmd_data, async=False, msg_type=u"chat")
00226         self.commands_out.put_nowait(info)
00227 
00228         self.multicall.getAllPages()
00229         command.data = self._get_multicall_result(command.jid)
00230 
00231     get_page_list = _xmlrpc_decorator(get_page_list)
00232 
00233 
00234     def get_page_info(self, command):
00235         """Returns detailed information about a given page"""
00236 
00237         self.multicall.getPageInfo(command.pagename)
00238         command.data = self._get_multicall_result(command.jid)
00239 
00240     get_page_info = _xmlrpc_decorator(get_page_info)
00241 
00242     def do_search(self, command):
00243         """Performs a search"""
00244 
00245         # Dummy function, so that the string appears in a .po file
00246         _ = lambda x: x
00247 
00248         cmd_data = {'text': _("This command may take a while to complete, please be patient...")}
00249         info = cmd.NotificationCommandI18n([command.jid], cmd_data, async=False, msg_type=u"chat")
00250         self.commands_out.put_nowait(info)
00251 
00252         c = command
00253         self.multicall.searchPagesEx(c.term, c.search_type, 30, c.case, c.mtime, c.regexp)
00254         command.data = self._get_multicall_result(command.jid)
00255 
00256     do_search = _xmlrpc_decorator(do_search)
00257 
00258     def do_revert(self, command):
00259         """Performs a page revert"""
00260 
00261         # Dummy function, so that the string appears in a .po file
00262         _ = lambda x: x
00263 
00264         self.multicall.revertPage(command.pagename, command.revision)
00265         data = self._get_multicall_result(command.jid)
00266 
00267         if type(data) == bool and data:
00268             cmd_data = {'text': _("Page has been reverted.")}
00269         elif isinstance(str, data) or isinstance(unicode, data):
00270             cmd_data = {'text': _("Revert failed: %(reason)s" % {'reason': data})}
00271         else:
00272             cmd_data = {'text': _("Revert failed.")}
00273 
00274         info = cmd.NotificationCommandI18n([command.jid], cmd_data, async=False, msg_type=u"chat")
00275         self.commands_out.put_nowait(info)
00276 
00277     do_revert = _xmlrpc_decorator(do_revert)
00278 
00279     def get_language_by_jid(self, command):
00280         """Returns language of the a user identified by the given JID"""
00281 
00282         server = xmlrpclib.ServerProxy(self.config.wiki_url + "?action=xmlrpc2")
00283         language = "en"
00284 
00285         try:
00286             language = server.getUserLanguageByJID(command.jid)
00287         except xmlrpclib.Fault, fault:
00288             self.log.error(str(fault))
00289         except xmlrpclib.Error, err:
00290             self.log.error(str(err))
00291         except Exception, exc:
00292             self.log.critical(str(exc))
00293 
00294         command.language = language
00295         self.commands_out.put_nowait(command)
00296 
00297 
00298 class XMLRPCServer(Thread):
00299     """XMLRPC Server
00300 
00301     It waits for notifications requests coming from wiki,
00302     creates command objects and puts them on a queue for
00303     later processing by the XMPP component
00304 
00305     @param commands: an input command queue
00306     """
00307 
00308     def __init__(self, config, commands):
00309         Thread.__init__(self)
00310         self.commands = commands
00311         self.verbose = config.verbose
00312         self.log = logging.getLogger(__name__)
00313         self.config = config
00314 
00315         if config.secret:
00316             self.secret = config.secret
00317         else:
00318             error = "You must set a (long) secret string"
00319             self.log.critical(error)
00320             raise ConfigurationError(error)
00321 
00322         self.server = None
00323 
00324     def run(self):
00325         """Starts the server / thread"""
00326 
00327         self.server = SimpleXMLRPCServer((self.config.xmlrpc_host, self.config.xmlrpc_port))
00328 
00329         # Register methods having an "export" attribute as XML RPC functions and
00330         # decorate them with a check for a shared (wiki-bot) secret.
00331         items = self.__class__.__dict__.items()
00332         methods = [(name, func) for (name, func) in items if callable(func)
00333                    and "export" in func.__dict__]
00334 
00335         for name, func in methods:
00336             self.server.register_function(self.secret_check(func), name)
00337 
00338         self.server.serve_forever()
00339 
00340     def secret_check(self, function):
00341         """Adds a check for a secret to a given function
00342 
00343         Using this one does not have to worry about checking for the secret
00344         in every XML RPC function.
00345         """
00346         def protected_func(secret, *args):
00347             if secret != self.secret:
00348                 raise xmlrpclib.Fault(1, "You are not allowed to use this bot!")
00349             else:
00350                 return function(self, *args)
00351 
00352         return protected_func
00353 
00354 
00355     def send_notification(self, jids, notification):
00356         """Instructs the XMPP component to send a notification
00357 
00358         The notification dict has following entries:
00359         'text' - notification text (REQUIRED)
00360         'subject' - notification subject
00361         'url_list' - a list of dicts describing attached URLs
00362 
00363         @param jids: a list of JIDs to send a message to (bare JIDs)
00364         @type jids: a list of str or unicode
00365         @param notification: dictionary with notification data
00366         @type notification: dict
00367 
00368         """
00369         command = cmd.NotificationCommand(jids, notification, async=True)
00370         self.commands.put_nowait(command)
00371         return True
00372     send_notification.export = True
00373 
00374     def addJIDToRoster(self, jid):
00375         """Instructs the XMPP component to add a new JID to its roster
00376 
00377         @param jid: a jid to add, this must be a bare jid
00378         @type jid: str or unicode,
00379 
00380         """
00381         command = cmd.AddJIDToRosterCommand(jid)
00382         self.commands.put_nowait(command)
00383         return True
00384     addJIDToRoster.export = True
00385 
00386     def removeJIDFromRoster(self, jid):
00387         """Instructs the XMPP component to remove a JID from its roster
00388 
00389         @param jid: a jid to remove, this must be a bare jid
00390         @type jid: str or unicode
00391 
00392         """
00393         command = cmd.RemoveJIDFromRosterCommand(jid)
00394         self.commands.put_nowait(command)
00395         return True
00396     removeJIDFromRoster.export = True