view browser/sat_browser/json.py @ 1128:6414fd795df4

server, pages: multi-sites refactoring: Libervia is now handling external sites (i.e. other sites than Libervia official site). The external site are declared in sites_path_public_dict (in [DEFAULT] section) which is read by template engine, then they are linked to virtual host with vhosts_dict (linking host name to site name) in [libervia] section. Sites are only instanced once, so adding an alias is just a matter of mapping the alias host name in vhosts_dict with the same site name. menu_json and url_redirections_dict can now accept keys named after site name, which will be linked to the data for the site. Data for default site can still be keyed at first level. Libervia official pages are added to external site (if pages are not overriden), allowing to call pages of the framework and to have facilities like login handling. Deprecated url_redirections_profile option has been removed.
author Goffi <goffi@goffi.org>
date Fri, 14 Sep 2018 21:41:28 +0200
parents 28e3eb3bb217
children 2af117bfe6cc
line wrap: on
line source

#!/usr/bin/python
# -*- coding: utf-8 -*-

# Libervia: a Salut à Toi frontend
# Copyright (C) 2011-2018 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/>.


### logging configuration ###
from sat.core.log import getLogger
log = getLogger(__name__)
###

from pyjamas.Timer import Timer
from pyjamas import Window
from pyjamas import JSONService
import time
from sat_browser import main_panel

from sat_browser.constants import Const as C
import random


class LiberviaMethodProxy(object):
    """This class manage calling for one method"""

    def __init__(self, parent, method):
        self._parent = parent
        self._method = method

    def call(self, *args, **kwargs):
        """Method called when self._method attribue is used in JSON_PROXY_PARENT

        This method manage callback/errback in kwargs, and profile(_key) removing
        @param *args: positional arguments of self._method
        @param **kwargs: keyword arguments of self._method
        """
        callback=kwargs.pop('callback', None)
        errback=kwargs.pop('errback', None)

        # as profile is linked to browser session and managed server side, we remove them
        profile_removed = False
        try:
            kwargs['profile'] # FIXME: workaround for pyjamas bug: KeyError is not raised with del
            del kwargs['profile']
            profile_removed = True
        except KeyError:
            pass

        try:
            kwargs['profile_key'] # FIXME: workaround for pyjamas bug: KeyError is not raised iwith del
            del kwargs['profile_key']
            profile_removed = True
        except KeyError:
            pass

        if not profile_removed and args:
            # if profile was not in kwargs, there is most probably one in args
            args = list(args)
            assert isinstance(args[-1], basestring) # Detect when we want to remove a callback (or something else) instead of the profile
            del args[-1]

        if kwargs:
            # kwargs should be empty here, we don't manage keyword arguments on bridge calls
            log.error(u"kwargs is not empty after treatment on method call: kwargs={}".format(kwargs))

        id_ = self._parent.callMethod(self._method, args)

        # callback or errback are managed in parent LiberviaJsonProxy with call id
        if callback is not None:
            self._parent.cb[id_] = callback
        if errback is not None:
            self._parent.eb[id_] = errback


class LiberviaJsonProxy(JSONService.JSONService):

    def __init__(self, url, methods):
        self._serviceURL = url
        self.methods = methods
        JSONService.JSONService.__init__(self, url, self)
        self.cb = {}
        self.eb = {}
        self._registerMethods(methods)

    def _registerMethods(self, methods):
        if methods:
            for method in methods:
                log.debug(u"Registering JSON method call [{}]".format(method))
                setattr(self,
                        method,
                        getattr(LiberviaMethodProxy(self, method), 'call')
                       )

    def callMethod(self, method, params, handler = None):
        ret = super(LiberviaJsonProxy, self).callMethod(method, params, handler)
        return ret

    def call(self, method, cb, *args):
        # FIXME: deprecated call method, must be removed once it's not used anymore
        id_ = self.callMethod(method, args)
        log.debug(u"call: method={} [id={}], args={}".format(method, id_, args))
        if cb:
            if isinstance(cb, tuple):
                if len(cb) != 2:
                    log.error("tuple syntax for bridge.call is (callback, errback), aborting")
                    return
                if cb[0] is not None:
                    self.cb[id_] = cb[0]
                self.eb[id_] = cb[1]
            else:
                self.cb[id_] = cb

    def onRemoteResponse(self, response, request_info):
        try:
            _cb = self.cb[request_info.id]
        except KeyError:
            pass
        else:
            _cb(response)
            del self.cb[request_info.id]

        try:
            del self.eb[request_info.id]
        except KeyError:
            pass

    def onRemoteError(self, code, errobj, request_info):
        """def dump(obj):
            print "\n\nDUMPING %s\n\n" % obj
            for i in dir(obj):
                print "%s: %s" % (i, getattr(obj,i))"""
        try:
            _eb = self.eb[request_info.id]
        except KeyError:
            if code != 0:
                log.error("Internal server error")
                """for o in code, error, request_info:
                    dump(o)"""
            else:
                if isinstance(errobj['message'], dict):
                    log.error(u"Error %s: %s" % (errobj['message']['faultCode'], errobj['message']['faultString']))
                else:
                    log.error(u"%s" % errobj['message'])
        else:
            _eb((code, errobj))
            del self.eb[request_info.id]

        try:
            del self.cb[request_info.id]
        except KeyError:
            pass


class RegisterCall(LiberviaJsonProxy):
    def __init__(self):
        LiberviaJsonProxy.__init__(self, "/register_api",
                        ["getSessionMetadata", "isConnected", "connect", "registerParams", "menusGet"])


