Mercurial > libervia-web
comparison libervia_server/__init__.py @ 331:06a48d805547
server side: make Libervia a Twisted plugin, and add it the --port argument + add a config file for the port.
==> NOTE from Goffi: it's a fixed version of Link Mauve's patch c144b603fb93
Fixes bug 16.
author | Emmanuel Gil Peyrot <linkmauve@linkmauve.fr> |
---|---|
date | Tue, 04 Feb 2014 17:09:00 +0100 |
parents | server_side/__init__.py@e43a1a0b4f23 |
children | c01397d18026 |
comparison
equal
deleted
inserted
replaced
330:e43a1a0b4f23 | 331:06a48d805547 |
---|---|
1 #!/usr/bin/python | |
2 # -*- coding: utf-8 -*- | |
3 | |
4 """ | |
5 Libervia: a Salut à Toi frontend | |
6 Copyright (C) 2011, 2012, 2013 Jérôme Poisson <goffi@goffi.org> | |
7 | |
8 This program is free software: you can redistribute it and/or modify | |
9 it under the terms of the GNU Affero General Public License as published by | |
10 the Free Software Foundation, either version 3 of the License, or | |
11 (at your option) any later version. | |
12 | |
13 This program is distributed in the hope that it will be useful, | |
14 but WITHOUT ANY WARRANTY; without even the implied warranty of | |
15 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
16 GNU Affero General Public License for more details. | |
17 | |
18 You should have received a copy of the GNU Affero General Public License | |
19 along with this program. If not, see <http://www.gnu.org/licenses/>. | |
20 """ | |
21 | |
22 from twisted.application import internet, service | |
23 from twisted.internet import glib2reactor | |
24 glib2reactor.install() | |
25 from twisted.internet import reactor, defer | |
26 from twisted.web import server | |
27 from twisted.web import error as weberror | |
28 from twisted.web.static import File | |
29 from twisted.web.resource import Resource, NoResource | |
30 from twisted.web.util import Redirect | |
31 from twisted.python.components import registerAdapter | |
32 from twisted.python.failure import Failure | |
33 from twisted.words.protocols.jabber.jid import JID | |
34 from txjsonrpc.web import jsonrpc | |
35 from txjsonrpc import jsonrpclib | |
36 | |
37 from logging import debug, info, warning, error | |
38 import re, glob | |
39 import os.path, sys | |
40 import tempfile, shutil, uuid | |
41 from zope.interface import Interface, Attribute, implements | |
42 from xml.dom import minidom | |
43 | |
44 from constants import Const | |
45 from libervia_server.blog import MicroBlog | |
46 from sat_frontends.bridge.DBus import DBusBridgeFrontend, BridgeExceptionNoService | |
47 from sat.core.i18n import _, D_ | |
48 | |
49 | |
50 class ISATSession(Interface): | |
51 profile = Attribute("Sat profile") | |
52 jid = Attribute("JID associated with the profile") | |
53 | |
54 class SATSession(object): | |
55 implements(ISATSession) | |
56 def __init__(self, session): | |
57 self.profile = None | |
58 self.jid = None | |
59 | |
60 class LiberviaSession(server.Session): | |
61 sessionTimeout = Const.TIMEOUT | |
62 | |
63 def __init__(self, *args, **kwargs): | |
64 self.__lock = False | |
65 server.Session.__init__(self, *args, **kwargs) | |
66 | |
67 def lock(self): | |
68 """Prevent session from expiring""" | |
69 self.__lock = True | |
70 self._expireCall.reset(sys.maxint) | |
71 | |
72 def unlock(self): | |
73 """Allow session to expire again, and touch it""" | |
74 self.__lock = False | |
75 self.touch() | |
76 | |
77 def touch(self): | |
78 if not self.__lock: | |
79 server.Session.touch(self) | |
80 | |
81 class ProtectedFile(File): | |
82 """A File class which doens't show directory listing""" | |
83 | |
84 def directoryListing(self): | |
85 return NoResource() | |
86 | |
87 class SATActionIDHandler(object): | |
88 """Manage SàT action action_id lifecycle""" | |
89 ID_LIFETIME = 30 #after this time (in seconds), action_id will be suppressed and action result will be ignored | |
90 | |
91 def __init__(self): | |
92 self.waiting_ids = {} | |
93 | |
94 def waitForId(self, callback, action_id, profile, *args, **kwargs): | |
95 """Wait for an action result | |
96 @param callback: method to call when action gave a result back | |
97 @param action_id: action_id to wait for | |
98 @param profile: %(doc_profile)s | |
99 @param *args: additional argument to pass to callback | |
100 @param **kwargs: idem""" | |
101 action_tuple = (action_id, profile) | |
102 self.waiting_ids[action_tuple] = (callback, args, kwargs) | |
103 reactor.callLater(self.ID_LIFETIME, self.purgeID, action_tuple) | |
104 | |
105 def purgeID(self, action_tuple): | |
106 """Called when an action_id has not be handled in time""" | |
107 if action_tuple in self.waiting_ids: | |
108 warning ("action of action_id %s [%s] has not been managed, action_id is now ignored" % action_tuple) | |
109 del self.waiting_ids[action_tuple] | |
110 | |
111 def actionResultCb(self, answer_type, action_id, data, profile): | |
112 """Manage the actionResult signal""" | |
113 action_tuple = (action_id, profile) | |
114 if action_tuple in self.waiting_ids: | |
115 callback, args, kwargs = self.waiting_ids[action_tuple] | |
116 del self.waiting_ids[action_tuple] | |
117 callback(answer_type, action_id, data, *args, **kwargs) | |
118 | |
119 class JSONRPCMethodManager(jsonrpc.JSONRPC): | |
120 | |
121 def __init__(self, sat_host): | |
122 jsonrpc.JSONRPC.__init__(self) | |
123 self.sat_host=sat_host | |
124 | |
125 def asyncBridgeCall(self, method_name, *args, **kwargs): | |
126 """Call an asynchrone bridge method and return a deferred | |
127 @param method_name: name of the method as a unicode | |
128 @return: a deferred which trigger the result | |
129 | |
130 """ | |
131 d = defer.Deferred() | |
132 | |
133 def _callback(*args): | |
134 if not args: | |
135 d.callback(None) | |
136 else: | |
137 if len(args) != 1: | |
138 Exception("Multiple return arguments not supported") | |
139 d.callback(args[0]) | |
140 | |
141 def _errback(result): | |
142 d.errback(Failure(jsonrpclib.Fault(Const.ERRNUM_BRIDGE_ERRBACK, unicode(result)))) | |
143 | |
144 kwargs["callback"] = _callback | |
145 kwargs["errback"] = _errback | |
146 getattr(self.sat_host.bridge, method_name)(*args, **kwargs) | |
147 return d | |
148 | |
149 | |
150 class MethodHandler(JSONRPCMethodManager): | |
151 | |
152 def __init__(self, sat_host): | |
153 JSONRPCMethodManager.__init__(self, sat_host) | |
154 self.authorized_params = None | |
155 | |
156 def render(self, request): | |
157 self.session = request.getSession() | |
158 profile = ISATSession(self.session).profile | |
159 if not profile: | |
160 #user is not identified, we return a jsonrpc fault | |
161 parsed = jsonrpclib.loads(request.content.read()) | |
162 fault = jsonrpclib.Fault(Const.ERRNUM_LIBERVIA, "Not allowed") #FIXME: define some standard error codes for libervia | |
163 return jsonrpc.JSONRPC._cbRender(self, fault, request, parsed.get('id'), parsed.get('jsonrpc')) | |
164 return jsonrpc.JSONRPC.render(self, request) | |
165 | |
166 def jsonrpc_getProfileJid(self): | |
167 """Return the jid of the profile""" | |
168 sat_session = ISATSession(self.session) | |
169 profile = sat_session.profile | |
170 sat_session.jid = JID(self.sat_host.bridge.getParamA("JabberID", "Connection", profile_key=profile)) | |
171 return sat_session.jid.full() | |
172 | |
173 def jsonrpc_disconnect(self): | |
174 """Disconnect the profile""" | |
175 sat_session = ISATSession(self.session) | |
176 profile = sat_session.profile | |
177 self.sat_host.bridge.disconnect(profile) | |
178 | |
179 def jsonrpc_getContacts(self): | |
180 """Return all passed args.""" | |
181 profile = ISATSession(self.session).profile | |
182 return self.sat_host.bridge.getContacts(profile) | |
183 | |
184 def jsonrpc_addContact(self, entity, name, groups): | |
185 """Subscribe to contact presence, and add it to the given groups""" | |
186 profile = ISATSession(self.session).profile | |
187 self.sat_host.bridge.addContact(entity, profile) | |
188 self.sat_host.bridge.updateContact(entity, name, groups, profile) | |
189 | |
190 def jsonrpc_delContact(self, entity): | |
191 """Remove contact from contacts list""" | |
192 profile = ISATSession(self.session).profile | |
193 self.sat_host.bridge.delContact(entity, profile) | |
194 | |
195 def jsonrpc_updateContact(self, entity, name, groups): | |
196 """Update contact's roster item""" | |
197 profile = ISATSession(self.session).profile | |
198 self.sat_host.bridge.updateContact(entity, name, groups, profile) | |
199 | |
200 def jsonrpc_subscription(self, sub_type, entity, name, groups): | |
201 """Confirm (or infirm) subscription, | |
202 and setup user roster in case of subscription""" | |
203 profile = ISATSession(self.session).profile | |
204 self.sat_host.bridge.subscription(sub_type, entity, profile) | |
205 if sub_type == 'subscribed': | |
206 self.sat_host.bridge.updateContact(entity, name, groups, profile) | |
207 | |
208 def jsonrpc_getWaitingSub(self): | |
209 """Return list of room already joined by user""" | |
210 profile = ISATSession(self.session).profile | |
211 return self.sat_host.bridge.getWaitingSub(profile) | |
212 | |
213 def jsonrpc_setStatus(self, presence, status): | |
214 """Change the presence and/or status | |
215 @param presence: value from ("", "chat", "away", "dnd", "xa") | |
216 @param status: any string to describe your status | |
217 """ | |
218 profile = ISATSession(self.session).profile | |
219 self.sat_host.bridge.setPresence('', presence, 0, {'': status}, profile) | |
220 | |
221 | |
222 def jsonrpc_sendMessage(self, to_jid, msg, subject, type_, options={}): | |
223 """send message""" | |
224 profile = ISATSession(self.session).profile | |
225 return self.asyncBridgeCall("sendMessage", to_jid, msg, subject, type_, options, profile) | |
226 | |
227 def jsonrpc_sendMblog(self, type_, dest, text, extra={}): | |
228 """ Send microblog message | |
229 @param type_: one of "PUBLIC", "GROUP" | |
230 @param dest: destinees (list of groups, ignored for "PUBLIC") | |
231 @param text: microblog's text | |
232 """ | |
233 profile = ISATSession(self.session).profile | |
234 extra['allow_comments'] = 'True' | |
235 | |
236 if not type_: # auto-detect | |
237 type_ = "PUBLIC" if dest == [] else "GROUP" | |
238 | |
239 if type_ in ("PUBLIC", "GROUP") and text: | |
240 if type_ == "PUBLIC": | |
241 #This text if for the public microblog | |
242 print "sending public blog" | |
243 return self.sat_host.bridge.sendGroupBlog("PUBLIC", [], text, extra, profile) | |
244 else: | |
245 print "sending group blog" | |
246 return self.sat_host.bridge.sendGroupBlog("GROUP", [dest], text, extra, profile) | |
247 else: | |
248 raise Exception("Invalid data") | |
249 | |
250 def jsonrpc_deleteMblog(self, pub_data, comments): | |
251 """Delete a microblog node | |
252 @param pub_data: a tuple (service, comment node identifier, item identifier) | |
253 @param comments: comments node identifier (for main item) or False | |
254 """ | |
255 profile = ISATSession(self.session).profile | |
256 return self.sat_host.bridge.deleteGroupBlog(pub_data, comments if comments else '', profile) | |
257 | |
258 def jsonrpc_updateMblog(self, pub_data, comments, message, extra={}): | |
259 """Modify a microblog node | |
260 @param pub_data: a tuple (service, comment node identifier, item identifier) | |
261 @param comments: comments node identifier (for main item) or False | |
262 @param message: new message | |
263 @param extra: dict which option name as key, which can be: | |
264 - allow_comments: True to accept an other level of comments, False else (default: False) | |
265 - rich: if present, contain rich text in currently selected syntax | |
266 """ | |
267 profile = ISATSession(self.session).profile | |
268 if comments: | |
269 extra['allow_comments'] = 'True' | |
270 return self.sat_host.bridge.updateGroupBlog(pub_data, comments if comments else '', message, extra, profile) | |
271 | |
272 def jsonrpc_sendMblogComment(self, node, text, extra={}): | |
273 """ Send microblog message | |
274 @param node: url of the comments node | |
275 @param text: comment | |
276 """ | |
277 profile = ISATSession(self.session).profile | |
278 if node and text: | |
279 return self.sat_host.bridge.sendGroupBlogComment(node, text, extra, profile) | |
280 else: | |
281 raise Exception("Invalid data") | |
282 | |
283 def jsonrpc_getLastMblogs(self, publisher_jid, max_item): | |
284 """Get last microblogs posted by a contact | |
285 @param publisher_jid: jid of the publisher | |
286 @param max_item: number of items to ask | |
287 @return list of microblog data (dict)""" | |
288 profile = ISATSession(self.session).profile | |
289 d = self.asyncBridgeCall("getLastGroupBlogs", publisher_jid, max_item, profile) | |
290 return d | |
291 | |
292 def jsonrpc_getMassiveLastMblogs(self, publishers_type, publishers_list, max_item): | |
293 """Get lasts microblogs posted by several contacts at once | |
294 @param publishers_type: one of "ALL", "GROUP", "JID" | |
295 @param publishers_list: list of publishers type (empty list of all, list of groups or list of jids) | |
296 @param max_item: number of items to ask | |
297 @return: dictionary key=publisher's jid, value=list of microblog data (dict)""" | |
298 profile = ISATSession(self.session).profile | |
299 d = self.asyncBridgeCall("getMassiveLastGroupBlogs", publishers_type, publishers_list, max_item, profile) | |
300 self.sat_host.bridge.massiveSubscribeGroupBlogs(publishers_type, publishers_list, profile) | |
301 return d | |
302 | |
303 def jsonrpc_getMblogComments(self, service, node): | |
304 """Get all comments of given node | |
305 @param service: jid of the service hosting the node | |
306 @param node: comments node | |
307 """ | |
308 profile = ISATSession(self.session).profile | |
309 d = self.asyncBridgeCall("getGroupBlogComments", service, node, profile) | |
310 return d | |
311 | |
312 | |
313 def jsonrpc_getPresenceStatus(self): | |
314 """Get Presence information for connected contacts""" | |
315 profile = ISATSession(self.session).profile | |
316 return self.sat_host.bridge.getPresenceStatus(profile) | |
317 | |
318 def jsonrpc_getHistory(self, from_jid, to_jid, size, between): | |
319 """Return history for the from_jid/to_jid couple""" | |
320 sat_session = ISATSession(self.session) | |
321 profile = sat_session.profile | |
322 sat_jid = sat_session.jid | |
323 if not sat_jid: | |
324 error("No jid saved for this profile") | |
325 return {} | |
326 if JID(from_jid).userhost() != sat_jid.userhost() and JID(to_jid).userhost() != sat_jid.userhost(): | |
327 error("Trying to get history from a different jid, maybe a hack attempt ?") | |
328 return {} | |
329 d = self.asyncBridgeCall("getHistory", from_jid, to_jid, size, between, profile) | |
330 def show(result_dbus): | |
331 result = [] | |
332 for line in result_dbus: | |
333 #XXX: we have to do this stupid thing because Python D-Bus use its own types instead of standard types | |
334 # and txJsonRPC doesn't accept D-Bus types, resulting in a empty query | |
335 timestamp, from_jid, to_jid, message, mess_type, extra = line | |
336 result.append((float(timestamp), unicode(from_jid), unicode(to_jid), unicode(message), unicode(mess_type), dict(extra))) | |
337 return result | |
338 d.addCallback(show) | |
339 return d | |
340 | |
341 def jsonrpc_joinMUC(self, room_jid, nick): | |
342 """Join a Multi-User Chat room | |
343 @room_jid: leave empty string to generate a unique name | |
344 """ | |
345 profile = ISATSession(self.session).profile | |
346 try: | |
347 if room_jid != "": | |
348 room_jid = JID(room_jid).userhost() | |
349 except: | |
350 warning('Invalid room jid') | |
351 return | |
352 d = self.asyncBridgeCall("joinMUC", room_jid, nick, {}, profile) | |
353 return d | |
354 | |
355 def jsonrpc_inviteMUC(self, contact_jid, room_jid): | |
356 """Invite a user to a Multi-User Chat room""" | |
357 profile = ISATSession(self.session).profile | |
358 try: | |
359 room_jid = JID(room_jid).userhost() | |
360 except: | |
361 warning('Invalid room jid') | |
362 return | |
363 room_id = room_jid.split("@")[0] | |
364 service = room_jid.split("@")[1] | |
365 self.sat_host.bridge.inviteMUC(contact_jid, service, room_id, {}, profile) | |
366 | |
367 def jsonrpc_mucLeave(self, room_jid): | |
368 """Quit a Multi-User Chat room""" | |
369 profile = ISATSession(self.session).profile | |
370 try: | |
371 room_jid = JID(room_jid) | |
372 except: | |
373 warning('Invalid room jid') | |
374 return | |
375 self.sat_host.bridge.mucLeave(room_jid.userhost(), profile) | |
376 | |
377 def jsonrpc_getRoomsJoined(self): | |
378 """Return list of room already joined by user""" | |
379 profile = ISATSession(self.session).profile | |
380 return self.sat_host.bridge.getRoomsJoined(profile) | |
381 | |
382 def jsonrpc_launchTarotGame(self, other_players, room_jid=""): | |
383 """Create a room, invite the other players and start a Tarot game | |
384 @param room_jid: leave empty string to generate a unique room name | |
385 """ | |
386 profile = ISATSession(self.session).profile | |
387 try: | |
388 if room_jid != "": | |
389 room_jid = JID(room_jid).userhost() | |
390 except: | |
391 warning('Invalid room jid') | |
392 return | |
393 self.sat_host.bridge.tarotGameLaunch(other_players, room_jid, profile) | |
394 | |
395 def jsonrpc_getTarotCardsPaths(self): | |
396 """Give the path of all the tarot cards""" | |
397 _join = os.path.join | |
398 _media_dir = _join(self.sat_host.media_dir,'') | |
399 return map(lambda x: _join(Const.MEDIA_DIR, x[len(_media_dir):]), glob.glob(_join(_media_dir, Const.CARDS_DIR, '*_*.png'))); | |
400 | |
401 def jsonrpc_tarotGameReady(self, player, referee): | |
402 """Tell to the server that we are ready to start the game""" | |
403 profile = ISATSession(self.session).profile | |
404 self.sat_host.bridge.tarotGameReady(player, referee, profile) | |
405 | |
406 def jsonrpc_tarotGameContratChoosed(self, player_nick, referee, contrat): | |
407 """Tell to the server that we are ready to start the game""" | |
408 profile = ISATSession(self.session).profile | |
409 self.sat_host.bridge.tarotGameContratChoosed(player_nick, referee, contrat, profile) | |
410 | |
411 def jsonrpc_tarotGamePlayCards(self, player_nick, referee, cards): | |
412 """Tell to the server the cards we want to put on the table""" | |
413 profile = ISATSession(self.session).profile | |
414 self.sat_host.bridge.tarotGamePlayCards(player_nick, referee, cards, profile) | |
415 | |
416 def jsonrpc_launchRadioCollective(self, invited, room_jid=""): | |
417 """Create a room, invite people, and start a radio collective | |
418 @param room_jid: leave empty string to generate a unique room name | |
419 """ | |
420 profile = ISATSession(self.session).profile | |
421 try: | |
422 if room_jid != "": | |
423 room_jid = JID(room_jid).userhost() | |
424 except: | |
425 warning('Invalid room jid') | |
426 return | |
427 self.sat_host.bridge.radiocolLaunch(invited, room_jid, profile) | |
428 | |
429 def jsonrpc_getEntityData(self, jid, keys): | |
430 """Get cached data for an entit | |
431 @param jid: jid of contact from who we want data | |
432 @param keys: name of data we want (list) | |
433 @return: requested data""" | |
434 profile = ISATSession(self.session).profile | |
435 return self.sat_host.bridge.getEntityData(jid, keys, profile) | |
436 | |
437 def jsonrpc_getCard(self, jid): | |
438 """Get VCard for entiry | |
439 @param jid: jid of contact from who we want data | |
440 @return: id to retrieve the profile""" | |
441 profile = ISATSession(self.session).profile | |
442 return self.sat_host.bridge.getCard(jid, profile) | |
443 | |
444 def jsonrpc_getParamsUI(self): | |
445 """Return the parameters XML for profile""" | |
446 profile = ISATSession(self.session).profile | |
447 d = self.asyncBridgeCall("getParams", Const.SECURITY_LIMIT, Const.APP_NAME, profile) | |
448 | |
449 def setAuthorizedParams(d): | |
450 if self.authorized_params is None: | |
451 self.authorized_params = {} | |
452 for cat in minidom.parseString(d.encode('utf-8')).getElementsByTagName("category"): | |
453 params = cat.getElementsByTagName("param") | |
454 params_list = [param.getAttribute("name") for param in params] | |
455 self.authorized_params[cat.getAttribute("name")] = params_list | |
456 if self.authorized_params: | |
457 return d | |
458 else: | |
459 return None | |
460 | |
461 d.addCallback(setAuthorizedParams) | |
462 | |
463 from sat.tools.xml_tools import paramsXml2xmlUI | |
464 d.addCallback(lambda d: paramsXml2xmlUI(d) if d else "") | |
465 | |
466 return d | |
467 | |
468 def jsonrpc_asyncGetParamA(self, param, category, attribute="value"): | |
469 """Return the parameter value for profile""" | |
470 profile = ISATSession(self.session).profile | |
471 d = self.asyncBridgeCall("asyncGetParamA", param, category, attribute, Const.SECURITY_LIMIT, profile_key=profile) | |
472 return d | |
473 | |
474 def jsonrpc_setParam(self, name, value, category): | |
475 profile = ISATSession(self.session).profile | |
476 if category in self.authorized_params and name in self.authorized_params[category]: | |
477 return self.sat_host.bridge.setParam(name, value, category, Const.SECURITY_LIMIT, profile) | |
478 else: | |
479 warning("Trying to set parameter '%s' in category '%s' without authorization!!!" | |
480 % (name, category)) | |
481 | |
482 def jsonrpc_launchAction(self, callback_id, data): | |
483 profile = ISATSession(self.session).profile | |
484 d = self.asyncBridgeCall("launchAction", callback_id, data, profile) | |
485 return d | |
486 | |
487 def jsonrpc_chatStateComposing(self, to_jid_s): | |
488 """Call the method to process a "composing" state. | |
489 @param to_jid_s: contact the user is composing to | |
490 """ | |
491 profile = ISATSession(self.session).profile | |
492 self.sat_host.bridge.chatStateComposing(to_jid_s, profile) | |
493 | |
494 def jsonrpc_getNewAccountDomain(self): | |
495 """@return: the domain for new account creation""" | |
496 d = self.asyncBridgeCall("getNewAccountDomain") | |
497 return d | |
498 | |
499 def jsonrpc_confirmationAnswer(self, confirmation_id, result, answer_data): | |
500 """Send the user's answer to any previous 'askConfirmation' signal""" | |
501 profile = ISATSession(self.session).profile | |
502 self.sat_host.bridge.confirmationAnswer(confirmation_id, result, answer_data, profile) | |
503 | |
504 def jsonrpc_syntaxConvert(self, text, syntax_from=Const.SYNTAX_XHTML, syntax_to=Const.SYNTAX_CURRENT): | |
505 """ Convert a text between two syntaxes | |
506 @param text: text to convert | |
507 @param syntax_from: source syntax (e.g. "markdown") | |
508 @param syntax_to: dest syntax (e.g.: "XHTML") | |
509 @param safe: clean resulting XHTML to avoid malicious code if True (forced here) | |
510 @return: converted text """ | |
511 profile = ISATSession(self.session).profile | |
512 return self.sat_host.bridge.syntaxConvert(text, syntax_from, syntax_to, True, profile) | |
513 | |
514 | |
515 class Register(JSONRPCMethodManager): | |
516 """This class manage the registration procedure with SàT | |
517 It provide an api for the browser, check password and setup the web server""" | |
518 | |
519 def __init__(self, sat_host): | |
520 JSONRPCMethodManager.__init__(self, sat_host) | |
521 self.profiles_waiting={} | |
522 self.request=None | |
523 | |
524 def getWaitingRequest(self, profile): | |
525 """Tell if a profile is trying to log in""" | |
526 if self.profiles_waiting.has_key(profile): | |
527 return self.profiles_waiting[profile] | |
528 else: | |
529 return None | |
530 | |
531 def render(self, request): | |
532 """ | |
533 Render method with some hacks: | |
534 - if login is requested, try to login with form data | |
535 - except login, every method is jsonrpc | |
536 - user doesn't need to be authentified for isRegistered or registerParams, but must be for all other methods | |
537 """ | |
538 if request.postpath==['login']: | |
539 return self.login(request) | |
540 _session = request.getSession() | |
541 parsed = jsonrpclib.loads(request.content.read()) | |
542 method = parsed.get("method") | |
543 if method != "isRegistered" and method != "registerParams": | |
544 #if we don't call login or isRegistered, we need to be identified | |
545 profile = ISATSession(_session).profile | |
546 if not profile: | |
547 #user is not identified, we return a jsonrpc fault | |
548 fault = jsonrpclib.Fault(Const.ERRNUM_LIBERVIA, "Not allowed") #FIXME: define some standard error codes for libervia | |
549 return jsonrpc.JSONRPC._cbRender(self, fault, request, parsed.get('id'), parsed.get('jsonrpc')) | |
550 self.request = request | |
551 return jsonrpc.JSONRPC.render(self, request) | |
552 | |
553 def login(self, request): | |
554 """ | |
555 this method is called with the POST information from the registering form | |
556 it test if the password is ok, and log in if it's the case, | |
557 else it return an error | |
558 @param request: request of the register formulaire, must have "login" and "password" as arguments | |
559 @return: A constant indicating the state: | |
560 - BAD REQUEST: something is wrong in the request (bad arguments, profile_key for login) | |
561 - AUTH ERROR: either the profile or the password is wrong | |
562 - ALREADY WAITING: a request has already be made for this profile | |
563 - server.NOT_DONE_YET: the profile is being processed, the return value will be given by self._logged or self._logginError | |
564 """ | |
565 | |
566 try: | |
567 if request.args['submit_type'][0] == 'login': | |
568 _login = request.args['login'][0] | |
569 if _login.startswith('@'): | |
570 raise Exception('No profile_key allowed') | |
571 _pass = request.args['login_password'][0] | |
572 | |
573 elif request.args['submit_type'][0] == 'register': | |
574 return self._registerNewAccount(request) | |
575 | |
576 else: | |
577 raise Exception('Unknown submit type') | |
578 except KeyError: | |
579 return "BAD REQUEST" | |
580 | |
581 _profile_check = self.sat_host.bridge.getProfileName(_login) | |
582 | |
583 def profile_pass_cb(_profile_pass): | |
584 if not _profile_check or _profile_check != _login or _profile_pass != _pass: | |
585 request.write("AUTH ERROR") | |
586 request.finish() | |
587 return | |
588 | |
589 if self.profiles_waiting.has_key(_login): | |
590 request.write("ALREADY WAITING") | |
591 request.finish() | |
592 return | |
593 | |
594 if self.sat_host.bridge.isConnected(_login): | |
595 request.write(self._logged(_login, request, finish=False)) | |
596 request.finish() | |
597 return | |
598 | |
599 self.profiles_waiting[_login] = request | |
600 d = self.asyncBridgeCall("asyncConnect", _login) | |
601 return d | |
602 | |
603 def profile_pass_errback(ignore): | |
604 error("INTERNAL ERROR: can't check profile password") | |
605 request.write("AUTH ERROR") | |
606 request.finish() | |
607 | |
608 d = self.asyncBridgeCall("asyncGetParamA", "Password", "Connection", profile_key=_login) | |
609 d.addCallbacks(profile_pass_cb, profile_pass_errback) | |
610 | |
611 return server.NOT_DONE_YET | |
612 | |
613 def _postAccountCreation(self, answer_type, id, data, profile): | |
614 """Called when a account has just been created, | |
615 setup stuff has microblog access""" | |
616 def _connected(ignore): | |
617 mblog_d = self.asyncBridgeCall("setMicroblogAccess", "open", profile) | |
618 mblog_d.addBoth(lambda ignore: self.sat_host.bridge.disconnect(profile)) | |
619 | |
620 d = self.asyncBridgeCall("asyncConnect", profile) | |
621 d.addCallback(_connected) | |
622 | |
623 def _registerNewAccount(self, request): | |
624 """Create a new account, or return error | |
625 @param request: initial login request | |
626 @return: "REGISTRATION" in case of success""" | |
627 #TODO: must be moved in SàT core | |
628 | |
629 try: | |
630 profile = login = request.args['register_login'][0] | |
631 password = request.args['register_password'][0] #FIXME: password is ignored so far | |
632 email = request.args['email'][0] | |
633 except KeyError: | |
634 return "BAD REQUEST" | |
635 if not re.match(r'^[a-z0-9_-]+$', login, re.IGNORECASE) or \ | |
636 not re.match(r'^.+@.+\..+', email, re.IGNORECASE): | |
637 return "BAD REQUEST" | |
638 | |
639 def registered(result): | |
640 request.write('REGISTRATION') | |
641 request.finish() | |
642 | |
643 def registeringError(failure): | |
644 reason = str(failure.value) | |
645 if reason == "ConflictError": | |
646 request.write('ALREADY EXISTS') | |
647 elif reason == "InternalError": | |
648 request.write('INTERNAL') | |
649 else: | |
650 error('Unknown registering error: %s' % (reason,)) | |
651 request.write('Unknown error (%s)' % reason) | |
652 request.finish() | |
653 | |
654 d = self.asyncBridgeCall("registerSatAccount", email, password, profile) | |
655 d.addCallback(registered) | |
656 d.addErrback(registeringError) | |
657 return server.NOT_DONE_YET | |
658 | |
659 def __cleanWaiting(self, login): | |
660 """Remove login from waiting queue""" | |
661 try: | |
662 del self.profiles_waiting[login] | |
663 except KeyError: | |
664 pass | |
665 | |
666 def _logged(self, profile, request, finish=True): | |
667 """Set everything when a user just logged | |
668 and return "LOGGED" to the requester""" | |
669 def result(answer): | |
670 if finish: | |
671 request.write(answer) | |
672 request.finish() | |
673 else: | |
674 return answer | |
675 | |
676 self.__cleanWaiting(profile) | |
677 _session = request.getSession() | |
678 sat_session = ISATSession(_session) | |
679 if sat_session.profile: | |
680 error (('/!\\ Session has already a profile, this should NEVER happen !')) | |
681 return result('SESSION_ACTIVE') | |
682 sat_session.profile = profile | |
683 self.sat_host.prof_connected.add(profile) | |
684 | |
685 def onExpire(): | |
686 info ("Session expired (profile=%s)" % (profile,)) | |
687 try: | |
688 #We purge the queue | |
689 del self.sat_host.signal_handler.queue[profile] | |
690 except KeyError: | |
691 pass | |
692 #and now we disconnect the profile | |
693 self.sat_host.bridge.disconnect(profile) | |
694 | |
695 _session.notifyOnExpire(onExpire) | |
696 | |
697 d = defer.Deferred() | |
698 return result('LOGGED') | |
699 | |
700 def _logginError(self, login, request, error_type): | |
701 """Something went wrong during loggin, return an error""" | |
702 self.__cleanWaiting(login) | |
703 return error_type | |
704 | |
705 def jsonrpc_isConnected(self): | |
706 _session = self.request.getSession() | |
707 profile = ISATSession(_session).profile | |
708 return self.sat_host.bridge.isConnected(profile) | |
709 | |
710 def jsonrpc_connect(self): | |
711 _session = self.request.getSession() | |
712 profile = ISATSession(_session).profile | |
713 if self.profiles_waiting.has_key(profile): | |
714 raise jsonrpclib.Fault(1,'Already waiting') #FIXME: define some standard error codes for libervia | |
715 self.profiles_waiting[profile] = self.request | |
716 self.sat_host.bridge.connect(profile) | |
717 return server.NOT_DONE_YET | |
718 | |
719 def jsonrpc_isRegistered(self): | |
720 """Tell if the user is already registered""" | |
721 _session = self.request.getSession() | |
722 profile = ISATSession(_session).profile | |
723 return bool(profile) | |
724 | |
725 def jsonrpc_registerParams(self): | |
726 """Register the frontend specific parameters""" | |
727 params = """ | |
728 <params> | |
729 <individual> | |
730 <category name="%(category_name)s" label="%(category_label)s"> | |
731 <param name="%(param_name)s" label="%(param_label)s" value="false" type="bool" security="0"/> | |
732 </category> | |
733 </individual> | |
734 </params> | |
735 """ % { | |
736 'category_name': Const.ENABLE_UNIBOX_KEY, | |
737 'category_label': _(Const.ENABLE_UNIBOX_KEY), | |
738 'param_name': Const.ENABLE_UNIBOX_PARAM, | |
739 'param_label': _(Const.ENABLE_UNIBOX_PARAM) | |
740 } | |
741 | |
742 self.sat_host.bridge.paramsRegisterApp(params, Const.SECURITY_LIMIT, Const.APP_NAME) | |
743 | |
744 | |
745 class SignalHandler(jsonrpc.JSONRPC): | |
746 | |
747 def __init__(self, sat_host): | |
748 Resource.__init__(self) | |
749 self.register=None | |
750 self.sat_host=sat_host | |
751 self.signalDeferred = {} | |
752 self.queue = {} | |
753 | |
754 def plugRegister(self, register): | |
755 self.register = register | |
756 | |
757 def jsonrpc_getSignals(self): | |
758 """Keep the connection alive until a signal is received, then send it | |
759 @return: (signal, *signal_args)""" | |
760 _session = self.request.getSession() | |
761 profile = ISATSession(_session).profile | |
762 if profile in self.queue: #if we have signals to send in queue | |
763 if self.queue[profile]: | |
764 return self.queue[profile].pop(0) | |
765 else: | |
766 #the queue is empty, we delete the profile from queue | |
767 del self.queue[profile] | |
768 _session.lock() #we don't want the session to expire as long as this connection is active | |
769 def unlock(signal, profile): | |
770 _session.unlock() | |
771 try: | |
772 source_defer = self.signalDeferred[profile] | |
773 if source_defer.called and source_defer.result[0] == "disconnected": | |
774 info(u"[%s] disconnected" % (profile,)) | |
775 _session.expire() | |
776 except IndexError: | |
777 error("Deferred result should be a tuple with fonction name first") | |
778 | |
779 self.signalDeferred[profile] = defer.Deferred() | |
780 self.request.notifyFinish().addBoth(unlock, profile) | |
781 return self.signalDeferred[profile] | |
782 | |
783 def getGenericCb(self, function_name): | |
784 """Return a generic function which send all params to signalDeferred.callback | |
785 function must have profile as last argument""" | |
786 def genericCb(*args): | |
787 profile = args[-1] | |
788 if not profile in self.sat_host.prof_connected: | |
789 return | |
790 if profile in self.signalDeferred: | |
791 self.signalDeferred[profile].callback((function_name,args[:-1])) | |
792 del self.signalDeferred[profile] | |
793 else: | |
794 if not self.queue.has_key(profile): | |
795 self.queue[profile] = [] | |
796 self.queue[profile].append((function_name, args[:-1])) | |
797 return genericCb | |
798 | |
799 def connected(self, profile): | |
800 assert(self.register) #register must be plugged | |
801 request = self.register.getWaitingRequest(profile) | |
802 if request: | |
803 self.register._logged(profile, request) | |
804 | |
805 def disconnected(self, profile): | |
806 if not profile in self.sat_host.prof_connected: | |
807 error("'disconnected' signal received for a not connected profile") | |
808 return | |
809 self.sat_host.prof_connected.remove(profile) | |
810 if profile in self.signalDeferred: | |
811 self.signalDeferred[profile].callback(("disconnected",)) | |
812 del self.signalDeferred[profile] | |
813 else: | |
814 if not self.queue.has_key(profile): | |
815 self.queue[profile] = [] | |
816 self.queue[profile].append(("disconnected",)) | |
817 | |
818 | |
819 def connectionError(self, error_type, profile): | |
820 assert(self.register) #register must be plugged | |
821 request = self.register.getWaitingRequest(profile) | |
822 if request: #The user is trying to log in | |
823 if error_type == "AUTH_ERROR": | |
824 _error_t = "AUTH ERROR" | |
825 else: | |
826 _error_t = "UNKNOWN" | |
827 self.register._logginError(profile, request, _error_t) | |
828 | |
829 def render(self, request): | |
830 """ | |
831 Render method wich reject access if user is not identified | |
832 """ | |
833 _session = request.getSession() | |
834 parsed = jsonrpclib.loads(request.content.read()) | |
835 profile = ISATSession(_session).profile | |
836 if not profile: | |
837 #user is not identified, we return a jsonrpc fault | |
838 fault = jsonrpclib.Fault(Const.ERRNUM_LIBERVIA, "Not allowed") #FIXME: define some standard error codes for libervia | |
839 return jsonrpc.JSONRPC._cbRender(self, fault, request, parsed.get('id'), parsed.get('jsonrpc')) | |
840 self.request = request | |
841 return jsonrpc.JSONRPC.render(self, request) | |
842 | |
843 class UploadManager(Resource): | |
844 """This class manage the upload of a file | |
845 It redirect the stream to SàT core backend""" | |
846 isLeaf = True | |
847 NAME = 'path' #name use by the FileUpload | |
848 | |
849 def __init__(self, sat_host): | |
850 self.sat_host=sat_host | |
851 self.upload_dir = tempfile.mkdtemp() | |
852 self.sat_host.addCleanup(shutil.rmtree, self.upload_dir) | |
853 | |
854 def getTmpDir(self): | |
855 return self.upload_dir | |
856 | |
857 def _getFileName(self, request): | |
858 """Generate unique filename for a file""" | |
859 raise NotImplementedError | |
860 | |
861 def _fileWritten(self, request, filepath): | |
862 """Called once the file is actually written on disk | |
863 @param request: HTTP request object | |
864 @param filepath: full filepath on the server | |
865 @return: a tuple with the name of the async bridge method | |
866 to be called followed by its arguments. | |
867 """ | |
868 raise NotImplementedError | |
869 | |
870 def render(self, request): | |
871 """ | |
872 Render method with some hacks: | |
873 - if login is requested, try to login with form data | |
874 - except login, every method is jsonrpc | |
875 - user doesn't need to be authentified for isRegistered, but must be for all other methods | |
876 """ | |
877 filename = self._getFileName(request) | |
878 filepath = os.path.join(self.upload_dir, filename) | |
879 #FIXME: the uploaded file is fully loaded in memory at form parsing time so far | |
880 # (see twisted.web.http.Request.requestReceived). A custom requestReceived should | |
881 # be written in the futur. In addition, it is not yet possible to get progression informations | |
882 # (see http://twistedmatrix.com/trac/ticket/288) | |
883 | |
884 with open(filepath,'w') as f: | |
885 f.write(request.args[self.NAME][0]) | |
886 | |
887 def finish(d): | |
888 error = isinstance(d, Exception) or isinstance (d, Failure) | |
889 request.write('KO' if error else 'OK') | |
890 # TODO: would be great to re-use the original Exception class and message | |
891 # but it is lost in the middle of the backtrace and encapsulated within | |
892 # a DBusException instance --> extract the data from the backtrace? | |
893 request.finish() | |
894 | |
895 d = JSONRPCMethodManager(self.sat_host).asyncBridgeCall(*self._fileWritten(request, filepath)) | |
896 d.addCallbacks(lambda d: finish(d), lambda failure: finish(failure)) | |
897 return server.NOT_DONE_YET | |
898 | |
899 | |
900 class UploadManagerRadioCol(UploadManager): | |
901 NAME = 'song' | |
902 | |
903 def _getFileName(self, request): | |
904 return "%s.ogg" % str(uuid.uuid4()) #XXX: chromium doesn't seem to play song without the .ogg extension, even with audio/ogg mime-type | |
905 | |
906 def _fileWritten(self, request, filepath): | |
907 """Called once the file is actually written on disk | |
908 @param request: HTTP request object | |
909 @param filepath: full filepath on the server | |
910 @return: a tuple with the name of the async bridge method | |
911 to be called followed by its arguments. | |
912 """ | |
913 profile = ISATSession(request.getSession()).profile | |
914 return ("radiocolSongAdded", request.args['referee'][0], filepath, profile) | |
915 | |
916 | |
917 class UploadManagerAvatar(UploadManager): | |
918 NAME = 'avatar_path' | |
919 | |
920 def _getFileName(self, request): | |
921 return str(uuid.uuid4()) | |
922 | |
923 def _fileWritten(self, request, filepath): | |
924 """Called once the file is actually written on disk | |
925 @param request: HTTP request object | |
926 @param filepath: full filepath on the server | |
927 @return: a tuple with the name of the async bridge method | |
928 to be called followed by its arguments. | |
929 """ | |
930 profile = ISATSession(request.getSession()).profile | |
931 return ("setAvatar", filepath, profile) | |
932 | |
933 | |
934 class Libervia(service.Service): | |
935 | |
936 def __init__(self, port=8080): | |
937 self._cleanup = [] | |
938 self.port = port | |
939 root = ProtectedFile(Const.LIBERVIA_DIR) | |
940 self.signal_handler = SignalHandler(self) | |
941 _register = Register(self) | |
942 _upload_radiocol = UploadManagerRadioCol(self) | |
943 _upload_avatar = UploadManagerAvatar(self) | |
944 self.signal_handler.plugRegister(_register) | |
945 self.sessions = {} #key = session value = user | |
946 self.prof_connected = set() #Profiles connected | |
947 self.action_handler = SATActionIDHandler() | |
948 ## bridge ## | |
949 try: | |
950 self.bridge=DBusBridgeFrontend() | |
951 except BridgeExceptionNoService: | |
952 print(u"Can't connect to SàT backend, are you sure it's launched ?") | |
953 sys.exit(1) | |
954 self.bridge.register("connected", self.signal_handler.connected) | |
955 self.bridge.register("disconnected", self.signal_handler.disconnected) | |
956 self.bridge.register("connectionError", self.signal_handler.connectionError) | |
957 self.bridge.register("actionResult", self.action_handler.actionResultCb) | |
958 #core | |
959 for signal_name in ['presenceUpdate', 'newMessage', 'subscribe', 'contactDeleted', 'newContact', 'entityDataUpdated', 'askConfirmation', 'newAlert', 'paramUpdate']: | |
960 self.bridge.register(signal_name, self.signal_handler.getGenericCb(signal_name)) | |
961 #plugins | |
962 for signal_name in ['personalEvent', 'roomJoined', 'roomUserJoined', 'roomUserLeft', 'tarotGameStarted', 'tarotGameNew', 'tarotGameChooseContrat', | |
963 'tarotGameShowCards', 'tarotGameInvalidCards', 'tarotGameCardsPlayed', 'tarotGameYourTurn', 'tarotGameScore', 'tarotGamePlayers', | |
964 'radiocolStarted', 'radiocolPreload', 'radiocolPlay', 'radiocolNoUpload', 'radiocolUploadOk', 'radiocolSongRejected', 'radiocolPlayers', | |
965 'roomLeft', 'chatStateReceived']: | |
966 self.bridge.register(signal_name, self.signal_handler.getGenericCb(signal_name), "plugin") | |
967 self.media_dir = self.bridge.getConfig('','media_dir') | |
968 self.local_dir = self.bridge.getConfig('','local_dir') | |
969 root.putChild('', Redirect('libervia.html')) | |
970 root.putChild('json_signal_api', self.signal_handler) | |
971 root.putChild('json_api', MethodHandler(self)) | |
972 root.putChild('register_api', _register) | |
973 root.putChild('upload_radiocol', _upload_radiocol) | |
974 root.putChild('upload_avatar', _upload_avatar) | |
975 root.putChild('blog', MicroBlog(self)) | |
976 root.putChild('css', ProtectedFile("server_css/")) | |
977 root.putChild(os.path.dirname(Const.MEDIA_DIR), ProtectedFile(self.media_dir)) | |
978 root.putChild(os.path.dirname(Const.AVATARS_DIR), ProtectedFile(os.path.join(self.local_dir, Const.AVATARS_DIR))) | |
979 root.putChild('radiocol', ProtectedFile(_upload_radiocol.getTmpDir(), defaultType="audio/ogg")) #We cheat for PoC because we know we are on the same host, so we use directly upload dir | |
980 self.site = server.Site(root) | |
981 self.site.sessionFactory = LiberviaSession | |
982 | |
983 def addCleanup(self, callback, *args, **kwargs): | |
984 """Add cleaning method to call when service is stopped | |
985 cleaning method will be called in reverse order of they insertion | |
986 @param callback: callable to call on service stop | |
987 @param *args: list of arguments of the callback | |
988 @param **kwargs: list of keyword arguments of the callback""" | |
989 self._cleanup.insert(0, (callback, args, kwargs)) | |
990 | |
991 def startService(self): | |
992 reactor.listenTCP(self.port, self.site) | |
993 | |
994 def stopService(self): | |
995 print "launching cleaning methods" | |
996 for callback, args, kwargs in self._cleanup: | |
997 callback(*args, **kwargs) | |
998 | |
999 def run(self): | |
1000 reactor.run() | |
1001 | |
1002 def stop(self): | |
1003 reactor.stop() | |
1004 | |
1005 registerAdapter(SATSession, server.Session, ISATSession) | |
1006 application = service.Application(Const.APP_NAME) | |
1007 service = Libervia() | |
1008 service.setServiceParent(application) |