changeset 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 e43a1a0b4f23
children 6abd099c7007
files libervia.tac libervia_server/__init__.py libervia_server/blog.py libervia_server/html_tools.py server_side/__init__.py server_side/blog.py server_side/html_tools.py twisted/plugins/libervia.py
diffstat 7 files changed, 1203 insertions(+), 1141 deletions(-) [+]
line wrap: on
line diff
--- a/libervia.tac	Sat Jan 11 18:30:10 2014 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,1007 +0,0 @@
-#!/usr/bin/python
-# -*- coding: utf-8 -*-
-
-"""
-Libervia: a Salut à Toi frontend
-Copyright (C) 2011, 2012, 2013 Jérôme Poisson <goffi@goffi.org>
-
-This program is free software: you can redistribute it and/or modify
-it under the terms of the GNU Affero General Public License as published by
-the Free Software Foundation, either version 3 of the License, or
-(at your option) any later version.
-
-This program is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-GNU Affero General Public License for more details.
-
-You should have received a copy of the GNU Affero General Public License
-along with this program.  If not, see <http://www.gnu.org/licenses/>.
-"""
-
-from twisted.application import internet, service
-from twisted.internet import glib2reactor
-glib2reactor.install()
-from twisted.internet import reactor, defer
-from twisted.web import server
-from twisted.web import error as weberror
-from twisted.web.static import File
-from twisted.web.resource import Resource, NoResource
-from twisted.web.util import Redirect
-from twisted.python.components import registerAdapter
-from twisted.python.failure import Failure
-from twisted.words.protocols.jabber.jid import JID
-from txjsonrpc.web import jsonrpc
-from txjsonrpc import jsonrpclib
-
-from logging import debug, info, warning, error
-import re, glob
-import os.path, sys
-import tempfile, shutil, uuid
-from zope.interface import Interface, Attribute, implements
-from xml.dom import minidom
-
-from constants import Const
-from server_side.blog import MicroBlog
-from sat_frontends.bridge.DBus import DBusBridgeFrontend, BridgeExceptionNoService
-from sat.core.i18n import _, D_
-
-
-class ISATSession(Interface):
-    profile = Attribute("Sat profile")
-    jid = Attribute("JID associated with the profile")
-
-class SATSession(object):
-    implements(ISATSession)
-    def __init__(self, session):
-        self.profile = None
-        self.jid = None
-
-class LiberviaSession(server.Session):
-    sessionTimeout = Const.TIMEOUT
-
-    def __init__(self, *args, **kwargs):
-        self.__lock = False
-        server.Session.__init__(self, *args, **kwargs)
-
-    def lock(self):
-        """Prevent session from expiring"""
-        self.__lock = True
-        self._expireCall.reset(sys.maxint)
-
-    def unlock(self):
-        """Allow session to expire again, and touch it"""
-        self.__lock = False
-        self.touch()
-
-    def touch(self):
-        if not self.__lock:
-            server.Session.touch(self)
-
-class ProtectedFile(File):
-    """A File class which doens't show directory listing"""
-
-    def directoryListing(self):
-        return NoResource()
-
-class SATActionIDHandler(object):
-    """Manage SàT action action_id lifecycle"""
-    ID_LIFETIME = 30 #after this time (in seconds), action_id will be suppressed and action result will be ignored
-
-    def __init__(self):
-        self.waiting_ids = {}
-
-    def waitForId(self, callback, action_id, profile, *args, **kwargs):
-        """Wait for an action result
-        @param callback: method to call when action gave a result back
-        @param action_id: action_id to wait for
-        @param profile: %(doc_profile)s
-        @param *args: additional argument to pass to callback
-        @param **kwargs: idem"""
-        action_tuple = (action_id, profile)
-        self.waiting_ids[action_tuple] = (callback, args, kwargs)
-        reactor.callLater(self.ID_LIFETIME, self.purgeID, action_tuple)
-
-    def purgeID(self, action_tuple):
-        """Called when an action_id has not be handled in time"""
-        if action_tuple in self.waiting_ids:
-            warning ("action of action_id %s [%s] has not been managed, action_id is now ignored" % action_tuple)
-            del self.waiting_ids[action_tuple]
-
-    def actionResultCb(self, answer_type, action_id, data, profile):
-        """Manage the actionResult signal"""
-        action_tuple = (action_id, profile)
-        if action_tuple in self.waiting_ids:
-            callback, args, kwargs = self.waiting_ids[action_tuple]
-            del self.waiting_ids[action_tuple]
-            callback(answer_type, action_id, data, *args, **kwargs)
-
-class JSONRPCMethodManager(jsonrpc.JSONRPC):
-
-    def __init__(self, sat_host):
-        jsonrpc.JSONRPC.__init__(self)
-        self.sat_host=sat_host
-
-    def asyncBridgeCall(self, method_name, *args, **kwargs):
-        """Call an asynchrone bridge method and return a deferred
-        @param method_name: name of the method as a unicode
-        @return: a deferred which trigger the result
-
-        """
-        d = defer.Deferred()
-
-        def _callback(*args):
-            if not args:
-                d.callback(None)
-            else:
-                if len(args) != 1:
-                    Exception("Multiple return arguments not supported")
-                d.callback(args[0])
-
-        def _errback(result):
-            d.errback(Failure(jsonrpclib.Fault(Const.ERRNUM_BRIDGE_ERRBACK, unicode(result))))
-
-        kwargs["callback"] = _callback
-        kwargs["errback"] = _errback
-        getattr(self.sat_host.bridge, method_name)(*args, **kwargs)
-        return d
-
-
-class MethodHandler(JSONRPCMethodManager):
-
-    def __init__(self, sat_host):
-        JSONRPCMethodManager.__init__(self, sat_host)
-        self.authorized_params = None
-
-    def render(self, request):
-        self.session = request.getSession()
-        profile = ISATSession(self.session).profile
-        if not profile:
-            #user is not identified, we return a jsonrpc fault
-            parsed = jsonrpclib.loads(request.content.read())
-            fault = jsonrpclib.Fault(Const.ERRNUM_LIBERVIA, "Not allowed") #FIXME: define some standard error codes for libervia
-            return jsonrpc.JSONRPC._cbRender(self, fault, request, parsed.get('id'), parsed.get('jsonrpc'))
-        return jsonrpc.JSONRPC.render(self, request)
-
-    def jsonrpc_getProfileJid(self):
-        """Return the jid of the profile"""
-        sat_session = ISATSession(self.session)
-        profile = sat_session.profile
-        sat_session.jid = JID(self.sat_host.bridge.getParamA("JabberID", "Connection", profile_key=profile))
-        return sat_session.jid.full()
-
-    def jsonrpc_disconnect(self):
-        """Disconnect the profile"""
-        sat_session = ISATSession(self.session)
-        profile = sat_session.profile
-        self.sat_host.bridge.disconnect(profile)
-
-    def jsonrpc_getContacts(self):
-        """Return all passed args."""
-        profile = ISATSession(self.session).profile
-        return self.sat_host.bridge.getContacts(profile)
-
-    def jsonrpc_addContact(self, entity, name, groups):
-        """Subscribe to contact presence, and add it to the given groups"""
-        profile = ISATSession(self.session).profile
-        self.sat_host.bridge.addContact(entity, profile)
-        self.sat_host.bridge.updateContact(entity, name, groups, profile)
-
-    def jsonrpc_delContact(self, entity):
-        """Remove contact from contacts list"""
-        profile = ISATSession(self.session).profile
-        self.sat_host.bridge.delContact(entity, profile)
-
-    def jsonrpc_updateContact(self, entity, name, groups):
-        """Update contact's roster item"""
-        profile = ISATSession(self.session).profile
-        self.sat_host.bridge.updateContact(entity, name, groups, profile)
-
-    def jsonrpc_subscription(self, sub_type, entity, name, groups):
-        """Confirm (or infirm) subscription,
-        and setup user roster in case of subscription"""
-        profile = ISATSession(self.session).profile
-        self.sat_host.bridge.subscription(sub_type, entity, profile)
-        if sub_type == 'subscribed':
-            self.sat_host.bridge.updateContact(entity, name, groups, profile)
-
-    def jsonrpc_getWaitingSub(self):
-        """Return list of room already joined by user"""
-        profile = ISATSession(self.session).profile
-        return self.sat_host.bridge.getWaitingSub(profile)
-
-    def jsonrpc_setStatus(self, presence, status):
-        """Change the presence and/or status
-        @param presence: value from ("", "chat", "away", "dnd", "xa")
-        @param status: any string to describe your status
-        """
-        profile = ISATSession(self.session).profile
-        self.sat_host.bridge.setPresence('', presence, 0, {'': status}, profile)
-
-
-    def jsonrpc_sendMessage(self, to_jid, msg, subject, type_, options={}):
-        """send message"""
-        profile = ISATSession(self.session).profile
-        return self.asyncBridgeCall("sendMessage", to_jid, msg, subject, type_, options, profile)
-
-    def jsonrpc_sendMblog(self, type_, dest, text, extra={}):
-        """ Send microblog message
-        @param type_: one of "PUBLIC", "GROUP"
-        @param dest: destinees (list of groups, ignored for "PUBLIC")
-        @param text: microblog's text
-        """
-        profile = ISATSession(self.session).profile
-        extra['allow_comments'] = 'True'
-
-        if not type_:  # auto-detect
-            type_ = "PUBLIC" if dest == [] else "GROUP"
-
-        if type_ in ("PUBLIC", "GROUP") and text:
-            if type_ == "PUBLIC":
-                #This text if for the public microblog
-                print "sending public blog"
-                return self.sat_host.bridge.sendGroupBlog("PUBLIC", [], text, extra, profile)
-            else:
-                print "sending group blog"
-                return self.sat_host.bridge.sendGroupBlog("GROUP", [dest], text, extra, profile)
-        else:
-            raise Exception("Invalid data")
-
-    def jsonrpc_deleteMblog(self, pub_data, comments):
-        """Delete a microblog node
-        @param pub_data: a tuple (service, comment node identifier, item identifier)
-        @param comments: comments node identifier (for main item) or False
-        """
-        profile = ISATSession(self.session).profile
-        return self.sat_host.bridge.deleteGroupBlog(pub_data, comments if comments else '', profile)
-
-    def jsonrpc_updateMblog(self, pub_data, comments, message, extra={}):
-        """Modify a microblog node
-        @param pub_data: a tuple (service, comment node identifier, item identifier)
-        @param comments: comments node identifier (for main item) or False
-        @param message: new message
-        @param extra: dict which option name as key, which can be:
-            - allow_comments: True to accept an other level of comments, False else (default: False)
-            - rich: if present, contain rich text in currently selected syntax
-        """
-        profile = ISATSession(self.session).profile
-        if comments:
-            extra['allow_comments'] = 'True'
-        return self.sat_host.bridge.updateGroupBlog(pub_data, comments if comments else '', message, extra, profile)
-
-    def jsonrpc_sendMblogComment(self, node, text, extra={}):
-        """ Send microblog message
-        @param node: url of the comments node
-        @param text: comment
-        """
-        profile = ISATSession(self.session).profile
-        if node and text:
-            return self.sat_host.bridge.sendGroupBlogComment(node, text, extra, profile)
-        else:
-            raise Exception("Invalid data")
-
-    def jsonrpc_getLastMblogs(self, publisher_jid, max_item):
-        """Get last microblogs posted by a contact
-        @param publisher_jid: jid of the publisher
-        @param max_item: number of items to ask
-        @return list of microblog data (dict)"""
-        profile = ISATSession(self.session).profile
-        d = self.asyncBridgeCall("getLastGroupBlogs", publisher_jid, max_item, profile)
-        return d
-
-    def jsonrpc_getMassiveLastMblogs(self, publishers_type, publishers_list, max_item):
-        """Get lasts microblogs posted by several contacts at once
-        @param publishers_type: one of "ALL", "GROUP", "JID"
-        @param publishers_list: list of publishers type (empty list of all, list of groups or list of jids)
-        @param max_item: number of items to ask
-        @return: dictionary key=publisher's jid, value=list of microblog data (dict)"""
-        profile = ISATSession(self.session).profile
-        d = self.asyncBridgeCall("getMassiveLastGroupBlogs", publishers_type, publishers_list, max_item, profile)
-        self.sat_host.bridge.massiveSubscribeGroupBlogs(publishers_type, publishers_list, profile)
-        return d
-
-    def jsonrpc_getMblogComments(self, service, node):
-        """Get all comments of given node
-        @param service: jid of the service hosting the node
-        @param node: comments node
-        """
-        profile = ISATSession(self.session).profile
-        d = self.asyncBridgeCall("getGroupBlogComments", service, node, profile)
-        return d
-
-
-    def jsonrpc_getPresenceStatus(self):
-        """Get Presence information for connected contacts"""
-        profile = ISATSession(self.session).profile
-        return self.sat_host.bridge.getPresenceStatus(profile)
-
-    def jsonrpc_getHistory(self, from_jid, to_jid, size, between):
-        """Return history for the from_jid/to_jid couple"""
-        sat_session = ISATSession(self.session)
-        profile = sat_session.profile
-        sat_jid = sat_session.jid
-        if not sat_jid:
-            error("No jid saved for this profile")
-            return {}
-        if JID(from_jid).userhost() != sat_jid.userhost() and JID(to_jid).userhost() != sat_jid.userhost():
-            error("Trying to get history from a different jid, maybe a hack attempt ?")
-            return {}
-        d = self.asyncBridgeCall("getHistory", from_jid, to_jid, size, between, profile)
-        def show(result_dbus):
-            result = []
-            for line in result_dbus:
-                #XXX: we have to do this stupid thing because Python D-Bus use its own types instead of standard types
-                #     and txJsonRPC doesn't accept D-Bus types, resulting in a empty query
-                timestamp, from_jid, to_jid, message, mess_type, extra = line
-                result.append((float(timestamp), unicode(from_jid), unicode(to_jid), unicode(message), unicode(mess_type), dict(extra)))
-            return result
-        d.addCallback(show)
-        return d
-
-    def jsonrpc_joinMUC(self, room_jid, nick):
-        """Join a Multi-User Chat room
-        @room_jid: leave empty string to generate a unique name
-        """
-        profile = ISATSession(self.session).profile
-        try:
-            if room_jid != "":
-                room_jid = JID(room_jid).userhost()
-        except:
-            warning('Invalid room jid')
-            return
-        d = self.asyncBridgeCall("joinMUC", room_jid, nick, {}, profile)
-        return d
-
-    def jsonrpc_inviteMUC(self, contact_jid, room_jid):
-        """Invite a user to a Multi-User Chat room"""
-        profile = ISATSession(self.session).profile
-        try:
-            room_jid = JID(room_jid).userhost()
-        except:
-            warning('Invalid room jid')
-            return
-        room_id = room_jid.split("@")[0]
-        service = room_jid.split("@")[1]
-        self.sat_host.bridge.inviteMUC(contact_jid, service, room_id, {}, profile)
-
-    def jsonrpc_mucLeave(self, room_jid):
-        """Quit a Multi-User Chat room"""
-        profile = ISATSession(self.session).profile
-        try:
-            room_jid = JID(room_jid)
-        except:
-            warning('Invalid room jid')
-            return
-        self.sat_host.bridge.mucLeave(room_jid.userhost(), profile)
-
-    def jsonrpc_getRoomsJoined(self):
-        """Return list of room already joined by user"""
-        profile = ISATSession(self.session).profile
-        return self.sat_host.bridge.getRoomsJoined(profile)
-
-    def jsonrpc_launchTarotGame(self, other_players, room_jid=""):
-        """Create a room, invite the other players and start a Tarot game
-        @param room_jid: leave empty string to generate a unique room name
-        """
-        profile = ISATSession(self.session).profile
-        try:
-            if room_jid != "":
-                room_jid = JID(room_jid).userhost()
-        except:
-            warning('Invalid room jid')
-            return
-        self.sat_host.bridge.tarotGameLaunch(other_players, room_jid, profile)
-
-    def jsonrpc_getTarotCardsPaths(self):
-        """Give the path of all the tarot cards"""
-        _join = os.path.join
-        _media_dir = _join(self.sat_host.media_dir,'')
-        return map(lambda x: _join(Const.MEDIA_DIR, x[len(_media_dir):]), glob.glob(_join(_media_dir, Const.CARDS_DIR, '*_*.png')));
-
-    def jsonrpc_tarotGameReady(self, player, referee):
-        """Tell to the server that we are ready to start the game"""
-        profile = ISATSession(self.session).profile
-        self.sat_host.bridge.tarotGameReady(player, referee, profile)
-
-    def jsonrpc_tarotGameContratChoosed(self, player_nick, referee, contrat):
-        """Tell to the server that we are ready to start the game"""
-        profile = ISATSession(self.session).profile
-        self.sat_host.bridge.tarotGameContratChoosed(player_nick, referee, contrat, profile)
-
-    def jsonrpc_tarotGamePlayCards(self, player_nick, referee, cards):
-        """Tell to the server the cards we want to put on the table"""
-        profile = ISATSession(self.session).profile
-        self.sat_host.bridge.tarotGamePlayCards(player_nick, referee, cards, profile)
-
-    def jsonrpc_launchRadioCollective(self, invited, room_jid=""):
-        """Create a room, invite people, and start a radio collective
-        @param room_jid: leave empty string to generate a unique room name
-        """
-        profile = ISATSession(self.session).profile
-        try:
-            if room_jid != "":
-                room_jid = JID(room_jid).userhost()
-        except:
-            warning('Invalid room jid')
-            return
-        self.sat_host.bridge.radiocolLaunch(invited, room_jid, profile)
-
-    def jsonrpc_getEntityData(self, jid, keys):
-        """Get cached data for an entit
-        @param jid: jid of contact from who we want data
-        @param keys: name of data we want (list)
-        @return: requested data"""
-        profile = ISATSession(self.session).profile
-        return self.sat_host.bridge.getEntityData(jid, keys, profile)
-
-    def jsonrpc_getCard(self, jid):
-        """Get VCard for entiry
-        @param jid: jid of contact from who we want data
-        @return: id to retrieve the profile"""
-        profile = ISATSession(self.session).profile
-        return self.sat_host.bridge.getCard(jid, profile)
-
-    def jsonrpc_getParamsUI(self):
-        """Return the parameters XML for profile"""
-        profile = ISATSession(self.session).profile
-        d = self.asyncBridgeCall("getParams", Const.SECURITY_LIMIT, Const.APP_NAME, profile)
-
-        def setAuthorizedParams(d):
-            if self.authorized_params is None:
-                self.authorized_params = {}
-                for cat in minidom.parseString(d.encode('utf-8')).getElementsByTagName("category"):
-                    params = cat.getElementsByTagName("param")
-                    params_list = [param.getAttribute("name") for param in params]
-                    self.authorized_params[cat.getAttribute("name")] = params_list
-            if self.authorized_params:
-                return d
-            else:
-                return None
-
-        d.addCallback(setAuthorizedParams)
-
-        from sat.tools.xml_tools import paramsXml2xmlUI
-        d.addCallback(lambda d: paramsXml2xmlUI(d) if d else "")
-
-        return d
-
-    def jsonrpc_asyncGetParamA(self, param, category, attribute="value"):
-        """Return the parameter value for profile"""
-        profile = ISATSession(self.session).profile
-        d = self.asyncBridgeCall("asyncGetParamA", param, category, attribute, Const.SECURITY_LIMIT, profile_key=profile)
-        return d
-
-    def jsonrpc_setParam(self, name, value, category):
-        profile = ISATSession(self.session).profile
-        if category in self.authorized_params and name in self.authorized_params[category]:
-            return self.sat_host.bridge.setParam(name, value, category, Const.SECURITY_LIMIT, profile)
-        else:
-            warning("Trying to set parameter '%s' in category '%s' without authorization!!!"
-                    % (name, category))
-
-    def jsonrpc_launchAction(self, callback_id, data):
-        profile = ISATSession(self.session).profile
-        d = self.asyncBridgeCall("launchAction", callback_id, data, profile)
-        return d
-
-    def jsonrpc_chatStateComposing(self, to_jid_s):
-        """Call the method to process a "composing" state.
-        @param to_jid_s: contact the user is composing to
-        """
-        profile = ISATSession(self.session).profile
-        self.sat_host.bridge.chatStateComposing(to_jid_s, profile)
-
-    def jsonrpc_getNewAccountDomain(self):
-        """@return: the domain for new account creation"""
-        d = self.asyncBridgeCall("getNewAccountDomain")
-        return d
-
-    def jsonrpc_confirmationAnswer(self, confirmation_id, result, answer_data):
-        """Send the user's answer to any previous 'askConfirmation' signal"""
-        profile = ISATSession(self.session).profile
-        self.sat_host.bridge.confirmationAnswer(confirmation_id, result, answer_data, profile)
-
-    def jsonrpc_syntaxConvert(self, text, syntax_from=Const.SYNTAX_XHTML, syntax_to=Const.SYNTAX_CURRENT):
-        """ Convert a text between two syntaxes
-        @param text: text to convert
-        @param syntax_from: source syntax (e.g. "markdown")
-        @param syntax_to: dest syntax (e.g.: "XHTML")
-        @param safe: clean resulting XHTML to avoid malicious code if True (forced here)
-        @return: converted text """
-        profile = ISATSession(self.session).profile
-        return self.sat_host.bridge.syntaxConvert(text, syntax_from, syntax_to, True, profile)
-
-
-class Register(JSONRPCMethodManager):
-    """This class manage the registration procedure with SàT
-    It provide an api for the browser, check password and setup the web server"""
-
-    def __init__(self, sat_host):
-        JSONRPCMethodManager.__init__(self, sat_host)
-        self.profiles_waiting={}
-        self.request=None
-
-    def getWaitingRequest(self, profile):
-        """Tell if a profile is trying to log in"""
-        if self.profiles_waiting.has_key(profile):
-            return self.profiles_waiting[profile]
-        else:
-            return None
-
-    def render(self, request):
-        """
-        Render method with some hacks:
-           - if login is requested, try to login with form data
-           - except login, every method is jsonrpc
-           - user doesn't need to be authentified for isRegistered or registerParams, but must be for all other methods
-        """
-        if request.postpath==['login']:
-            return self.login(request)
-        _session = request.getSession()
-        parsed = jsonrpclib.loads(request.content.read())
-        method = parsed.get("method")
-        if  method != "isRegistered" and method != "registerParams":
-            #if we don't call login or isRegistered, we need to be identified
-            profile = ISATSession(_session).profile
-            if not profile:
-                #user is not identified, we return a jsonrpc fault
-                fault = jsonrpclib.Fault(Const.ERRNUM_LIBERVIA, "Not allowed") #FIXME: define some standard error codes for libervia
-                return jsonrpc.JSONRPC._cbRender(self, fault, request, parsed.get('id'), parsed.get('jsonrpc'))
-        self.request = request
-        return jsonrpc.JSONRPC.render(self, request)
-
-    def login(self, request):
-        """
-        this method is called with the POST information from the registering form
-        it test if the password is ok, and log in if it's the case,
-        else it return an error
-        @param request: request of the register formulaire, must have "login" and "password" as arguments
-        @return: A constant indicating the state:
-            - BAD REQUEST: something is wrong in the request (bad arguments, profile_key for login)
-            - AUTH ERROR: either the profile or the password is wrong
-            - ALREADY WAITING: a request has already be made for this profile
-            - server.NOT_DONE_YET: the profile is being processed, the return value will be given by self._logged or self._logginError
-        """
-
-        try:
-            if request.args['submit_type'][0] == 'login':
-                _login = request.args['login'][0]
-                if _login.startswith('@'):
-                    raise Exception('No profile_key allowed')
-                _pass = request.args['login_password'][0]
-
-            elif request.args['submit_type'][0] == 'register':
-                return self._registerNewAccount(request)
-
-            else:
-                raise Exception('Unknown submit type')
-        except KeyError:
-            return "BAD REQUEST"
-
-        _profile_check = self.sat_host.bridge.getProfileName(_login)
-
-        def profile_pass_cb(_profile_pass):
-            if not _profile_check or _profile_check != _login or _profile_pass != _pass:
-                request.write("AUTH ERROR")
-                request.finish()
-                return
-
-            if self.profiles_waiting.has_key(_login):
-                request.write("ALREADY WAITING")
-                request.finish()
-                return
-
-            if self.sat_host.bridge.isConnected(_login):
-                request.write(self._logged(_login, request, finish=False))
-                request.finish()
-                return
-
-            self.profiles_waiting[_login] = request
-            d = self.asyncBridgeCall("asyncConnect", _login)
-            return d
-
-        def profile_pass_errback(ignore):
-            error("INTERNAL ERROR: can't check profile password")
-            request.write("AUTH ERROR")
-            request.finish()
-
-        d = self.asyncBridgeCall("asyncGetParamA", "Password", "Connection", profile_key=_login)
-        d.addCallbacks(profile_pass_cb, profile_pass_errback)
-
-        return server.NOT_DONE_YET
-
-    def _postAccountCreation(self, answer_type, id, data, profile):
-        """Called when a account has just been created,
-        setup stuff has microblog access"""
-        def _connected(ignore):
-            mblog_d = self.asyncBridgeCall("setMicroblogAccess", "open", profile)
-            mblog_d.addBoth(lambda ignore: self.sat_host.bridge.disconnect(profile))
-
-        d = self.asyncBridgeCall("asyncConnect", profile)
-        d.addCallback(_connected)
-
-    def _registerNewAccount(self, request):
-        """Create a new account, or return error
-        @param request: initial login request
-        @return: "REGISTRATION" in case of success"""
-        #TODO: must be moved in SàT core
-
-        try:
-            profile = login = request.args['register_login'][0]
-            password = request.args['register_password'][0] #FIXME: password is ignored so far
-            email = request.args['email'][0]
-        except KeyError:
-            return "BAD REQUEST"
-        if not re.match(r'^[a-z0-9_-]+$', login, re.IGNORECASE) or \
-           not re.match(r'^.+@.+\..+', email, re.IGNORECASE):
-            return "BAD REQUEST"
-
-        def registered(result):
-            request.write('REGISTRATION')
-            request.finish()
-
-        def registeringError(failure):
-            reason = str(failure.value)
-            if reason == "ConflictError":
-                request.write('ALREADY EXISTS')
-            elif reason == "InternalError":
-                request.write('INTERNAL')
-            else:
-                error('Unknown registering error: %s' % (reason,))
-                request.write('Unknown error (%s)' % reason)
-            request.finish()
-
-        d = self.asyncBridgeCall("registerSatAccount", email, password, profile)
-        d.addCallback(registered)
-        d.addErrback(registeringError)
-        return server.NOT_DONE_YET
-
-    def __cleanWaiting(self, login):
-        """Remove login from waiting queue"""
-        try:
-            del self.profiles_waiting[login]
-        except KeyError:
-            pass
-
-    def _logged(self, profile, request, finish=True):
-        """Set everything when a user just logged
-        and return "LOGGED" to the requester"""
-        def result(answer):
-            if finish:
-                request.write(answer)
-                request.finish()
-            else:
-                return answer
-
-        self.__cleanWaiting(profile)
-        _session = request.getSession()
-        sat_session = ISATSession(_session)
-        if sat_session.profile:
-            error (('/!\\ Session has already a profile, this should NEVER happen !'))
-            return result('SESSION_ACTIVE')
-        sat_session.profile = profile
-        self.sat_host.prof_connected.add(profile)
-
-        def onExpire():
-            info ("Session expired (profile=%s)" % (profile,))
-            try:
-                #We purge the queue
-                del self.sat_host.signal_handler.queue[profile]
-            except KeyError:
-                pass
-            #and now we disconnect the profile
-            self.sat_host.bridge.disconnect(profile)
-
-        _session.notifyOnExpire(onExpire)
-
-        d = defer.Deferred()
-        return result('LOGGED')
-
-    def _logginError(self, login, request, error_type):
-        """Something went wrong during loggin, return an error"""
-        self.__cleanWaiting(login)
-        return error_type
-
-    def jsonrpc_isConnected(self):
-        _session = self.request.getSession()
-        profile = ISATSession(_session).profile
-        return self.sat_host.bridge.isConnected(profile)
-
-    def jsonrpc_connect(self):
-        _session = self.request.getSession()
-        profile = ISATSession(_session).profile
-        if self.profiles_waiting.has_key(profile):
-            raise jsonrpclib.Fault(1,'Already waiting') #FIXME: define some standard error codes for libervia
-        self.profiles_waiting[profile] = self.request
-        self.sat_host.bridge.connect(profile)
-        return server.NOT_DONE_YET
-
-    def jsonrpc_isRegistered(self):
-        """Tell if the user is already registered"""
-        _session = self.request.getSession()
-        profile = ISATSession(_session).profile
-        return bool(profile)
-
-    def jsonrpc_registerParams(self):
-        """Register the frontend specific parameters"""
-        params = """
-        <params>
-        <individual>
-        <category name="%(category_name)s" label="%(category_label)s">
-            <param name="%(param_name)s" label="%(param_label)s" value="false" type="bool" security="0"/>
-         </category>
-        </individual>
-        </params>
-        """ % {
-            'category_name': Const.ENABLE_UNIBOX_KEY,
-            'category_label': _(Const.ENABLE_UNIBOX_KEY),
-            'param_name': Const.ENABLE_UNIBOX_PARAM,
-            'param_label': _(Const.ENABLE_UNIBOX_PARAM)
-        }
-
-        self.sat_host.bridge.paramsRegisterApp(params, Const.SECURITY_LIMIT, Const.APP_NAME)
-
-
-class SignalHandler(jsonrpc.JSONRPC):
-
-    def __init__(self, sat_host):
-        Resource.__init__(self)
-        self.register=None
-        self.sat_host=sat_host
-        self.signalDeferred = {}
-        self.queue = {}
-
-    def plugRegister(self, register):
-        self.register = register
-
-    def jsonrpc_getSignals(self):
-        """Keep the connection alive until a signal is received, then send it
-        @return: (signal, *signal_args)"""
-        _session = self.request.getSession()
-        profile = ISATSession(_session).profile
-        if profile in self.queue: #if we have signals to send in queue
-            if self.queue[profile]:
-                return self.queue[profile].pop(0)
-            else:
-                #the queue is empty, we delete the profile from queue
-                del self.queue[profile]
-        _session.lock() #we don't want the session to expire as long as this connection is active
-        def unlock(signal, profile):
-            _session.unlock()
-            try:
-                source_defer = self.signalDeferred[profile]
-                if source_defer.called and source_defer.result[0] == "disconnected":
-                    info(u"[%s] disconnected" % (profile,))
-                    _session.expire()
-            except IndexError:
-                error("Deferred result should be a tuple with fonction name first")
-
-        self.signalDeferred[profile] = defer.Deferred()
-        self.request.notifyFinish().addBoth(unlock, profile)
-        return self.signalDeferred[profile]
-
-    def getGenericCb(self, function_name):
-        """Return a generic function which send all params to signalDeferred.callback
-        function must have profile as last argument"""
-        def genericCb(*args):
-            profile = args[-1]
-            if not profile in self.sat_host.prof_connected:
-                return
-            if profile in self.signalDeferred:
-                self.signalDeferred[profile].callback((function_name,args[:-1]))
-                del self.signalDeferred[profile]
-            else:
-                if not self.queue.has_key(profile):
-                    self.queue[profile] = []
-                self.queue[profile].append((function_name, args[:-1]))
-        return genericCb
-
-    def connected(self, profile):
-        assert(self.register) #register must be plugged
-        request = self.register.getWaitingRequest(profile)
-        if request:
-            self.register._logged(profile, request)
-
-    def disconnected(self, profile):
-        if not profile in self.sat_host.prof_connected:
-            error("'disconnected' signal received for a not connected profile")
-            return
-        self.sat_host.prof_connected.remove(profile)
-        if profile in self.signalDeferred:
-            self.signalDeferred[profile].callback(("disconnected",))
-            del self.signalDeferred[profile]
-        else:
-            if not self.queue.has_key(profile):
-                self.queue[profile] = []
-            self.queue[profile].append(("disconnected",))
-
-
-    def connectionError(self, error_type, profile):
-        assert(self.register) #register must be plugged
-        request = self.register.getWaitingRequest(profile)
-        if request: #The user is trying to log in
-            if error_type == "AUTH_ERROR":
-                _error_t = "AUTH ERROR"
-            else:
-                _error_t = "UNKNOWN"
-            self.register._logginError(profile, request, _error_t)
-
-    def render(self, request):
-        """
-        Render method wich reject access if user is not identified
-        """
-        _session = request.getSession()
-        parsed = jsonrpclib.loads(request.content.read())
-        profile = ISATSession(_session).profile
-        if not profile:
-            #user is not identified, we return a jsonrpc fault
-            fault = jsonrpclib.Fault(Const.ERRNUM_LIBERVIA, "Not allowed") #FIXME: define some standard error codes for libervia
-            return jsonrpc.JSONRPC._cbRender(self, fault, request, parsed.get('id'), parsed.get('jsonrpc'))
-        self.request = request
-        return jsonrpc.JSONRPC.render(self, request)
-
-class UploadManager(Resource):
-    """This class manage the upload of a file
-    It redirect the stream to SàT core backend"""
-    isLeaf = True
-    NAME = 'path' #name use by the FileUpload
-
-    def __init__(self, sat_host):
-        self.sat_host=sat_host
-        self.upload_dir = tempfile.mkdtemp()
-        self.sat_host.addCleanup(shutil.rmtree, self.upload_dir)
-
-    def getTmpDir(self):
-        return self.upload_dir
-
-    def _getFileName(self, request):
-        """Generate unique filename for a file"""
-        raise NotImplementedError
-
-    def _fileWritten(self, request, filepath):
-        """Called once the file is actually written on disk
-        @param request: HTTP request object
-        @param filepath: full filepath on the server
-        @return: a tuple with the name of the async bridge method
-        to be called followed by its arguments.
-        """
-        raise NotImplementedError
-
-    def render(self, request):
-        """
-        Render method with some hacks:
-           - if login is requested, try to login with form data
-           - except login, every method is jsonrpc
-           - user doesn't need to be authentified for isRegistered, but must be for all other methods
-        """
-        filename = self._getFileName(request)
-        filepath = os.path.join(self.upload_dir, filename)
-        #FIXME: the uploaded file is fully loaded in memory at form parsing time so far
-        #       (see twisted.web.http.Request.requestReceived). A custom requestReceived should
-        #       be written in the futur. In addition, it is not yet possible to get progression informations
-        #       (see http://twistedmatrix.com/trac/ticket/288)
-
-        with open(filepath,'w') as f:
-            f.write(request.args[self.NAME][0])
-
-        def finish(d):
-            error = isinstance(d, Exception) or isinstance (d, Failure)
-            request.write('KO' if error else 'OK')
-            # TODO: would be great to re-use the original Exception class and message
-            # but it is lost in the middle of the backtrace and encapsulated within
-            # a DBusException instance --> extract the data from the backtrace?
-            request.finish()
-
-        d = JSONRPCMethodManager(self.sat_host).asyncBridgeCall(*self._fileWritten(request, filepath))
-        d.addCallbacks(lambda d: finish(d), lambda failure: finish(failure))
-        return server.NOT_DONE_YET
-
-
-class UploadManagerRadioCol(UploadManager):
-    NAME = 'song'
-
-    def _getFileName(self, request):
-        return "%s.ogg" % str(uuid.uuid4()) #XXX: chromium doesn't seem to play song without the .ogg extension, even with audio/ogg mime-type
-
-    def _fileWritten(self, request, filepath):
-        """Called once the file is actually written on disk
-        @param request: HTTP request object
-        @param filepath: full filepath on the server
-        @return: a tuple with the name of the async bridge method
-        to be called followed by its arguments.
-        """
-        profile = ISATSession(request.getSession()).profile
-        return ("radiocolSongAdded", request.args['referee'][0], filepath, profile)
-
-
-class UploadManagerAvatar(UploadManager):
-    NAME = 'avatar_path'
-
-    def _getFileName(self, request):
-        return str(uuid.uuid4())
-
-    def _fileWritten(self, request, filepath):
-        """Called once the file is actually written on disk
-        @param request: HTTP request object
-        @param filepath: full filepath on the server
-        @return: a tuple with the name of the async bridge method
-        to be called followed by its arguments.
-        """
-        profile = ISATSession(request.getSession()).profile
-        return ("setAvatar", filepath, profile)
-
-
-class Libervia(service.Service):
-
-    def __init__(self):
-        self._cleanup = []
-        root = ProtectedFile(Const.LIBERVIA_DIR)
-        self.signal_handler = SignalHandler(self)
-        _register = Register(self)
-        _upload_radiocol = UploadManagerRadioCol(self)
-        _upload_avatar = UploadManagerAvatar(self)
-        self.signal_handler.plugRegister(_register)
-        self.sessions = {} #key = session value = user
-        self.prof_connected = set() #Profiles connected
-        self.action_handler = SATActionIDHandler()
-        ## bridge ##
-        try:
-            self.bridge=DBusBridgeFrontend()
-        except BridgeExceptionNoService:
-            print(u"Can't connect to SàT backend, are you sure it's launched ?")
-            sys.exit(1)
-        self.bridge.register("connected", self.signal_handler.connected)
-        self.bridge.register("disconnected", self.signal_handler.disconnected)
-        self.bridge.register("connectionError", self.signal_handler.connectionError)
-        self.bridge.register("actionResult", self.action_handler.actionResultCb)
-        #core
-        for signal_name in ['presenceUpdate', 'newMessage', 'subscribe', 'contactDeleted', 'newContact', 'entityDataUpdated', 'askConfirmation', 'newAlert', 'paramUpdate']:
-            self.bridge.register(signal_name, self.signal_handler.getGenericCb(signal_name))
-        #plugins
-        for signal_name in ['personalEvent', 'roomJoined', 'roomUserJoined', 'roomUserLeft', 'tarotGameStarted', 'tarotGameNew', 'tarotGameChooseContrat',
-                            'tarotGameShowCards', 'tarotGameInvalidCards', 'tarotGameCardsPlayed', 'tarotGameYourTurn', 'tarotGameScore', 'tarotGamePlayers',
-                            'radiocolStarted', 'radiocolPreload', 'radiocolPlay', 'radiocolNoUpload', 'radiocolUploadOk', 'radiocolSongRejected', 'radiocolPlayers',
-                            'roomLeft', 'chatStateReceived']:
-            self.bridge.register(signal_name, self.signal_handler.getGenericCb(signal_name), "plugin")
-        self.media_dir = self.bridge.getConfig('','media_dir')
-        self.local_dir = self.bridge.getConfig('','local_dir')
-        root.putChild('', Redirect('libervia.html'))
-        root.putChild('json_signal_api', self.signal_handler)
-        root.putChild('json_api', MethodHandler(self))
-        root.putChild('register_api', _register)
-        root.putChild('upload_radiocol', _upload_radiocol)
-        root.putChild('upload_avatar', _upload_avatar)
-        root.putChild('blog', MicroBlog(self))
-        root.putChild('css', ProtectedFile("server_css/"))
-        root.putChild(os.path.dirname(Const.MEDIA_DIR), ProtectedFile(self.media_dir))
-        root.putChild(os.path.dirname(Const.AVATARS_DIR), ProtectedFile(os.path.join(self.local_dir, Const.AVATARS_DIR)))
-        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
-        self.site = server.Site(root)
-        self.site.sessionFactory = LiberviaSession
-
-    def addCleanup(self, callback, *args, **kwargs):
-        """Add cleaning method to call when service is stopped
-        cleaning method will be called in reverse order of they insertion
-        @param callback: callable to call on service stop
-        @param *args: list of arguments of the callback
-        @param **kwargs: list of keyword arguments of the callback"""
-        self._cleanup.insert(0, (callback, args, kwargs))
-
-    def startService(self):
-        reactor.listenTCP(8080, self.site)
-
-    def stopService(self):
-        print "launching cleaning methods"
-        for callback, args, kwargs in self._cleanup:
-            callback(*args, **kwargs)
-
-    def run(self):
-        reactor.run()
-
-    def stop(self):
-        reactor.stop()
-
-registerAdapter(SATSession, server.Session, ISATSession)
-application = service.Application(Const.APP_NAME)
-service = Libervia()
-service.setServiceParent(application)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libervia_server/__init__.py	Tue Feb 04 17:09:00 2014 +0100
@@ -0,0 +1,1008 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+"""
+Libervia: a Salut à Toi frontend
+Copyright (C) 2011, 2012, 2013 Jérôme Poisson <goffi@goffi.org>
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU Affero General Public License for more details.
+
+You should have received a copy of the GNU Affero General Public License
+along with this program.  If not, see <http://www.gnu.org/licenses/>.
+"""
+
+from twisted.application import internet, service
+from twisted.internet import glib2reactor
+glib2reactor.install()
+from twisted.internet import reactor, defer
+from twisted.web import server
+from twisted.web import error as weberror
+from twisted.web.static import File
+from twisted.web.resource import Resource, NoResource
+from twisted.web.util import Redirect
+from twisted.python.components import registerAdapter
+from twisted.python.failure import Failure
+from twisted.words.protocols.jabber.jid import JID
+from txjsonrpc.web import jsonrpc
+from txjsonrpc import jsonrpclib
+
+from logging import debug, info, warning, error
+import re, glob
+import os.path, sys
+import tempfile, shutil, uuid
+from zope.interface import Interface, Attribute, implements
+from xml.dom import minidom
+
+from constants import Const
+from libervia_server.blog import MicroBlog
+from sat_frontends.bridge.DBus import DBusBridgeFrontend, BridgeExceptionNoService
+from sat.core.i18n import _, D_
+
+
+class ISATSession(Interface):
+    profile = Attribute("Sat profile")
+    jid = Attribute("JID associated with the profile")
+
+class SATSession(object):
+    implements(ISATSession)
+    def __init__(self, session):
+        self.profile = None
+        self.jid = None
+
+class LiberviaSession(server.Session):
+    sessionTimeout = Const.TIMEOUT
+
+    def __init__(self, *args, **kwargs):
+        self.__lock = False
+        server.Session.__init__(self, *args, **kwargs)
+
+    def lock(self):
+        """Prevent session from expiring"""
+        self.__lock = True
+        self._expireCall.reset(sys.maxint)
+
+    def unlock(self):
+        """Allow session to expire again, and touch it"""
+        self.__lock = False
+        self.touch()
+
+    def touch(self):
+        if not self.__lock:
+            server.Session.touch(self)
+
+class ProtectedFile(File):
+    """A File class which doens't show directory listing"""
+
+    def directoryListing(self):
+        return NoResource()
+
+class SATActionIDHandler(object):
+    """Manage SàT action action_id lifecycle"""
+    ID_LIFETIME = 30 #after this time (in seconds), action_id will be suppressed and action result will be ignored
+
+    def __init__(self):
+        self.waiting_ids = {}
+
+    def waitForId(self, callback, action_id, profile, *args, **kwargs):
+        """Wait for an action result
+        @param callback: method to call when action gave a result back
+        @param action_id: action_id to wait for
+        @param profile: %(doc_profile)s
+        @param *args: additional argument to pass to callback
+        @param **kwargs: idem"""
+        action_tuple = (action_id, profile)
+        self.waiting_ids[action_tuple] = (callback, args, kwargs)
+        reactor.callLater(self.ID_LIFETIME, self.purgeID, action_tuple)
+
+    def purgeID(self, action_tuple):
+        """Called when an action_id has not be handled in time"""
+        if action_tuple in self.waiting_ids:
+            warning ("action of action_id %s [%s] has not been managed, action_id is now ignored" % action_tuple)
+            del self.waiting_ids[action_tuple]
+
+    def actionResultCb(self, answer_type, action_id, data, profile):
+        """Manage the actionResult signal"""
+        action_tuple = (action_id, profile)
+        if action_tuple in self.waiting_ids:
+            callback, args, kwargs = self.waiting_ids[action_tuple]
+            del self.waiting_ids[action_tuple]
+            callback(answer_type, action_id, data, *args, **kwargs)
+
+class JSONRPCMethodManager(jsonrpc.JSONRPC):
+
+    def __init__(self, sat_host):
+        jsonrpc.JSONRPC.__init__(self)
+        self.sat_host=sat_host
+
+    def asyncBridgeCall(self, method_name, *args, **kwargs):
+        """Call an asynchrone bridge method and return a deferred
+        @param method_name: name of the method as a unicode
+        @return: a deferred which trigger the result
+
+        """
+        d = defer.Deferred()
+
+        def _callback(*args):
+            if not args:
+                d.callback(None)
+            else:
+                if len(args) != 1:
+                    Exception("Multiple return arguments not supported")
+                d.callback(args[0])
+
+        def _errback(result):
+            d.errback(Failure(jsonrpclib.Fault(Const.ERRNUM_BRIDGE_ERRBACK, unicode(result))))
+
+        kwargs["callback"] = _callback
+        kwargs["errback"] = _errback
+        getattr(self.sat_host.bridge, method_name)(*args, **kwargs)
+        return d
+
+
+class MethodHandler(JSONRPCMethodManager):
+
+    def __init__(self, sat_host):
+        JSONRPCMethodManager.__init__(self, sat_host)
+        self.authorized_params = None
+
+    def render(self, request):
+        self.session = request.getSession()
+        profile = ISATSession(self.session).profile
+        if not profile:
+            #user is not identified, we return a jsonrpc fault
+            parsed = jsonrpclib.loads(request.content.read())
+            fault = jsonrpclib.Fault(Const.ERRNUM_LIBERVIA, "Not allowed") #FIXME: define some standard error codes for libervia
+            return jsonrpc.JSONRPC._cbRender(self, fault, request, parsed.get('id'), parsed.get('jsonrpc'))
+        return jsonrpc.JSONRPC.render(self, request)
+
+    def jsonrpc_getProfileJid(self):
+        """Return the jid of the profile"""
+        sat_session = ISATSession(self.session)
+        profile = sat_session.profile
+        sat_session.jid = JID(self.sat_host.bridge.getParamA("JabberID", "Connection", profile_key=profile))
+        return sat_session.jid.full()
+
+    def jsonrpc_disconnect(self):
+        """Disconnect the profile"""
+        sat_session = ISATSession(self.session)
+        profile = sat_session.profile
+        self.sat_host.bridge.disconnect(profile)
+
+    def jsonrpc_getContacts(self):
+        """Return all passed args."""
+        profile = ISATSession(self.session).profile
+        return self.sat_host.bridge.getContacts(profile)
+
+    def jsonrpc_addContact(self, entity, name, groups):
+        """Subscribe to contact presence, and add it to the given groups"""
+        profile = ISATSession(self.session).profile
+        self.sat_host.bridge.addContact(entity, profile)
+        self.sat_host.bridge.updateContact(entity, name, groups, profile)
+
+    def jsonrpc_delContact(self, entity):
+        """Remove contact from contacts list"""
+        profile = ISATSession(self.session).profile
+        self.sat_host.bridge.delContact(entity, profile)
+
+    def jsonrpc_updateContact(self, entity, name, groups):
+        """Update contact's roster item"""
+        profile = ISATSession(self.session).profile
+        self.sat_host.bridge.updateContact(entity, name, groups, profile)
+
+    def jsonrpc_subscription(self, sub_type, entity, name, groups):
+        """Confirm (or infirm) subscription,
+        and setup user roster in case of subscription"""
+        profile = ISATSession(self.session).profile
+        self.sat_host.bridge.subscription(sub_type, entity, profile)
+        if sub_type == 'subscribed':
+            self.sat_host.bridge.updateContact(entity, name, groups, profile)
+
+    def jsonrpc_getWaitingSub(self):
+        """Return list of room already joined by user"""
+        profile = ISATSession(self.session).profile
+        return self.sat_host.bridge.getWaitingSub(profile)
+
+    def jsonrpc_setStatus(self, presence, status):
+        """Change the presence and/or status
+        @param presence: value from ("", "chat", "away", "dnd", "xa")
+        @param status: any string to describe your status
+        """
+        profile = ISATSession(self.session).profile
+        self.sat_host.bridge.setPresence('', presence, 0, {'': status}, profile)
+
+
+    def jsonrpc_sendMessage(self, to_jid, msg, subject, type_, options={}):
+        """send message"""
+        profile = ISATSession(self.session).profile
+        return self.asyncBridgeCall("sendMessage", to_jid, msg, subject, type_, options, profile)
+
+    def jsonrpc_sendMblog(self, type_, dest, text, extra={}):
+        """ Send microblog message
+        @param type_: one of "PUBLIC", "GROUP"
+        @param dest: destinees (list of groups, ignored for "PUBLIC")
+        @param text: microblog's text
+        """
+        profile = ISATSession(self.session).profile
+        extra['allow_comments'] = 'True'
+
+        if not type_:  # auto-detect
+            type_ = "PUBLIC" if dest == [] else "GROUP"
+
+        if type_ in ("PUBLIC", "GROUP") and text:
+            if type_ == "PUBLIC":
+                #This text if for the public microblog
+                print "sending public blog"
+                return self.sat_host.bridge.sendGroupBlog("PUBLIC", [], text, extra, profile)
+            else:
+                print "sending group blog"
+                return self.sat_host.bridge.sendGroupBlog("GROUP", [dest], text, extra, profile)
+        else:
+            raise Exception("Invalid data")
+
+    def jsonrpc_deleteMblog(self, pub_data, comments):
+        """Delete a microblog node
+        @param pub_data: a tuple (service, comment node identifier, item identifier)
+        @param comments: comments node identifier (for main item) or False
+        """
+        profile = ISATSession(self.session).profile
+        return self.sat_host.bridge.deleteGroupBlog(pub_data, comments if comments else '', profile)
+
+    def jsonrpc_updateMblog(self, pub_data, comments, message, extra={}):
+        """Modify a microblog node
+        @param pub_data: a tuple (service, comment node identifier, item identifier)
+        @param comments: comments node identifier (for main item) or False
+        @param message: new message
+        @param extra: dict which option name as key, which can be:
+            - allow_comments: True to accept an other level of comments, False else (default: False)
+            - rich: if present, contain rich text in currently selected syntax
+        """
+        profile = ISATSession(self.session).profile
+        if comments:
+            extra['allow_comments'] = 'True'
+        return self.sat_host.bridge.updateGroupBlog(pub_data, comments if comments else '', message, extra, profile)
+
+    def jsonrpc_sendMblogComment(self, node, text, extra={}):
+        """ Send microblog message
+        @param node: url of the comments node
+        @param text: comment
+        """
+        profile = ISATSession(self.session).profile
+        if node and text:
+            return self.sat_host.bridge.sendGroupBlogComment(node, text, extra, profile)
+        else:
+            raise Exception("Invalid data")
+
+    def jsonrpc_getLastMblogs(self, publisher_jid, max_item):
+        """Get last microblogs posted by a contact
+        @param publisher_jid: jid of the publisher
+        @param max_item: number of items to ask
+        @return list of microblog data (dict)"""
+        profile = ISATSession(self.session).profile
+        d = self.asyncBridgeCall("getLastGroupBlogs", publisher_jid, max_item, profile)
+        return d
+
+    def jsonrpc_getMassiveLastMblogs(self, publishers_type, publishers_list, max_item):
+        """Get lasts microblogs posted by several contacts at once
+        @param publishers_type: one of "ALL", "GROUP", "JID"
+        @param publishers_list: list of publishers type (empty list of all, list of groups or list of jids)
+        @param max_item: number of items to ask
+        @return: dictionary key=publisher's jid, value=list of microblog data (dict)"""
+        profile = ISATSession(self.session).profile
+        d = self.asyncBridgeCall("getMassiveLastGroupBlogs", publishers_type, publishers_list, max_item, profile)
+        self.sat_host.bridge.massiveSubscribeGroupBlogs(publishers_type, publishers_list, profile)
+        return d
+
+    def jsonrpc_getMblogComments(self, service, node):
+        """Get all comments of given node
+        @param service: jid of the service hosting the node
+        @param node: comments node
+        """
+        profile = ISATSession(self.session).profile
+        d = self.asyncBridgeCall("getGroupBlogComments", service, node, profile)
+        return d
+
+
+    def jsonrpc_getPresenceStatus(self):
+        """Get Presence information for connected contacts"""
+        profile = ISATSession(self.session).profile
+        return self.sat_host.bridge.getPresenceStatus(profile)
+
+    def jsonrpc_getHistory(self, from_jid, to_jid, size, between):
+        """Return history for the from_jid/to_jid couple"""
+        sat_session = ISATSession(self.session)
+        profile = sat_session.profile
+        sat_jid = sat_session.jid
+        if not sat_jid:
+            error("No jid saved for this profile")
+            return {}
+        if JID(from_jid).userhost() != sat_jid.userhost() and JID(to_jid).userhost() != sat_jid.userhost():
+            error("Trying to get history from a different jid, maybe a hack attempt ?")
+            return {}
+        d = self.asyncBridgeCall("getHistory", from_jid, to_jid, size, between, profile)
+        def show(result_dbus):
+            result = []
+            for line in result_dbus:
+                #XXX: we have to do this stupid thing because Python D-Bus use its own types instead of standard types
+                #     and txJsonRPC doesn't accept D-Bus types, resulting in a empty query
+                timestamp, from_jid, to_jid, message, mess_type, extra = line
+                result.append((float(timestamp), unicode(from_jid), unicode(to_jid), unicode(message), unicode(mess_type), dict(extra)))
+            return result
+        d.addCallback(show)
+        return d
+
+    def jsonrpc_joinMUC(self, room_jid, nick):
+        """Join a Multi-User Chat room
+        @room_jid: leave empty string to generate a unique name
+        """
+        profile = ISATSession(self.session).profile
+        try:
+            if room_jid != "":
+                room_jid = JID(room_jid).userhost()
+        except:
+            warning('Invalid room jid')
+            return
+        d = self.asyncBridgeCall("joinMUC", room_jid, nick, {}, profile)
+        return d
+
+    def jsonrpc_inviteMUC(self, contact_jid, room_jid):
+        """Invite a user to a Multi-User Chat room"""
+        profile = ISATSession(self.session).profile
+        try:
+            room_jid = JID(room_jid).userhost()
+        except:
+            warning('Invalid room jid')
+            return
+        room_id = room_jid.split("@")[0]
+        service = room_jid.split("@")[1]
+        self.sat_host.bridge.inviteMUC(contact_jid, service, room_id, {}, profile)
+
+    def jsonrpc_mucLeave(self, room_jid):
+        """Quit a Multi-User Chat room"""
+        profile = ISATSession(self.session).profile
+        try:
+            room_jid = JID(room_jid)
+        except:
+            warning('Invalid room jid')
+            return
+        self.sat_host.bridge.mucLeave(room_jid.userhost(), profile)
+
+    def jsonrpc_getRoomsJoined(self):
+        """Return list of room already joined by user"""
+        profile = ISATSession(self.session).profile
+        return self.sat_host.bridge.getRoomsJoined(profile)
+
+    def jsonrpc_launchTarotGame(self, other_players, room_jid=""):
+        """Create a room, invite the other players and start a Tarot game
+        @param room_jid: leave empty string to generate a unique room name
+        """
+        profile = ISATSession(self.session).profile
+        try:
+            if room_jid != "":
+                room_jid = JID(room_jid).userhost()
+        except:
+            warning('Invalid room jid')
+            return
+        self.sat_host.bridge.tarotGameLaunch(other_players, room_jid, profile)
+
+    def jsonrpc_getTarotCardsPaths(self):
+        """Give the path of all the tarot cards"""
+        _join = os.path.join
+        _media_dir = _join(self.sat_host.media_dir,'')
+        return map(lambda x: _join(Const.MEDIA_DIR, x[len(_media_dir):]), glob.glob(_join(_media_dir, Const.CARDS_DIR, '*_*.png')));
+
+    def jsonrpc_tarotGameReady(self, player, referee):
+        """Tell to the server that we are ready to start the game"""
+        profile = ISATSession(self.session).profile
+        self.sat_host.bridge.tarotGameReady(player, referee, profile)
+
+    def jsonrpc_tarotGameContratChoosed(self, player_nick, referee, contrat):
+        """Tell to the server that we are ready to start the game"""
+        profile = ISATSession(self.session).profile
+        self.sat_host.bridge.tarotGameContratChoosed(player_nick, referee, contrat, profile)
+
+    def jsonrpc_tarotGamePlayCards(self, player_nick, referee, cards):
+        """Tell to the server the cards we want to put on the table"""
+        profile = ISATSession(self.session).profile
+        self.sat_host.bridge.tarotGamePlayCards(player_nick, referee, cards, profile)
+
+    def jsonrpc_launchRadioCollective(self, invited, room_jid=""):
+        """Create a room, invite people, and start a radio collective
+        @param room_jid: leave empty string to generate a unique room name
+        """
+        profile = ISATSession(self.session).profile
+        try:
+            if room_jid != "":
+                room_jid = JID(room_jid).userhost()
+        except:
+            warning('Invalid room jid')
+            return
+        self.sat_host.bridge.radiocolLaunch(invited, room_jid, profile)
+
+    def jsonrpc_getEntityData(self, jid, keys):
+        """Get cached data for an entit
+        @param jid: jid of contact from who we want data
+        @param keys: name of data we want (list)
+        @return: requested data"""
+        profile = ISATSession(self.session).profile
+        return self.sat_host.bridge.getEntityData(jid, keys, profile)
+
+    def jsonrpc_getCard(self, jid):
+        """Get VCard for entiry
+        @param jid: jid of contact from who we want data
+        @return: id to retrieve the profile"""
+        profile = ISATSession(self.session).profile
+        return self.sat_host.bridge.getCard(jid, profile)
+
+    def jsonrpc_getParamsUI(self):
+        """Return the parameters XML for profile"""
+        profile = ISATSession(self.session).profile
+        d = self.asyncBridgeCall("getParams", Const.SECURITY_LIMIT, Const.APP_NAME, profile)
+
+        def setAuthorizedParams(d):
+            if self.authorized_params is None:
+                self.authorized_params = {}
+                for cat in minidom.parseString(d.encode('utf-8')).getElementsByTagName("category"):
+                    params = cat.getElementsByTagName("param")
+                    params_list = [param.getAttribute("name") for param in params]
+                    self.authorized_params[cat.getAttribute("name")] = params_list
+            if self.authorized_params:
+                return d
+            else:
+                return None
+
+        d.addCallback(setAuthorizedParams)
+
+        from sat.tools.xml_tools import paramsXml2xmlUI
+        d.addCallback(lambda d: paramsXml2xmlUI(d) if d else "")
+
+        return d
+
+    def jsonrpc_asyncGetParamA(self, param, category, attribute="value"):
+        """Return the parameter value for profile"""
+        profile = ISATSession(self.session).profile
+        d = self.asyncBridgeCall("asyncGetParamA", param, category, attribute, Const.SECURITY_LIMIT, profile_key=profile)
+        return d
+
+    def jsonrpc_setParam(self, name, value, category):
+        profile = ISATSession(self.session).profile
+        if category in self.authorized_params and name in self.authorized_params[category]:
+            return self.sat_host.bridge.setParam(name, value, category, Const.SECURITY_LIMIT, profile)
+        else:
+            warning("Trying to set parameter '%s' in category '%s' without authorization!!!"
+                    % (name, category))
+
+    def jsonrpc_launchAction(self, callback_id, data):
+        profile = ISATSession(self.session).profile
+        d = self.asyncBridgeCall("launchAction", callback_id, data, profile)
+        return d
+
+    def jsonrpc_chatStateComposing(self, to_jid_s):
+        """Call the method to process a "composing" state.
+        @param to_jid_s: contact the user is composing to
+        """
+        profile = ISATSession(self.session).profile
+        self.sat_host.bridge.chatStateComposing(to_jid_s, profile)
+
+    def jsonrpc_getNewAccountDomain(self):
+        """@return: the domain for new account creation"""
+        d = self.asyncBridgeCall("getNewAccountDomain")
+        return d
+
+    def jsonrpc_confirmationAnswer(self, confirmation_id, result, answer_data):
+        """Send the user's answer to any previous 'askConfirmation' signal"""
+        profile = ISATSession(self.session).profile
+        self.sat_host.bridge.confirmationAnswer(confirmation_id, result, answer_data, profile)
+
+    def jsonrpc_syntaxConvert(self, text, syntax_from=Const.SYNTAX_XHTML, syntax_to=Const.SYNTAX_CURRENT):
+        """ Convert a text between two syntaxes
+        @param text: text to convert
+        @param syntax_from: source syntax (e.g. "markdown")
+        @param syntax_to: dest syntax (e.g.: "XHTML")
+        @param safe: clean resulting XHTML to avoid malicious code if True (forced here)
+        @return: converted text """
+        profile = ISATSession(self.session).profile
+        return self.sat_host.bridge.syntaxConvert(text, syntax_from, syntax_to, True, profile)
+
+
+class Register(JSONRPCMethodManager):
+    """This class manage the registration procedure with SàT
+    It provide an api for the browser, check password and setup the web server"""
+
+    def __init__(self, sat_host):
+        JSONRPCMethodManager.__init__(self, sat_host)
+        self.profiles_waiting={}
+        self.request=None
+
+    def getWaitingRequest(self, profile):
+        """Tell if a profile is trying to log in"""
+        if self.profiles_waiting.has_key(profile):
+            return self.profiles_waiting[profile]
+        else:
+            return None
+
+    def render(self, request):
+        """
+        Render method with some hacks:
+           - if login is requested, try to login with form data
+           - except login, every method is jsonrpc
+           - user doesn't need to be authentified for isRegistered or registerParams, but must be for all other methods
+        """
+        if request.postpath==['login']:
+            return self.login(request)
+        _session = request.getSession()
+        parsed = jsonrpclib.loads(request.content.read())
+        method = parsed.get("method")
+        if  method != "isRegistered" and method != "registerParams":
+            #if we don't call login or isRegistered, we need to be identified
+            profile = ISATSession(_session).profile
+            if not profile:
+                #user is not identified, we return a jsonrpc fault
+                fault = jsonrpclib.Fault(Const.ERRNUM_LIBERVIA, "Not allowed") #FIXME: define some standard error codes for libervia
+                return jsonrpc.JSONRPC._cbRender(self, fault, request, parsed.get('id'), parsed.get('jsonrpc'))
+        self.request = request
+        return jsonrpc.JSONRPC.render(self, request)
+
+    def login(self, request):
+        """
+        this method is called with the POST information from the registering form
+        it test if the password is ok, and log in if it's the case,
+        else it return an error
+        @param request: request of the register formulaire, must have "login" and "password" as arguments
+        @return: A constant indicating the state:
+            - BAD REQUEST: something is wrong in the request (bad arguments, profile_key for login)
+            - AUTH ERROR: either the profile or the password is wrong
+            - ALREADY WAITING: a request has already be made for this profile
+            - server.NOT_DONE_YET: the profile is being processed, the return value will be given by self._logged or self._logginError
+        """
+
+        try:
+            if request.args['submit_type'][0] == 'login':
+                _login = request.args['login'][0]
+                if _login.startswith('@'):
+                    raise Exception('No profile_key allowed')
+                _pass = request.args['login_password'][0]
+
+            elif request.args['submit_type'][0] == 'register':
+                return self._registerNewAccount(request)
+
+            else:
+                raise Exception('Unknown submit type')
+        except KeyError:
+            return "BAD REQUEST"
+
+        _profile_check = self.sat_host.bridge.getProfileName(_login)
+
+        def profile_pass_cb(_profile_pass):
+            if not _profile_check or _profile_check != _login or _profile_pass != _pass:
+                request.write("AUTH ERROR")
+                request.finish()
+                return
+
+            if self.profiles_waiting.has_key(_login):
+                request.write("ALREADY WAITING")
+                request.finish()
+                return
+
+            if self.sat_host.bridge.isConnected(_login):
+                request.write(self._logged(_login, request, finish=False))
+                request.finish()
+                return
+
+            self.profiles_waiting[_login] = request
+            d = self.asyncBridgeCall("asyncConnect", _login)
+            return d
+
+        def profile_pass_errback(ignore):
+            error("INTERNAL ERROR: can't check profile password")
+            request.write("AUTH ERROR")
+            request.finish()
+
+        d = self.asyncBridgeCall("asyncGetParamA", "Password", "Connection", profile_key=_login)
+        d.addCallbacks(profile_pass_cb, profile_pass_errback)
+
+        return server.NOT_DONE_YET
+
+    def _postAccountCreation(self, answer_type, id, data, profile):
+        """Called when a account has just been created,
+        setup stuff has microblog access"""
+        def _connected(ignore):
+            mblog_d = self.asyncBridgeCall("setMicroblogAccess", "open", profile)
+            mblog_d.addBoth(lambda ignore: self.sat_host.bridge.disconnect(profile))
+
+        d = self.asyncBridgeCall("asyncConnect", profile)
+        d.addCallback(_connected)
+
+    def _registerNewAccount(self, request):
+        """Create a new account, or return error
+        @param request: initial login request
+        @return: "REGISTRATION" in case of success"""
+        #TODO: must be moved in SàT core
+
+        try:
+            profile = login = request.args['register_login'][0]
+            password = request.args['register_password'][0] #FIXME: password is ignored so far
+            email = request.args['email'][0]
+        except KeyError:
+            return "BAD REQUEST"
+        if not re.match(r'^[a-z0-9_-]+$', login, re.IGNORECASE) or \
+           not re.match(r'^.+@.+\..+', email, re.IGNORECASE):
+            return "BAD REQUEST"
+
+        def registered(result):
+            request.write('REGISTRATION')
+            request.finish()
+
+        def registeringError(failure):
+            reason = str(failure.value)
+            if reason == "ConflictError":
+                request.write('ALREADY EXISTS')
+            elif reason == "InternalError":
+                request.write('INTERNAL')
+            else:
+                error('Unknown registering error: %s' % (reason,))
+                request.write('Unknown error (%s)' % reason)
+            request.finish()
+
+        d = self.asyncBridgeCall("registerSatAccount", email, password, profile)
+        d.addCallback(registered)
+        d.addErrback(registeringError)
+        return server.NOT_DONE_YET
+
+    def __cleanWaiting(self, login):
+        """Remove login from waiting queue"""
+        try:
+            del self.profiles_waiting[login]
+        except KeyError:
+            pass
+
+    def _logged(self, profile, request, finish=True):
+        """Set everything when a user just logged
+        and return "LOGGED" to the requester"""
+        def result(answer):
+            if finish:
+                request.write(answer)
+                request.finish()
+            else:
+                return answer
+
+        self.__cleanWaiting(profile)
+        _session = request.getSession()
+        sat_session = ISATSession(_session)
+        if sat_session.profile:
+            error (('/!\\ Session has already a profile, this should NEVER happen !'))
+            return result('SESSION_ACTIVE')
+        sat_session.profile = profile
+        self.sat_host.prof_connected.add(profile)
+
+        def onExpire():
+            info ("Session expired (profile=%s)" % (profile,))
+            try:
+                #We purge the queue
+                del self.sat_host.signal_handler.queue[profile]
+            except KeyError:
+                pass
+            #and now we disconnect the profile
+            self.sat_host.bridge.disconnect(profile)
+
+        _session.notifyOnExpire(onExpire)
+
+        d = defer.Deferred()
+        return result('LOGGED')
+
+    def _logginError(self, login, request, error_type):
+        """Something went wrong during loggin, return an error"""
+        self.__cleanWaiting(login)
+        return error_type
+
+    def jsonrpc_isConnected(self):
+        _session = self.request.getSession()
+        profile = ISATSession(_session).profile
+        return self.sat_host.bridge.isConnected(profile)
+
+    def jsonrpc_connect(self):
+        _session = self.request.getSession()
+        profile = ISATSession(_session).profile
+        if self.profiles_waiting.has_key(profile):
+            raise jsonrpclib.Fault(1,'Already waiting') #FIXME: define some standard error codes for libervia
+        self.profiles_waiting[profile] = self.request
+        self.sat_host.bridge.connect(profile)
+        return server.NOT_DONE_YET
+
+    def jsonrpc_isRegistered(self):
+        """Tell if the user is already registered"""
+        _session = self.request.getSession()
+        profile = ISATSession(_session).profile
+        return bool(profile)
+
+    def jsonrpc_registerParams(self):
+        """Register the frontend specific parameters"""
+        params = """
+        <params>
+        <individual>
+        <category name="%(category_name)s" label="%(category_label)s">
+            <param name="%(param_name)s" label="%(param_label)s" value="false" type="bool" security="0"/>
+         </category>
+        </individual>
+        </params>
+        """ % {
+            'category_name': Const.ENABLE_UNIBOX_KEY,
+            'category_label': _(Const.ENABLE_UNIBOX_KEY),
+            'param_name': Const.ENABLE_UNIBOX_PARAM,
+            'param_label': _(Const.ENABLE_UNIBOX_PARAM)
+        }
+
+        self.sat_host.bridge.paramsRegisterApp(params, Const.SECURITY_LIMIT, Const.APP_NAME)
+
+
+class SignalHandler(jsonrpc.JSONRPC):
+
+    def __init__(self, sat_host):
+        Resource.__init__(self)
+        self.register=None
+        self.sat_host=sat_host
+        self.signalDeferred = {}
+        self.queue = {}
+
+    def plugRegister(self, register):
+        self.register = register
+
+    def jsonrpc_getSignals(self):
+        """Keep the connection alive until a signal is received, then send it
+        @return: (signal, *signal_args)"""
+        _session = self.request.getSession()
+        profile = ISATSession(_session).profile
+        if profile in self.queue: #if we have signals to send in queue
+            if self.queue[profile]:
+                return self.queue[profile].pop(0)
+            else:
+                #the queue is empty, we delete the profile from queue
+                del self.queue[profile]
+        _session.lock() #we don't want the session to expire as long as this connection is active
+        def unlock(signal, profile):
+            _session.unlock()
+            try:
+                source_defer = self.signalDeferred[profile]
+                if source_defer.called and source_defer.result[0] == "disconnected":
+                    info(u"[%s] disconnected" % (profile,))
+                    _session.expire()
+            except IndexError:
+                error("Deferred result should be a tuple with fonction name first")
+
+        self.signalDeferred[profile] = defer.Deferred()
+        self.request.notifyFinish().addBoth(unlock, profile)
+        return self.signalDeferred[profile]
+
+    def getGenericCb(self, function_name):
+        """Return a generic function which send all params to signalDeferred.callback
+        function must have profile as last argument"""
+        def genericCb(*args):
+            profile = args[-1]
+            if not profile in self.sat_host.prof_connected:
+                return
+            if profile in self.signalDeferred:
+                self.signalDeferred[profile].callback((function_name,args[:-1]))
+                del self.signalDeferred[profile]
+            else:
+                if not self.queue.has_key(profile):
+                    self.queue[profile] = []
+                self.queue[profile].append((function_name, args[:-1]))
+        return genericCb
+
+    def connected(self, profile):
+        assert(self.register) #register must be plugged
+        request = self.register.getWaitingRequest(profile)
+        if request:
+            self.register._logged(profile, request)
+
+    def disconnected(self, profile):
+        if not profile in self.sat_host.prof_connected:
+            error("'disconnected' signal received for a not connected profile")
+            return
+        self.sat_host.prof_connected.remove(profile)
+        if profile in self.signalDeferred:
+            self.signalDeferred[profile].callback(("disconnected",))
+            del self.signalDeferred[profile]
+        else:
+            if not self.queue.has_key(profile):
+                self.queue[profile] = []
+            self.queue[profile].append(("disconnected",))
+
+
+    def connectionError(self, error_type, profile):
+        assert(self.register) #register must be plugged
+        request = self.register.getWaitingRequest(profile)
+        if request: #The user is trying to log in
+            if error_type == "AUTH_ERROR":
+                _error_t = "AUTH ERROR"
+            else:
+                _error_t = "UNKNOWN"
+            self.register._logginError(profile, request, _error_t)
+
+    def render(self, request):
+        """
+        Render method wich reject access if user is not identified
+        """
+        _session = request.getSession()
+        parsed = jsonrpclib.loads(request.content.read())
+        profile = ISATSession(_session).profile
+        if not profile:
+            #user is not identified, we return a jsonrpc fault
+            fault = jsonrpclib.Fault(Const.ERRNUM_LIBERVIA, "Not allowed") #FIXME: define some standard error codes for libervia
+            return jsonrpc.JSONRPC._cbRender(self, fault, request, parsed.get('id'), parsed.get('jsonrpc'))
+        self.request = request
+        return jsonrpc.JSONRPC.render(self, request)
+
+class UploadManager(Resource):
+    """This class manage the upload of a file
+    It redirect the stream to SàT core backend"""
+    isLeaf = True
+    NAME = 'path' #name use by the FileUpload
+
+    def __init__(self, sat_host):
+        self.sat_host=sat_host
+        self.upload_dir = tempfile.mkdtemp()
+        self.sat_host.addCleanup(shutil.rmtree, self.upload_dir)
+
+    def getTmpDir(self):
+        return self.upload_dir
+
+    def _getFileName(self, request):
+        """Generate unique filename for a file"""
+        raise NotImplementedError
+
+    def _fileWritten(self, request, filepath):
+        """Called once the file is actually written on disk
+        @param request: HTTP request object
+        @param filepath: full filepath on the server
+        @return: a tuple with the name of the async bridge method
+        to be called followed by its arguments.
+        """
+        raise NotImplementedError
+
+    def render(self, request):
+        """
+        Render method with some hacks:
+           - if login is requested, try to login with form data
+           - except login, every method is jsonrpc
+           - user doesn't need to be authentified for isRegistered, but must be for all other methods
+        """
+        filename = self._getFileName(request)
+        filepath = os.path.join(self.upload_dir, filename)
+        #FIXME: the uploaded file is fully loaded in memory at form parsing time so far
+        #       (see twisted.web.http.Request.requestReceived). A custom requestReceived should
+        #       be written in the futur. In addition, it is not yet possible to get progression informations
+        #       (see http://twistedmatrix.com/trac/ticket/288)
+
+        with open(filepath,'w') as f:
+            f.write(request.args[self.NAME][0])
+
+        def finish(d):
+            error = isinstance(d, Exception) or isinstance (d, Failure)
+            request.write('KO' if error else 'OK')
+            # TODO: would be great to re-use the original Exception class and message
+            # but it is lost in the middle of the backtrace and encapsulated within
+            # a DBusException instance --> extract the data from the backtrace?
+            request.finish()
+
+        d = JSONRPCMethodManager(self.sat_host).asyncBridgeCall(*self._fileWritten(request, filepath))
+        d.addCallbacks(lambda d: finish(d), lambda failure: finish(failure))
+        return server.NOT_DONE_YET
+
+
+class UploadManagerRadioCol(UploadManager):
+    NAME = 'song'
+
+    def _getFileName(self, request):
+        return "%s.ogg" % str(uuid.uuid4()) #XXX: chromium doesn't seem to play song without the .ogg extension, even with audio/ogg mime-type
+
+    def _fileWritten(self, request, filepath):
+        """Called once the file is actually written on disk
+        @param request: HTTP request object
+        @param filepath: full filepath on the server
+        @return: a tuple with the name of the async bridge method
+        to be called followed by its arguments.
+        """
+        profile = ISATSession(request.getSession()).profile
+        return ("radiocolSongAdded", request.args['referee'][0], filepath, profile)
+
+
+class UploadManagerAvatar(UploadManager):
+    NAME = 'avatar_path'
+
+    def _getFileName(self, request):
+        return str(uuid.uuid4())
+
+    def _fileWritten(self, request, filepath):
+        """Called once the file is actually written on disk
+        @param request: HTTP request object
+        @param filepath: full filepath on the server
+        @return: a tuple with the name of the async bridge method
+        to be called followed by its arguments.
+        """
+        profile = ISATSession(request.getSession()).profile
+        return ("setAvatar", filepath, profile)
+
+
+class Libervia(service.Service):
+
+    def __init__(self, port=8080):
+        self._cleanup = []
+        self.port = port
+        root = ProtectedFile(Const.LIBERVIA_DIR)
+        self.signal_handler = SignalHandler(self)
+        _register = Register(self)
+        _upload_radiocol = UploadManagerRadioCol(self)
+        _upload_avatar = UploadManagerAvatar(self)
+        self.signal_handler.plugRegister(_register)
+        self.sessions = {} #key = session value = user
+        self.prof_connected = set() #Profiles connected
+        self.action_handler = SATActionIDHandler()
+        ## bridge ##
+        try:
+            self.bridge=DBusBridgeFrontend()
+        except BridgeExceptionNoService:
+            print(u"Can't connect to SàT backend, are you sure it's launched ?")
+            sys.exit(1)
+        self.bridge.register("connected", self.signal_handler.connected)
+        self.bridge.register("disconnected", self.signal_handler.disconnected)
+        self.bridge.register("connectionError", self.signal_handler.connectionError)
+        self.bridge.register("actionResult", self.action_handler.actionResultCb)
+        #core
+        for signal_name in ['presenceUpdate', 'newMessage', 'subscribe', 'contactDeleted', 'newContact', 'entityDataUpdated', 'askConfirmation', 'newAlert', 'paramUpdate']:
+            self.bridge.register(signal_name, self.signal_handler.getGenericCb(signal_name))
+        #plugins
+        for signal_name in ['personalEvent', 'roomJoined', 'roomUserJoined', 'roomUserLeft', 'tarotGameStarted', 'tarotGameNew', 'tarotGameChooseContrat',
+                            'tarotGameShowCards', 'tarotGameInvalidCards', 'tarotGameCardsPlayed', 'tarotGameYourTurn', 'tarotGameScore', 'tarotGamePlayers',
+                            'radiocolStarted', 'radiocolPreload', 'radiocolPlay', 'radiocolNoUpload', 'radiocolUploadOk', 'radiocolSongRejected', 'radiocolPlayers',
+                            'roomLeft', 'chatStateReceived']:
+            self.bridge.register(signal_name, self.signal_handler.getGenericCb(signal_name), "plugin")
+        self.media_dir = self.bridge.getConfig('','media_dir')
+        self.local_dir = self.bridge.getConfig('','local_dir')
+        root.putChild('', Redirect('libervia.html'))
+        root.putChild('json_signal_api', self.signal_handler)
+        root.putChild('json_api', MethodHandler(self))
+        root.putChild('register_api', _register)
+        root.putChild('upload_radiocol', _upload_radiocol)
+        root.putChild('upload_avatar', _upload_avatar)
+        root.putChild('blog', MicroBlog(self))
+        root.putChild('css', ProtectedFile("server_css/"))
+        root.putChild(os.path.dirname(Const.MEDIA_DIR), ProtectedFile(self.media_dir))
+        root.putChild(os.path.dirname(Const.AVATARS_DIR), ProtectedFile(os.path.join(self.local_dir, Const.AVATARS_DIR)))
+        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
+        self.site = server.Site(root)
+        self.site.sessionFactory = LiberviaSession
+
+    def addCleanup(self, callback, *args, **kwargs):
+        """Add cleaning method to call when service is stopped
+        cleaning method will be called in reverse order of they insertion
+        @param callback: callable to call on service stop
+        @param *args: list of arguments of the callback
+        @param **kwargs: list of keyword arguments of the callback"""
+        self._cleanup.insert(0, (callback, args, kwargs))
+
+    def startService(self):
+        reactor.listenTCP(self.port, self.site)
+
+    def stopService(self):
+        print "launching cleaning methods"
+        for callback, args, kwargs in self._cleanup:
+            callback(*args, **kwargs)
+
+    def run(self):
+        reactor.run()
+
+    def stop(self):
+        reactor.stop()
+
+registerAdapter(SATSession, server.Session, ISATSession)
+application = service.Application(Const.APP_NAME)
+service = Libervia()
+service.setServiceParent(application)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libervia_server/blog.py	Tue Feb 04 17:09:00 2014 +0100
@@ -0,0 +1,100 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+"""
+Libervia: a Salut à Toi frontend
+Copyright (C) 2011, 2012, 2013 Jérôme Poisson <goffi@goffi.org>
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU Affero General Public License for more details.
+
+You should have received a copy of the GNU Affero General Public License
+along with this program.  If not, see <http://www.gnu.org/licenses/>.
+"""
+
+from sat_frontends.tools.strings import addURLToText
+from libervia_server.html_tools import sanitizeHtml
+from twisted.internet import reactor, defer
+from twisted.web import server
+from twisted.web.resource import Resource
+from twisted.words.protocols.jabber.jid import JID
+from datetime import datetime
+from constants import Const
+
+
+class MicroBlog(Resource):
+    isLeaf = True
+
+    ERROR_TEMPLATE = """
+                <html>
+                <head>
+                    <title>MICROBLOG ERROR</title>
+                </head>
+                <body>
+                    <h1 style='text-align: center; color: red;'>%s</h1>
+                </body>
+                </html>
+                """
+
+    def __init__(self, host):
+        self.host = host
+        Resource.__init__(self)
+        if not host.bridge.isConnected("libervia"):  # FIXME: hard coded value for test
+            host.bridge.connect("libervia")
+
+    def render_GET(self, request):
+        if not request.postpath:
+            return MicroBlog.ERROR_TEMPLATE % "You must indicate a nickname"
+        else:
+            prof_requested = request.postpath[0]
+            #TODO: char check: only use alphanumerical chars + some extra(_,-,...) here
+            prof_found = self.host.bridge.getProfileName(prof_requested)
+            if not prof_found or prof_found == 'libervia':
+                return MicroBlog.ERROR_TEMPLATE % "Invalid nickname"
+            else:
+                def got_jid(pub_jid_s):
+                    pub_jid = JID(pub_jid_s)
+                    d2 = defer.Deferred()
+                    d2.addCallbacks(self.render_html_blog, self.render_error_blog, [request, prof_found], None, [request, prof_found], None)
+                    self.host.bridge.getLastGroupBlogs(pub_jid.userhost(), 10, 'libervia', d2.callback, d2.errback)
+
+                d1 = defer.Deferred()
+                JID(self.host.bridge.asyncGetParamA('JabberID', 'Connection', 'value', Const.SERVER_SECURITY_LIMIT, prof_found, callback=d1.callback, errback=d1.errback))
+                d1.addCallbacks(got_jid)
+
+                return server.NOT_DONE_YET
+
+    def render_html_blog(self, mblog_data, request, profile):
+        user = sanitizeHtml(profile).encode('utf-8')
+        request.write("""
+            <html>
+            <head>
+                <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+                <link rel="stylesheet" type="text/css" href="../css/blog.css" />
+                <title>%(user)s's microblog</title>
+            </head>
+            <body>
+                <div class='mblog_title'>%(user)s</div>
+            """ % {'user': user})
+        #mblog_data.reverse()
+        for entry in mblog_data:
+            timestamp = float(entry.get('timestamp', 0))
+            _datetime = datetime.fromtimestamp(timestamp)
+            body = addURLToText(sanitizeHtml(entry['content'])).encode('utf-8') if 'xhtml' not in entry else entry['xhtml'].encode()
+            request.write("""<div class='mblog_entry'><span class='mblog_timestamp'>%(date)s</span>
+                          <span class='mblog_content'>%(content)s</span></div>""" % {
+                          'date': _datetime,
+                          'content': body})
+        request.write('</body></html>')
+        request.finish()
+
+    def render_error_blog(self, error, request, profile):
+        request.write(MicroBlog.ERROR_TEMPLATE % "Can't access requested data")
+        request.finish()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libervia_server/html_tools.py	Tue Feb 04 17:09:00 2014 +0100
@@ -0,0 +1,34 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+"""
+Libervia: a Salut à Toi frontend
+Copyright (C) 2011, 2012, 2013 Jérôme Poisson <goffi@goffi.org>
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU Affero General Public License for more details.
+
+You should have received a copy of the GNU Affero General Public License
+along with this program.  If not, see <http://www.gnu.org/licenses/>.
+"""
+
+def sanitizeHtml(text):
+    """Sanitize HTML by escaping everything"""
+    #this code comes from official python wiki: http://wiki.python.org/moin/EscapingHtml
+    html_escape_table = {
+        "&": "&amp;",
+        '"': "&quot;",
+        "'": "&apos;",
+        ">": "&gt;",
+        "<": "&lt;",
+        }
+
+    return "".join(html_escape_table.get(c,c) for c in text)
+
--- a/server_side/blog.py	Sat Jan 11 18:30:10 2014 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,100 +0,0 @@
-#!/usr/bin/python
-# -*- coding: utf-8 -*-
-
-"""
-Libervia: a Salut à Toi frontend
-Copyright (C) 2011, 2012, 2013 Jérôme Poisson <goffi@goffi.org>
-
-This program is free software: you can redistribute it and/or modify
-it under the terms of the GNU Affero General Public License as published by
-the Free Software Foundation, either version 3 of the License, or
-(at your option) any later version.
-
-This program is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-GNU Affero General Public License for more details.
-
-You should have received a copy of the GNU Affero General Public License
-along with this program.  If not, see <http://www.gnu.org/licenses/>.
-"""
-
-from sat_frontends.tools.strings import addURLToText
-from server_side.html_tools import sanitizeHtml
-from twisted.internet import reactor, defer
-from twisted.web import server
-from twisted.web.resource import Resource
-from twisted.words.protocols.jabber.jid import JID
-from datetime import datetime
-from constants import Const
-
-
-class MicroBlog(Resource):
-    isLeaf = True
-
-    ERROR_TEMPLATE = """
-                <html>
-                <head>
-                    <title>MICROBLOG ERROR</title>
-                </head>
-                <body>
-                    <h1 style='text-align: center; color: red;'>%s</h1>
-                </body>
-                </html>
-                """
-
-    def __init__(self, host):
-        self.host = host
-        Resource.__init__(self)
-        if not host.bridge.isConnected("libervia"):  # FIXME: hard coded value for test
-            host.bridge.connect("libervia")
-
-    def render_GET(self, request):
-        if not request.postpath:
-            return MicroBlog.ERROR_TEMPLATE % "You must indicate a nickname"
-        else:
-            prof_requested = request.postpath[0]
-            #TODO: char check: only use alphanumerical chars + some extra(_,-,...) here
-            prof_found = self.host.bridge.getProfileName(prof_requested)
-            if not prof_found or prof_found == 'libervia':
-                return MicroBlog.ERROR_TEMPLATE % "Invalid nickname"
-            else:
-                def got_jid(pub_jid_s):
-                    pub_jid = JID(pub_jid_s)
-                    d2 = defer.Deferred()
-                    d2.addCallbacks(self.render_html_blog, self.render_error_blog, [request, prof_found], None, [request, prof_found], None)
-                    self.host.bridge.getLastGroupBlogs(pub_jid.userhost(), 10, 'libervia', d2.callback, d2.errback)
-
-                d1 = defer.Deferred()
-                JID(self.host.bridge.asyncGetParamA('JabberID', 'Connection', 'value', Const.SERVER_SECURITY_LIMIT, prof_found, callback=d1.callback, errback=d1.errback))
-                d1.addCallbacks(got_jid)
-
-                return server.NOT_DONE_YET
-
-    def render_html_blog(self, mblog_data, request, profile):
-        user = sanitizeHtml(profile).encode('utf-8')
-        request.write("""
-            <html>
-            <head>
-                <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
-                <link rel="stylesheet" type="text/css" href="../css/blog.css" />
-                <title>%(user)s's microblog</title>
-            </head>
-            <body>
-                <div class='mblog_title'>%(user)s</div>
-            """ % {'user': user})
-        #mblog_data.reverse()
-        for entry in mblog_data:
-            timestamp = float(entry.get('timestamp', 0))
-            _datetime = datetime.fromtimestamp(timestamp)
-            body = addURLToText(sanitizeHtml(entry['content'])).encode('utf-8') if 'xhtml' not in entry else entry['xhtml'].encode()
-            request.write("""<div class='mblog_entry'><span class='mblog_timestamp'>%(date)s</span>
-                          <span class='mblog_content'>%(content)s</span></div>""" % {
-                          'date': _datetime,
-                          'content': body})
-        request.write('</body></html>')
-        request.finish()
-
-    def render_error_blog(self, error, request, profile):
-        request.write(MicroBlog.ERROR_TEMPLATE % "Can't access requested data")
-        request.finish()
--- a/server_side/html_tools.py	Sat Jan 11 18:30:10 2014 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,34 +0,0 @@
-#!/usr/bin/python
-# -*- coding: utf-8 -*-
-
-"""
-Libervia: a Salut à Toi frontend
-Copyright (C) 2011, 2012, 2013 Jérôme Poisson <goffi@goffi.org>
-
-This program is free software: you can redistribute it and/or modify
-it under the terms of the GNU Affero General Public License as published by
-the Free Software Foundation, either version 3 of the License, or
-(at your option) any later version.
-
-This program is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-GNU Affero General Public License for more details.
-
-You should have received a copy of the GNU Affero General Public License
-along with this program.  If not, see <http://www.gnu.org/licenses/>.
-"""
-
-def sanitizeHtml(text):
-    """Sanitize HTML by escaping everything"""
-    #this code comes from official python wiki: http://wiki.python.org/moin/EscapingHtml
-    html_escape_table = {
-        "&": "&amp;",
-        '"': "&quot;",
-        "'": "&apos;",
-        ">": "&gt;",
-        "<": "&lt;",
-        }
-
-    return "".join(html_escape_table.get(c,c) for c in text)
-
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/twisted/plugins/libervia.py	Tue Feb 04 17:09:00 2014 +0100
@@ -0,0 +1,61 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+"""
+Libervia: a Salut à Toi frontend
+Copyright (C) 2013  Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU Affero General Public License for more details.
+
+You should have received a copy of the GNU Affero General Public License
+along with this program.  If not, see <http://www.gnu.org/licenses/>.
+"""
+
+from zope.interface import implements
+
+from twisted.python import usage
+from twisted.plugin import IPlugin
+from twisted.application.service import IServiceMaker
+from twisted.application import internet
+
+from xdg.BaseDirectory import save_config_path
+from ConfigParser import SafeConfigParser, NoSectionError
+from os.path import expanduser
+
+from libervia_server import Libervia
+
+
+class Options(usage.Options):
+    optParameters = [['port', 'p', 8080, 'The port number to listen on.']]
+
+
+class LiberviaMaker(object):
+    implements(IServiceMaker, IPlugin)
+    tapname = 'libervia'
+    description = 'The web frontend of Salut à Toi'
+    options = Options
+
+    def makeService(self, options):
+        if not isinstance(options['port'], int):
+            port = int(options['port'])
+        else:
+            try:
+                port = config.getint('libervia', 'port')
+            except NoSectionError:
+                port = 8080
+        return Libervia(port=port)
+
+
+config_path = save_config_path('sat')
+config = SafeConfigParser()
+config.read(map(expanduser, ['/etc/sat.conf', config_path + '/sat.conf', 'sat.conf', '.sat.conf']))
+
+serviceMaker = LiberviaMaker()