class BridgeCall(LiberviaJsonProxy):
    def __init__(self):
        LiberviaJsonProxy.__init__(self, "/json_api",
                        ["getContacts", "addContact", "messageSend",
                         "psNodeDelete", "psRetractItem", "psRetractItems",
                         "mbSend", "mbRetract", "mbGet", "mbGetFromMany", "mbGetFromManyRTResult",
                         "mbGetFromManyWithComments", "mbGetFromManyWithCommentsRTResult",
                         "historyGet", "getPresenceStatuses", "joinMUC", "mucLeave", "mucGetRoomsJoined",
                         "inviteMUC", "launchTarotGame", "getTarotCardsPaths", "tarotGameReady",
                         "tarotGamePlayCards", "launchRadioCollective",
                         "getWaitingSub", "subscription", "delContact", "updateContact", "avatarGet",
                         "getEntityData", "getParamsUI", "asyncGetParamA", "setParam", "launchAction",
                         "disconnect", "chatStateComposing", "getNewAccountDomain",
                         "syntaxConvert", "getAccountDialogUI", "getMainResource", "getEntitiesData",
                         "getVersion", "getLiberviaVersion", "mucGetDefaultService", "getFeatures",
                         "namespacesGet",
                        ])

    def __call__(self, *args, **kwargs):
        return LiberviaJsonProxy.__call__(self, *args, **kwargs)

    def getConfig(self, dummy1, dummy2): # FIXME
        log.warning("getConfig is not implemeted in Libervia yet")
        return ''

    def isConnected(self, dummy, callback): # FIXME
        log.warning("isConnected is not implemeted in Libervia as for now profile is connected if session is opened")
        callback(True)

    def encryptionPluginsGet(self, callback, errback):
        """e2e encryption have no sense if made on backend, so we ignore this call"""
        callback([])

    def bridgeConnect(self, callback, errback):
        callback()


class BridgeSignals(LiberviaJsonProxy):

    def __init__(self, host):
        self.host = host
        self.retry_time = None
        self.retry_nb = 0
        self.retry_warning = None
        self.retry_timer = None
        LiberviaJsonProxy.__init__(self, "/json_signal_api",
                        ["getSignals"])
        self._signals = {} # key: signal name, value: callback

    def onRemoteResponse(self, response, request_info):
        if self.retry_time:
            log.info("Connection with server restablished")
            self.retry_nb = 0
            self.retry_time = None
        LiberviaJsonProxy.onRemoteResponse(self, response, request_info)

    def onRemoteError(self, code, errobj, request_info):
        if errobj['message'] == 'Empty Response':
            log.warning(u"Empty reponse bridgeSignal\ncode={}\nrequest_info: id={} method={} handler={}".format(code, request_info.id, request_info.method, request_info.handler))
            # FIXME: to check/replace by a proper session end on disconnected signal
            # Window.getLocation().reload()  # XXX: reset page in case of session ended.
                                           # FIXME: Should be done more properly without hard reload
        LiberviaJsonProxy.onRemoteError(self, code, errobj, request_info)
        #we now try to reconnect
        if isinstance(errobj['message'], dict) and errobj['message']['faultCode'] == 0:
            Window.alert('You are not allowed to connect to server')
        else:
            def _timerCb(dummy):
                current = time.time()
                if current > self.retry_time:
                    msg = "Trying to reconnect to server..."
                    log.info(msg)
                    self.retry_warning.showWarning("INFO", msg)
                    self.retry_timer.cancel()
                    self.retry_warning = self.retry_timer = None
                    self.getSignals(callback=self.signalHandler, profile=None)
                else:
                    remaining = int(self.retry_time - current)
                    msg_html = u"Connection with server lost. Retrying in <strong>{}</strong> s".format(remaining)
                    self.retry_warning.showWarning("WARNING", msg_html, None)

            if self.retry_nb < 3:
                retry_delay = 1
            elif self.retry_nb < 10:
                retry_delay = random.randint(1,10)
            else:
                retry_delay = random.randint(1,60)
            self.retry_nb += 1
            log.warning(u"Lost connection, trying to reconnect in {} s (try #{})".format(retry_delay, self.retry_nb))
            self.retry_time = time.time() + retry_delay
            self.retry_warning = main_panel.WarningPopup()
            self.retry_timer = Timer(notify=_timerCb)
            self.retry_timer.scheduleRepeating(1000)
            _timerCb(None)

    def register_signal(self, name, callback, with_profile=True):
        """Register a signal

        @param: name of the signal to register
        @param callback: method to call
        @param with_profile: True if the original bridge method need a profile
        """
        log.debug(u"Registering signal {}".format(name))
        if name in self._signals:
            log.error(u"Trying to register and already registered signal ({})".format(name))
        else:
            self._signals[name] = (callback, with_profile)

    def signalHandler(self,  signal_data):
        self.getSignals(callback=self.signalHandler, profile=None)
        if len(signal_data) == 1:
            signal_data.append([])
        log.debug(u"Got signal ==> name: %s, params: %s" % (signal_data[0], signal_data[1]))
        name, args = signal_data
        try:
            callback, with_profile = self._signals[name]
        except KeyError:
            log.warning(u"Ignoring {} signal: no handler registered !".format(name))
            return
        if with_profile:
            args.append(C.PROF_KEY_NONE)
        if not self.host._profile_plugged:
            log.debug("Profile is not plugged, we cache the signal")
            self.host.signals_cache[C.PROF_KEY_NONE].append((name, callback, args, {}))
        else:
            callback(*args)