1
# -*- coding: iso-8859-1 -*-
3
MoinMoin - a xmlrpc server and client for the notification bot
5
@copyright: 2007 by Karol Nowak <grywacz@gmail.com>
6
@license: GNU GPL, see COPYING for details.
9
import logging, xmlrpclib, Queue
10
from SimpleXMLRPCServer import SimpleXMLRPCServer
11
from threading import Thread
13
import jabberbot.commands as cmd
14
from jabberbot.multicall import MultiCall
17
class ConfigurationError(Exception):
19
def __init__(self, message):
20
Exception.__init__(self)
21
self.message = message
24
def _xmlrpc_decorator(function):
25
"""A decorator function, which adds some maintenance code
27
This function takes care of preparing a MultiCall object and
28
an authentication token, and deleting them at the end.
31
def wrapped_func(self, command):
32
# Dummy function, so that the string appears in a .po file
36
self.multicall = MultiCall(self.connection)
38
if type(jid) is not list:
43
self.get_auth_token(command.jid)
45
self.multicall.applyAuthToken(self.token)
47
function(self, command)
48
self.commands_out.put_nowait(command)
50
except xmlrpclib.Fault, fault:
51
msg = _("Your request has failed. The reason is:\n%(error)s")
52
self.log.error(str(fault))
53
self.report_error(jid, msg, {'error': fault.faultString})
54
except xmlrpclib.Error, err:
55
msg = _("A serious error occured while processing your request:\n%(error)s")
56
self.log.error(str(err))
57
self.report_error(jid, msg, {'error': str(err)})
58
except Exception, exc:
59
msg = _("An internal error has occured, please contact the administrator.")
60
self.log.critical(str(exc))
61
self.report_error(jid, msg)
69
class XMLRPCClient(Thread):
72
It's responsible for performing XMLRPC operations on
73
a wiki, as inctructed by command objects received from
76
def __init__(self, config, commands_in, commands_out):
79
@param commands_out: an output command queue (to xmpp)
80
@param commands_in: an input command queue (from xmpp)
84
self.log = logging.getLogger(__name__)
87
error = "You must set a (long) secret string!"
88
self.log.critical(error)
89
raise ConfigurationError(error)
91
self.commands_in = commands_in
92
self.commands_out = commands_out
94
self.url = config.wiki_url + "?action=xmlrpc2"
95
self.connection = self.create_connection()
100
self._cmd_handlers = {cmd.GetPage: self.get_page,
101
cmd.GetPageHTML: self.get_page_html,
102
cmd.GetPageList: self.get_page_list,
103
cmd.GetPageInfo: self.get_page_info,
104
cmd.GetUserLanguage: self.get_language_by_jid,
105
cmd.Search: self.do_search,
106
cmd.RevertPage: self.do_revert}
109
"""Starts the server / thread"""
115
command = self.commands_in.get(True, 2)
116
self.execute_command(command)
121
"""Stop the thread"""
124
def create_connection(self):
125
return xmlrpclib.ServerProxy(self.url, allow_none=True, verbose=self.config.verbose)
127
def execute_command(self, command):
128
"""Execute commands coming from the XMPP component"""
130
cmd_name = command.__class__
133
handler = self._cmd_handlers[cmd_name]
135
self.log.debug("No such command: " + cmd_name.__name__)
140
def report_error(self, jid, text, data={}):
141
"""Reports an internal error
143
@param jid: Jabber ID that should be informed about the error condition
144
@param text: description of the error
145
@param data: dictionary used to substitute strings in translated message
149
# Dummy function, so that the string appears in a .po file
152
cmddata = {'text': text, 'data': data}
153
report = cmd.NotificationCommandI18n(jid, cmddata, msg_type=u"chat", async=False)
154
self.commands_out.put_nowait(report)
156
def get_auth_token(self, jid):
157
"""Get an auth token using user's Jabber ID
161
# We have to use a bare JID
162
jid = jid.split('/')[0]
163
token = self.connection.getJabberAuthToken(jid, self.config.secret)
167
def warn_no_credentials(self, jid):
168
"""Warn a given JID that credentials check failed
170
@param jid: full JID to notify about failure
174
# Dummy function, so that the string appears in a .po file
177
cmddata = {'text': _("Credentials check failed, you might be unable to see all information.")}
178
warning = cmd.NotificationCommandI18n([jid], cmddata, async=False)
179
self.commands_out.put_nowait(warning)
181
def _get_multicall_result(self, jid):
182
"""Returns multicall results and issues a warning if there's an auth error
184
@param jid: a full JID to use if there's an error
190
result = self.multicall()[0]
191
token_result = u"FAILURE"
193
token_result, result = self.multicall()
195
if token_result != u"SUCCESS":
196
self.warn_no_credentials(jid)
201
def get_page(self, command):
202
"""Returns a raw page"""
204
self.multicall.getPage(command.pagename)
205
command.data = self._get_multicall_result(command.jid)
207
get_page = _xmlrpc_decorator(get_page)
210
def get_page_html(self, command):
211
"""Returns a html-formatted page"""
213
self.multicall.getPageHTML(command.pagename)
214
command.data = self._get_multicall_result(command.jid)
216
get_page_html = _xmlrpc_decorator(get_page_html)
219
def get_page_list(self, command):
220
"""Returns a list of all accesible pages"""
222
# Dummy function, so that the string appears in a .po file
225
cmd_data = {'text': _("This command may take a while to complete, please be patient...")}
226
info = cmd.NotificationCommandI18n([command.jid], cmd_data, async=False, msg_type=u"chat")
227
self.commands_out.put_nowait(info)
229
self.multicall.getAllPages()
230
command.data = self._get_multicall_result(command.jid)
232
get_page_list = _xmlrpc_decorator(get_page_list)
235
def get_page_info(self, command):
236
"""Returns detailed information about a given page"""
238
self.multicall.getPageInfo(command.pagename)
239
command.data = self._get_multicall_result(command.jid)
241
get_page_info = _xmlrpc_decorator(get_page_info)
243
def do_search(self, command):
244
"""Performs a search"""
246
# Dummy function, so that the string appears in a .po file
249
cmd_data = {'text': _("This command may take a while to complete, please be patient...")}
250
info = cmd.NotificationCommandI18n([command.jid], cmd_data, async=False, msg_type=u"chat")
251
self.commands_out.put_nowait(info)
254
self.multicall.searchPagesEx(c.term, c.search_type, 30, c.case, c.mtime, c.regexp)
255
command.data = self._get_multicall_result(command.jid)
257
do_search = _xmlrpc_decorator(do_search)
259
def do_revert(self, command):
260
"""Performs a page revert"""
262
# Dummy function, so that the string appears in a .po file
265
self.multicall.revertPage(command.pagename, command.revision)
266
data = self._get_multicall_result(command.jid)
268
if type(data) == bool and data:
269
cmd_data = {'text': _("Page has been reverted.")}
270
elif isinstance(str, data) or isinstance(unicode, data):
271
cmd_data = {'text': _("Revert failed: %(reason)s" % {'reason': data})}
273
cmd_data = {'text': _("Revert failed.")}
275
info = cmd.NotificationCommandI18n([command.jid], cmd_data, async=False, msg_type=u"chat")
276
self.commands_out.put_nowait(info)
278
do_revert = _xmlrpc_decorator(do_revert)
280
def get_language_by_jid(self, command):
281
"""Returns language of the a user identified by the given JID"""
283
server = xmlrpclib.ServerProxy(self.config.wiki_url + "?action=xmlrpc2")
287
language = server.getUserLanguageByJID(command.jid)
288
except xmlrpclib.Fault, fault:
289
self.log.error(str(fault))
290
except xmlrpclib.Error, err:
291
self.log.error(str(err))
292
except Exception, exc:
293
self.log.critical(str(exc))
295
command.language = language
296
self.commands_out.put_nowait(command)
299
class XMLRPCServer(Thread):
302
It waits for notifications requests coming from wiki,
303
creates command objects and puts them on a queue for
304
later processing by the XMPP component
306
@param commands: an input command queue
309
def __init__(self, config, commands):
310
Thread.__init__(self)
311
self.commands = commands
312
self.verbose = config.verbose
313
self.log = logging.getLogger(__name__)
317
self.secret = config.secret
319
error = "You must set a (long) secret string"
320
self.log.critical(error)
321
raise ConfigurationError(error)
326
"""Starts the server / thread"""
328
self.server = SimpleXMLRPCServer((self.config.xmlrpc_host, self.config.xmlrpc_port))
330
# Register methods having an "export" attribute as XML RPC functions and
331
# decorate them with a check for a shared (wiki-bot) secret.
332
items = self.__class__.__dict__.items()
333
methods = [(name, func) for (name, func) in items if callable(func)
334
and "export" in func.__dict__]
336
for name, func in methods:
337
self.server.register_function(self.secret_check(func), name)
339
self.server.serve_forever()
341
def secret_check(self, function):
342
"""Adds a check for a secret to a given function
344
Using this one does not have to worry about checking for the secret
345
in every XML RPC function.
347
def protected_func(secret, *args):
348
if secret != self.secret:
349
raise xmlrpclib.Fault(1, "You are not allowed to use this bot!")
351
return function(self, *args)
353
return protected_func
356
def send_notification(self, jids, notification):
357
"""Instructs the XMPP component to send a notification
359
The notification dict has following entries:
360
'text' - notification text (REQUIRED)
361
'subject' - notification subject
362
'url_list' - a list of dicts describing attached URLs
364
@param jids: a list of JIDs to send a message to (bare JIDs)
365
@type jids: a list of str or unicode
366
@param notification: dictionary with notification data
367
@type notification: dict
370
command = cmd.NotificationCommand(jids, notification, async=True)
371
self.commands.put_nowait(command)
373
send_notification.export = True
375
def addJIDToRoster(self, jid):
376
"""Instructs the XMPP component to add a new JID to its roster
378
@param jid: a jid to add, this must be a bare jid
379
@type jid: str or unicode,
382
command = cmd.AddJIDToRosterCommand(jid)
383
self.commands.put_nowait(command)
385
addJIDToRoster.export = True
387
def removeJIDFromRoster(self, jid):
388
"""Instructs the XMPP component to remove a JID from its roster
390
@param jid: a jid to remove, this must be a bare jid
391
@type jid: str or unicode
394
command = cmd.RemoveJIDFromRosterCommand(jid)
395
self.commands.put_nowait(command)
397
removeJIDFromRoster.export = True