~ubuntu-branches/ubuntu/natty/moin/natty-updates

« back to all changes in this revision

Viewing changes to jabberbot/xmlrpcbot.py

  • Committer: Bazaar Package Importer
  • Author(s): Jonas Smedegaard
  • Date: 2008-06-22 21:17:13 UTC
  • mfrom: (0.9.1 upstream)
  • Revision ID: james.westby@ubuntu.com-20080622211713-fpo2zrq3s5dfecxg
Tags: 1.7.0-3
Simplify /etc/moin/wikilist format: "USER URL" (drop unneeded middle
CONFIG_DIR that was wrongly advertised as DATA_DIR).  Make
moin-mass-migrate handle both formats and warn about deprecation of
the old one.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# -*- coding: iso-8859-1 -*-
 
2
"""
 
3
    MoinMoin - a xmlrpc server and client for the notification bot
 
4
 
 
5
    @copyright: 2007 by Karol Nowak <grywacz@gmail.com>
 
6
    @license: GNU GPL, see COPYING for details.
 
7
"""
 
8
 
 
9
import logging, xmlrpclib, Queue
 
10
from SimpleXMLRPCServer import SimpleXMLRPCServer
 
11
from threading import Thread
 
12
 
 
13
import jabberbot.commands as cmd
 
14
from jabberbot.multicall import MultiCall
 
15
 
 
16
 
 
17
class ConfigurationError(Exception):
 
18
 
 
19
    def __init__(self, message):
 
20
        Exception.__init__(self)
 
21
        self.message = message
 
22
 
 
23
 
 
24
def _xmlrpc_decorator(function):
 
25
    """A decorator function, which adds some maintenance code
 
26
 
 
27
    This function takes care of preparing a MultiCall object and
 
28
    an authentication token, and deleting them at the end.
 
29
 
 
30
    """
 
31
    def wrapped_func(self, command):
 
32
        # Dummy function, so that the string appears in a .po file
 
33
        _ = lambda x: x
 
34
 
 
35
        self.token = None
 
36
        self.multicall = MultiCall(self.connection)
 
37
        jid = command.jid
 
38
        if type(jid) is not list:
 
39
            jid = [jid]
 
40
 
 
41
        try:
 
42
            try:
 
43
                self.get_auth_token(command.jid)
 
44
                if self.token:
 
45
                    self.multicall.applyAuthToken(self.token)
 
46
 
 
47
                function(self, command)
 
48
                self.commands_out.put_nowait(command)
 
49
 
 
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)
 
62
 
 
63
        finally:
 
64
            del self.token
 
65
            del self.multicall
 
66
 
 
67
    return wrapped_func
 
68
 
 
69
class XMLRPCClient(Thread):
 
70
    """XMLRPC Client
 
71
 
 
72
    It's responsible for performing XMLRPC operations on
 
73
    a wiki, as inctructed by command objects received from
 
74
    the XMPP component"""
 
75
 
 
76
    def __init__(self, config, commands_in, commands_out):
 
77
        """A constructor
 
78
 
 
79
        @param commands_out: an output command queue (to xmpp)
 
80
        @param commands_in: an input command queue (from xmpp)
 
81
 
 
82
        """
 
83
        Thread.__init__(self)
 
84
        self.log = logging.getLogger(__name__)
 
85
 
 
86
        if not config.secret:
 
87
            error = "You must set a (long) secret string!"
 
88
            self.log.critical(error)
 
89
            raise ConfigurationError(error)
 
90
 
 
91
        self.commands_in = commands_in
 
92
        self.commands_out = commands_out
 
93
        self.config = config
 
94
        self.url = config.wiki_url + "?action=xmlrpc2"
 
95
        self.connection = self.create_connection()
 
96
        self.token = None
 
97
        self.multicall = None
 
98
        self.stopping = False
 
99
 
 
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}
 
107
 
 
108
    def run(self):
 
109
        """Starts the server / thread"""
 
110
        while True:
 
111
            if self.stopping:
 
112
                break
 
113
 
 
114
            try:
 
115
                command = self.commands_in.get(True, 2)
 
116
                self.execute_command(command)
 
117
            except Queue.Empty:
 
