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)