118
                pass
 
119
 
 
120
    def stop(self):
 
121
        """Stop the thread"""
 
122
        self.stopping = True
 
123
 
 
124
    def create_connection(self):
 
125
        return xmlrpclib.ServerProxy(self.url, allow_none=True, verbose=self.config.verbose)
 
126
 
 
127
    def execute_command(self, command):
 
128
        """Execute commands coming from the XMPP component"""
 
129
 
 
130
        cmd_name = command.__class__
 
131
 
 
132
        try:
 
133
            handler = self._cmd_handlers[cmd_name]
 
134
        except KeyError:
 
135
            self.log.debug("No such command: " + cmd_name.__name__)
 
136
            return
 
137
 
 
138
        handler(command)
 
139
 
 
140
    def report_error(self, jid, text, data={}):
 
141
        """Reports an internal error
 
142
 
 
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
 
146
        @type data: dict
 
147
 
 
148
        """
 
149
        # Dummy function, so that the string appears in a .po file
 
150
        _ = lambda x: x
 
151
 
 
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)
 
155
 
 
156
    def get_auth_token(self, jid):
 
157
        """Get an auth token using user's Jabber ID
 
158
 
 
159
        @type jid: unicode
 
160
        """
 
161
        # We have to use a bare JID
 
162
        jid = jid.split('/')[0]
 
163
        token = self.connection.getJabberAuthToken(jid, self.config.secret)
 
164
        if token:
 
165
            self.token = token
 
166
 
 
167
    def warn_no_credentials(self, jid):
 
168
        """Warn a given JID that credentials check failed
 
169
 
 
170
        @param jid: full JID to notify about failure
 
171
        @type jid: str
 
172
 
 
173
        """
 
174
        # Dummy function, so that the string appears in a .po file
 
175
        _ = lambda x: x
 
176
 
 
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)
 
180
 
 
181
    def _get_multicall_result(self, jid):
 
182
        """Returns multicall results and issues a warning if there's an auth error
 
183
 
 
184
        @param jid: a full JID to use if there's an error
 
185
        @type jid: str
 
186
 
 
187
        """
 
188
 
 
189
        if not self.token:
 
190
            result = self.multicall()[0]
 
191
            token_result = u"FAILURE"
 
192
        else:
 
193
            token_result, result = self.multicall()
 
194
 
 
195
        if token_result != u"SUCCESS":
 
196
            self.warn_no_credentials(jid)
 
197
 
 
198
        return result
 
199
 
 
200
 
 
201
    def get_page(self, command):
 
202
        """Returns a raw page"""
 
203
 
 
204
        self.multicall.getPage(command.pagename)
 
205
        command.data = self._get_multicall_result(command.jid)
 
206
 
 
207
    get_page = _xmlrpc_decorator(get_page)
 
208
 
 
209
 
 
210
    def get_page_html(self, command):
 
211
        """Returns a html-formatted page"""
 
212
 
 
213
        self.multicall.getPageHTML(command.pagename)
 
214
        command.data = self._get_multicall_result(command.jid)
 
215
 
 
216
    get_page_html = _xmlrpc_decorator(get_page_html)
 
217
 
 
218
 
 
219
    def get_page_list(self, command):
 
220
        """Returns a list of all accesible pages"""
 
221
 
 
222
        # Dummy function, so that the string appears in a .po file
 
223
        _ = lambda x: x
 
224
 
 
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)
 
228
 
 
229
        self.multicall.getAllPages()
 
230
        command.data = self._get_multicall_result(command.jid)
 
231
 
 
232
    get_page_list = _xmlrpc_decorator(get_page_list)
 
233
 
 
234
 
 
235
    def get_page_info(self, command):
 
236
        """Returns detailed information about a given page"""
 
237
 
 
238
        self.multicall.getPageInfo(command.pagename)
 
239
        command.data = self._get_multicall_result(command.jid)
 
240
 
 
241
    get_page_info = _xmlrpc_decorator(get_page_info)
 
242
 
 
243
    def do_search(self, command):
 
244
        """Performs a search"""
 
245
 
 
246
        # Dummy function, so that the string appears in a .po file
 
247
        _ = lambda x: x
 
248
 
 
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)
 
252
 
 
253
        c = command
 
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)
 
256
 
 
257
    do_search = _xmlrpc_decorator(do_search)
 
258
 
 
259
    def do_revert(self, command):
 
260
        """Performs a page revert"""
 
261
 
 
262
        # Dummy function, so that the string appears in a .po file
 
263
        _ = lambda x: x
 
264
 
 
265
        self.multicall.revertPage(command.pagename, command.revision)
 
266
        data = self._get_multicall_result(command.jid)
 
267
 
 
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})}
 
272
        else:
 
273
            cmd_data = {'text': _("Revert failed.")}
 
274
 
 
275
        info = cmd.NotificationCommandI18n([command.jid], cmd_data, async=False, msg_type=u"chat")
 
276
        self.commands_out.put_nowait(info)
 
277
 
 
278
    do_revert = _xmlrpc_decorator(do_revert)
 
279
 
 
280
    def get_language_by_jid(self, command):
 
281
        """Returns language of the a user identified by the given JID"""
 
282
 
 
283
        server = xmlrpclib.ServerProxy(self.config.wiki_url + "?action=xmlrpc2")
 
284
        language = "en"
 
285
 
 
286
        try:
 
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))
 
294
 
 
295
        command.language = language
 
296
        self.commands_out.put_nowait(command)
 
297
 
 
298
 
 
299
class XMLRPCServer(Thread):
 
300
    """XMLRPC Server
 
301
 
 
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
 
305
 
 
306
    @param commands: an input command queue
 
307
    """
 
308
 
 
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__)
 
314
        self.config = config
 
315
 
 
316
        if config.secret:
 
317
            self.secret = config.secret
 
318
        else:
 
319
            error = "You must set a (long) secret string"
 
320
            self.log.critical(error)
 
321
            raise ConfigurationError(error)
 
322
 
 
323
        self.server = None
 
324
 
 
325
    def run(self):
 
326
        """Starts the server / thread"""
 
327
 
 
328
        self.server = SimpleXMLRPCServer((self.config.xmlrpc_host, self.config.xmlrpc_port))
 
329
 
 
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__]
 
335
 
 
336
        for name, func in methods:
 
337
            self.server.register_function(self.secret_check(func), name)
 
338
 
 
339
        self.server.serve_forever()
 
340
 
 
341
    def secret_check(self, function):
 
342
        """Adds a check for a secret to a given function
 
343
 
 
344
        Using this one does not have to worry about checking for the secret
 
345
        in every XML RPC function.
 
346
        """
 
347
        def protected_func(secret, *args):
 
348
            if secret != self.secret:
 
349
                raise xmlrpclib.Fault(1, "You are not allowed to use this bot!")
 
350
            else:
 
351
                return function(self, *args)
 
352
 
 
353
        return protected_func
 
354
 
 
355
 
 
356
    def send_notification(self, jids, notification):
 
357
        """Instructs the XMPP component to send a notification
 
358
 
 
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
 
363
 
 
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
 
368
 
 
369
        """
 
370
        command = cmd.NotificationCommand(jids, notification, async=True)
 
371
        self.commands.put_nowait(command)
 
372
        return True
 
373
    send_notification.export = True
 
374
 
 
375
    def addJIDToRoster(self, jid):
 
376
        """Instructs the XMPP component to add a new JID to its roster
 
377
 
 
378
        @param jid: a jid to add, this must be a bare jid
 
379
        @type jid: str or unicode,
 
380
 
 
381
        """
 
382
        command = cmd.AddJIDToRosterCommand(jid)
 
383
        self.commands.put_nowait(command)
 
384
        return True
 
385
    addJIDToRoster.export = True
 
386
 
 
387
    def removeJIDFromRoster(self, jid):
 
388
        """Instructs the XMPP component to remove a JID from its roster
 
389
 
 
390
        @param jid: a jid to remove, this must be a bare jid
 
391
        @type jid: str or unicode
 
392
 
 
393
        """
 
394
        command = cmd.RemoveJIDFromRosterCommand(jid)
 
395
        self.commands.put_nowait(command)
 
396
        return True
 
397
    removeJIDFromRoster.export = True