view libervia.py @ 215:e830a0c60d32

server side: added the security_limit to setParam - in addition to the check which is done by the core, libervia checks if the param to be modified was really part of the XML that has been returned by getParams with security_limit = 0.
author souliane <souliane@mailoo.org>
date Sat, 07 Sep 2013 02:07:07 +0200
parents 8bbac49765d6
children 4e6467efd6bf
line wrap: on
line source

#!/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/>.
"""

import pyjd # this is dummy in pyjs
from pyjamas.ui.RootPanel import RootPanel
from pyjamas.ui.HTML import HTML
from pyjamas.ui.KeyboardListener import KEY_ESCAPE
from pyjamas.Timer import Timer
from pyjamas import Window, DOM
from pyjamas.JSONService import JSONProxy
from browser_side.register import RegisterBox
from browser_side.contact import ContactPanel
from browser_side.base_widget import WidgetsPanel
from browser_side.panels import MicroblogItem
from browser_side import panels, dialog
from browser_side.jid import JID
from browser_side.tools import html_sanitize

MAX_MBLOG_CACHE = 500 #Max microblog entries kept in memories

class LiberviaJsonProxy(JSONProxy):
    def __init__(self, *args, **kwargs):
        JSONProxy.__init__(self, *args, **kwargs)
        self.handler=self
        self.cb={}
        self.eb={}

    def call(self, method, cb, *args):
        _id = self.callMethod(method,args)
        if cb:
            if isinstance(cb, tuple):
                if len(cb) != 2:
                    print ("ERROR: tuple syntax for bridge.call is (callback, errback), aborting")
                    return
                self.cb[_id] = cb[0]
                self.eb[_id] = cb[1]
            else:
                self.cb[_id] = cb

    def onRemoteResponse(self, response, request_info):
        if request_info.id in self.cb:
            _cb = self.cb[request_info.id]
            # if isinstance(_cb, tuple):
            #     #we have arguments attached to the callback
            #     #we send them after the answer
            #     callback, args = _cb
            #     callback(response, *args)
            # else:
            #     #No additional argument, we call directly the callback
            _cb(response)
            del self.cb[request_info.id]
            if request_info.id in self.eb:
                del self.eb[request_info.id]
    
    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))"""
        if request_info.id in self.eb:
            _eb = self.eb[request_info.id]
            _eb((code, errobj))
            del self.cb[request_info.id]
            del self.eb[request_info.id]
        else:
            if code != 0:
                print ("Internal server error")
                """for o in code, error, request_info:
                    dump(o)"""
            else:
                if isinstance(errobj['message'],dict):
                   print("Error %s: %s" % (errobj['message']['faultCode'], errobj['message']['faultString']))
                else:
                   print("Error: %s" % errobj['message'])

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

class BridgeCall(LiberviaJsonProxy):
    def __init__(self):
        LiberviaJsonProxy.__init__(self, "/json_api",
                        ["getContacts", "addContact", "sendMessage", "sendMblog", "sendMblogComment",
                         "getLastMblogs", "getMassiveLastMblogs", "getMblogComments", "getProfileJid",
                         "getHistory", "getPresenceStatus", "joinMUC", "mucLeave", "getRoomsJoined",
                         "launchTarotGame", "getTarotCardsPaths", "tarotGameReady",
                         "tarotGameContratChoosed", "tarotGamePlayCards", "launchRadioCollective",
                         "getWaitingSub", "subscription", "delContact", "updateContact", "getCard",
                         "getEntityData", "getParamsUI", "setParam", "launchAction", "disconnect",
                         "chatStateComposing"
                        ])

class BridgeSignals(LiberviaJsonProxy):
    RETRY_BASE_DELAY = 1000

    def __init__(self, host):
        self.host = host
        self.retry_delay = self.RETRY_BASE_DELAY 
        LiberviaJsonProxy.__init__(self, "/json_signal_api",
                        ["getSignals"])

    def onRemoteResponse(self, response, request_info):
        self.retry_delay = self.RETRY_BASE_DELAY 
        LiberviaJsonProxy.onRemoteResponse(self, response, request_info)


    def onRemoteError(self, code, errobj, request_info):
        if errobj['message'] == 'Empty Response':
            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():
                self.host.bridge_signals.call('getSignals', self.host._getSignalsCB)
            Timer(notify=_timerCb).schedule(self.retry_delay)
            self.retry_delay *= 2

class SatWebFrontend:
    def onModuleLoad(self):
        print "============ onModuleLoad =============="
        panels.ChatPanel.registerClass()
        panels.MicroblogPanel.registerClass()
        self.whoami = None
        self._selected_listeners = set()
        self.bridge = BridgeCall()
        self.bridge_signals = BridgeSignals(self)
        self.uni_box = None
        self.status_panel = panels.StatusPanel(self)
        self.contact_panel = ContactPanel(self)
        self.panel = panels.MainPanel(self)
        self.discuss_panel = self.panel.discuss_panel
        self.tab_panel = self.panel.tab_panel
        self.tab_panel.addTabListener(self)
        self.libervia_widgets = set() #keep track of all actives LiberviaWidgets
        self.room_list = set() #set of rooms 
        self.mblog_cache = [] #used to keep our own blog entries in memory, to show them in new mblog panel
        self.avatars_cache = {} #keep track of jid's avatar hash (key=jid, value=file)
        self.current_action_ids = set()
        #self.discuss_panel.addWidget(panels.EmptyPanel(self))
        self.discuss_panel.addWidget(panels.MicroblogPanel(self, []))
        #self.discuss_panel.addWidget(panels.EmptyPanel(self))
        self._register_box = None
        RootPanel().add(self.panel)
        DOM.addEventPreview(self)
        self.resize()
        self._register = RegisterCall()
        self._register.call('isRegistered',self._isRegisteredCB)
        self.initialised = False
        self.init_cache = [] # used to cache events until initialisation is done

    def addSelectedListener(self, callback):
        self._selected_listeners.add(callback)

    def getSelected(self):
        wid = self.tab_panel.getCurrentPanel()
        if not isinstance(wid, WidgetsPanel):
            print "ERROR: Tab widget is not a WidgetsPanel, can't get selected widget"
            return None
        return wid.selected

    def setSelected(self, widget):
        """Define the selected widget"""
        widgets_panel = self.tab_panel.getCurrentPanel()
        if not isinstance(widgets_panel, WidgetsPanel):
            return

        selected = widgets_panel.selected

        if selected == widget:
            return

        if selected:
            selected.removeStyleName('selected_widget')
    
        widgets_panel.selected = widget
    
        if widget:
            widgets_panel.selected.addStyleName('selected_widget')

        for callback in self._selected_listeners:
            callback(widget)

    def resize(self):
        """Resize elements"""
        Window.onResize()

    def onBeforeTabSelected(self, sender, tab_index):
        return True

    def onTabSelected(self, sender, tab_index):
        selected = self.getSelected()
        for callback in self._selected_listeners:
            callback(selected)

    def onEventPreview(self, event):
        if event.type in ["keydown", "keypress", "keyup"] and event.keyCode == KEY_ESCAPE:
            #needed to prevent request cancellation in Firefox
            event.preventDefault()
        return True

    def getAvatar(self, jid_str):
        """Return avatar of a jid if in cache, else ask for it"""
        def dataReceived(result):
            if 'avatar' in result:
                self._entityDataUpdatedCb(jid_str, 'avatar', result['avatar'])
            else:
                self.bridge.call("getCard", None, jid_str)
        def avatarError(error_data):
            # The jid is maybe not in our roster, we ask for the VCard
            self.bridge.call("getCard", None, jid_str)
    
        if jid_str not in self.avatars_cache:
            self.bridge.call('getEntityData', (dataReceived, avatarError), jid_str, ['avatar'])
            self.avatars_cache[jid_str] = "/media/misc/empty_avatar"
        return self.avatars_cache[jid_str]

    def registerWidget(self, wid):
        print "Registering", wid
        self.libervia_widgets.add(wid)

    def unregisterWidget(self, wid):
        try:
            self.libervia_widgets.remove(wid)
        except KeyError:
            print ('WARNING: trying to remove a non registered Widget:', wid)

    def setUniBox(self, unibox):
        """register the unibox widget"""
        self.uni_box = unibox
        self.uni_box.addKey("@@: ")

    def addTab(self, wid, label):
        """Create a new tab and add a widget in
        @param wid: LiberviaWidget to add
        @param label: label of the tab"""
        _widgets_panel = WidgetsPanel(self)
        _widgets_panel.addWidget(wid)
        self.tab_panel.add(_widgets_panel, label)
        self.tab_panel.selectTab(self.tab_panel.getWidgetCount() - 1)

    def addWidget(self, wid):
        """ Add a widget at the bottom of the current tab
        @param wid: LiberviaWidget to add """
        panel = self.tab_panel.getCurrentPanel()
        panel.addWidget(wid)

    def _isRegisteredCB(self, registered):
        if not registered:
            self._register_box = RegisterBox(self.logged)
            self._register_box.centerBox()
            self._register_box.show()
        else:
            self._register.call('isConnected', self._isConnectedCB)

    def _isConnectedCB(self, connected):
        if not connected:
            self._register.call('connect',lambda x:self.logged())
        else:
            self.logged()

    def logged(self):
        if self._register_box:
            self._register_box.hide()
            del self._register_box # don't work if self._register_box is None

        #it's time to fill the page
        self.bridge.call('getContacts', self._getContactsCB)
        self.bridge_signals.call('getSignals', self._getSignalsCB)
        #We want to know our own jid
        self.bridge.call('getProfileJid', self._getProfileJidCB)

    def _getContactsCB(self, contacts_data):
        for contact in contacts_data:
            jid, attributes, groups = contact
            self._newContactCb(jid, attributes, groups)

    def _getSignalsCB(self, signal_data):
        self.bridge_signals.call('getSignals', self._getSignalsCB)
        print('Got signal ==> name: %s, params: %s' % (signal_data[0],signal_data[1]))
        name,args = signal_data
        if name == 'personalEvent':
            self._personalEventCb(*args)
        elif name == 'newMessage':
            self._newMessageCb(*args)
        elif name == 'presenceUpdate':
            self._presenceUpdateCb(*args)
        elif name == 'roomJoined':
            self._roomJoinedCb(*args)
        elif name == 'roomUserJoined':
            self._roomUserJoinedCb(*args)
        elif name == 'roomUserLeft':
            self._roomUserLeftCb(*args)
        elif name == 'tarotGameStarted':
            self._tarotGameStartedCb(*args)
        elif name == 'tarotGameNew' or \
             name == 'tarotGameChooseContrat' or \
             name == 'tarotGameShowCards' or \
             name == 'tarotGameInvalidCards' or \
             name == 'tarotGameCardsPlayed' or \
             name == 'tarotGameYourTurn' or \
             name == 'tarotGameScore':
            self._tarotGameGenericCb(name, args[0], args[1:])
        elif name == 'radiocolStarted':
            self._radioColStartedCb(*args)
        elif name == 'radiocolPreload':
            self._radioColGenericCb(name, args[0], args[1:])
        elif name == 'radiocolPlay':
            self._radioColGenericCb(name, args[0], args[1:])
        elif name == 'radiocolNoUpload':
            self._radioColGenericCb(name, args[0], args[1:])
        elif name == 'radiocolUploadOk':
            self._radioColGenericCb(name, args[0], args[1:])
        elif name == 'radiocolSongRejected':
            self._radioColGenericCb(name, args[0], args[1:])
        elif name == 'subscribe':
            self._subscribeCb(*args)
        elif name == 'contactDeleted':
            self._contactDeletedCb(*args)
        elif name == 'newContact':
            self._newContactCb(*args)
        elif name == 'entityDataUpdated':
            self._entityDataUpdatedCb(*args)
        elif name == 'chatStateReceived':
            self._chatStateReceivedCb(*args)

    def _ownBlogsFills(self, mblogs):
        #put our own microblogs in cache, then fill all panels with them
        for publisher in mblogs:
            for mblog in mblogs[publisher]:
                if not mblog.has_key('content'):
                    print ("WARNING: No content found in microblog [%s]", mblog)
                    continue
                if mblog.has_key('groups'):
                    _groups = set(mblog['groups'].split() if mblog['groups'] else [])
                else:
                    _groups=None
                mblog_entry = MicroblogItem(mblog)
                self.mblog_cache.append((_groups, mblog_entry))

        if len(self.mblog_cache) > MAX_MBLOG_CACHE:
            del self.mblog_cache[0:len(self.mblog_cache-MAX_MBLOG_CACHE)]
        for lib_wid in self.libervia_widgets:
            if isinstance(lib_wid, panels.MicroblogPanel):
                self.FillMicroblogPanel(lib_wid)
        self.initialised = True # initialisation phase is finished here
        for event_data in self.init_cache: # so we have to send all the cached events
            self._personalEventCb(*event_data)
        del self.init_cache

    def _getProfileJidCB(self, jid):
        self.whoami = JID(jid)
        #we can now ask our status
        self.bridge.call('getPresenceStatus', self._getPresenceStatusCb)
        #the rooms where we are
        self.bridge.call('getRoomsJoined', self._getRoomsJoinedCb)
        #and if there is any subscription request waiting for us
        self.bridge.call('getWaitingSub', self._getWaitingSubCb)
        #we fill the panels already here
        for lib_wid in self.libervia_widgets:
            if isinstance(lib_wid, panels.MicroblogPanel):
                if lib_wid.accept_all():
                    self.bridge.call('getMassiveLastMblogs', lib_wid.massiveInsert, 'ALL', [], 10)
                else:
                    self.bridge.call('getMassiveLastMblogs', lib_wid.massiveInsert, 'GROUP', lib_wid.accepted_groups, 10)
    
        #we ask for our own microblogs:
        self.bridge.call('getMassiveLastMblogs', self._ownBlogsFills, 'JID', [self.whoami.bare], 10)

    ## Signals callbacks ##

    def _personalEventCb(self, sender, event_type, data):
        if not self.initialised:
            self.init_cache.append((sender, event_type, data))
            return
        if event_type == "MICROBLOG":
            if not 'content' in data:
                print ("WARNING: No content found in microblog data")
                return
            if 'groups' in data:
                _groups = set(data['groups'].split() if data['groups'] else [])
            else:
                _groups=None
            mblog_entry = MicroblogItem(data)
        
            for lib_wid in self.libervia_widgets:
                if isinstance(lib_wid, panels.MicroblogPanel):
                    self.addBlogEntry(lib_wid, sender, _groups, mblog_entry) 

            if sender == self.whoami.bare:
                self.mblog_cache.append((_groups, mblog_entry))
                if len(self.mblog_cache) > MAX_MBLOG_CACHE:
                    del self.mblog_cache[0:len(self.mblog_cache-MAX_MBLOG_CACHE)]

    def addBlogEntry(self, mblog_panel, sender, _groups, mblog_entry):
        """Check if an entry can go in MicroblogPanel and add to it
        @param mblog_panel: MicroblogPanel instance
        @param sender: jid of the entry sender
        @param _groups: groups which can receive this entry
        @param mblog_entry: MicroblogItem instance"""
        if mblog_entry.type == "comment" or mblog_panel.isJidAccepted(sender) or (_groups == None and self.whoami and sender == self.whoami.bare) \
           or (_groups and _groups.intersection(mblog_panel.accepted_groups)):
            mblog_panel.addEntry(mblog_entry)

    def FillMicroblogPanel(self, mblog_panel):
        """Fill a microblog panel with entries in cache
        @param mblog_panel: MicroblogPanel instance
        """
        #XXX: only our own entries are cached
        for cache_entry in self.mblog_cache:
            _groups, mblog_entry = cache_entry 
            self.addBlogEntry(mblog_panel, self.whoami.bare, *cache_entry)

    def getEntityMBlog(self, entity):
        print "geting mblog for entity [%s]" % (entity,)
        for lib_wid in self.libervia_widgets:
            if isinstance(lib_wid, panels.MicroblogPanel):
                if lib_wid.isJidAccepted(entity):
                    self.bridge.call('getMassiveLastMblogs', lib_wid.massiveInsert, 'JID', [entity], 10)


    def _newMessageCb(self, from_jid, msg, msg_type, to_jid):
        _from = JID(from_jid)
        _to = JID(to_jid)
        showed = False
        for lib_wid in self.libervia_widgets:
            if isinstance(lib_wid,panels.ChatPanel) and (lib_wid.target.bare == _from.bare or lib_wid.target.bare == _to.bare):
                lib_wid.printMessage(_from, msg)
                showed = True
        if not showed:
            #The message has not been showed, we must indicate it
            other = _to if _from.bare == self.whoami.bare else _from
            self.contact_panel.setContactMessageWaiting(other.bare, True)
        
    def _presenceUpdateCb(self, entity, show, priority, statuses):
        _entity = JID(entity)
        #XXX: QnD way to get our status
        if self.whoami and self.whoami.bare == _entity.bare and statuses:
            self.status_panel.changeStatus(statuses.values()[0])
        if (not self.whoami or self.whoami.bare != _entity.bare):
            self.contact_panel.setConnected(_entity.bare, _entity.resource, show, priority, statuses)

    def _roomJoinedCb(self, room_jid, room_nicks, user_nick):
        _target = JID(room_jid)
        self.room_list.add(_target)
        chat_panel = panels.ChatPanel(self, _target, type_='group')
        chat_panel.setUserNick(user_nick)
        if _target.node.startswith('sat_tarot_'): #XXX: it's not really beautiful, but it works :)
            self.addTab(chat_panel, "Tarot")
        elif _target.node.startswith('sat_radiocol_'):
            self.addTab(chat_panel, "Radio collective")
        else:
            self.addTab(chat_panel, _target.node)
        chat_panel.setPresents(room_nicks)
        chat_panel.historyPrint()

    def _roomUserJoinedCb(self, room_jid_s, user_nick, user_data):
        for lib_wid in self.libervia_widgets:
            if isinstance(lib_wid,panels.ChatPanel) and lib_wid.type == 'group' and lib_wid.target.bare == room_jid_s:
                lib_wid.userJoined(user_nick, user_data)

    def _roomUserLeftCb(self, room_jid_s, user_nick, user_data):
        for lib_wid in self.libervia_widgets:
            if isinstance(lib_wid,panels.ChatPanel) and lib_wid.type == 'group' and lib_wid.target.bare == room_jid_s:
                lib_wid.userLeft(user_nick, user_data)
    
    def _tarotGameStartedCb(self, room_jid_s, referee, players):
        print ("Tarot Game Started \o/")
        for lib_wid in self.libervia_widgets:
            if isinstance(lib_wid,panels.ChatPanel) and lib_wid.type == 'group' and lib_wid.target.bare == room_jid_s:
                lib_wid.startGame("Tarot", referee, players)

    def _tarotGameGenericCb(self, event_name, room_jid_s, args):
        for lib_wid in self.libervia_widgets:
            if isinstance(lib_wid,panels.ChatPanel) and lib_wid.type == 'group' and lib_wid.target.bare == room_jid_s:
                getattr(lib_wid.getGame("Tarot"), event_name)(*args) 

    def _radioColStartedCb(self, room_jid_s, referee):
        for lib_wid in self.libervia_widgets:
            if isinstance(lib_wid,panels.ChatPanel) and lib_wid.type == 'group' and lib_wid.target.bare == room_jid_s:
                lib_wid.startGame("RadioCol", referee)

    def _radioColGenericCb(self, event_name, room_jid_s, args):
        for lib_wid in self.libervia_widgets:
            if isinstance(lib_wid,panels.ChatPanel) and lib_wid.type == 'group' and lib_wid.target.bare == room_jid_s:
                getattr(lib_wid.getGame("RadioCol"), event_name)(*args) 


    def _getPresenceStatusCb(self, presence_data):
        for entity in presence_data:
            for resource in presence_data[entity]:
                args = presence_data[entity][resource]
                self._presenceUpdateCb("%s/%s" % (entity, resource), *args)

    def _getRoomsJoinedCb(self, room_data):
        for room in room_data:
            self._roomJoinedCb(*room)

    def _getWaitingSubCb(self, waiting_sub):
        for sub in waiting_sub:
            self._subscribeCb(waiting_sub[sub], sub)

    def _subscribeCb(self, sub_type, entity):
        if sub_type == 'subscribed':
            dialog.InfoDialog('Subscription confirmation', 'The contact <b>%s</b> has added you to his/her contact list' % html_sanitize(entity)).show()
            self.getEntityMBlog(entity)

        elif sub_type == 'unsubscribed':
            dialog.InfoDialog('Subscription refusal', 'The contact <b>%s</b> has refused to add you in his/her contact list' % html_sanitize(entity)).show()
            #TODO: remove microblogs from panels

        elif sub_type == 'subscribe':
            #The user want to subscribe to our presence
            _dialog = None
            msg = HTML('The contact <b>%s</b> want to add you in his/her contact list, do you accept ?' % html_sanitize(entity))
            
            def ok_cb(ignore):
                self.bridge.call('subscription', None, "subscribed", entity, '', _dialog.getSelectedGroups())
            def cancel_cb(ignore):
                self.bridge.call('subscription', None, "unsubscribed", entity, '', '')
    
            _dialog = dialog.GroupSelector([msg], self.contact_panel.getGroups(), [], ok_cb, cancel_cb)
            _dialog.setHTML('<b>Add contact request</b>')
            _dialog.show()

    def _contactDeletedCb(self, entity):
        self.contact_panel.removeContact(entity)

    def _newContactCb(self, contact, attributes, groups):
        self.contact_panel.updateContact(contact, attributes, groups)

    def _entityDataUpdatedCb(self, entity_jid_s, key, value):
        if key == "avatar":
            avatar = '/avatars/%s' % value

            self.avatars_cache[entity_jid_s] = avatar

            for lib_wid in self.libervia_widgets:
                if isinstance(lib_wid, panels.MicroblogPanel):
                    if lib_wid.isJidAccepted(entity_jid_s) or (self.whoami and entity_jid_s == self.whoami.bare):
                        lib_wid.updateValue('avatar', entity_jid_s, avatar)

    def _chatStateReceivedCb(self, from_jid_s, state, profile):
        """Callback when a new chat state is received.
        @param from_jid_s: JID from the contact who sent his state
        @param state: new state
        @profile: current profile
        """
        _from = JID(from_jid_s).bare if from_jid_s != "@ALL@" else from_jid_s
        for lib_wid in self.libervia_widgets:
            if isinstance(lib_wid, panels.ChatPanel):
                win_from = lib_wid.target.bare
                good_win = win_from == _from or _from == "@ALL@"
                if (good_win and lib_wid.type == 'one2one'):
                    if state:
                        lib_wid.setTitle(win_from + " (" + state + ")")
                    else:
                        lib_wid.setTitle(win_from)
                elif (lib_wid.type == 'group'):
                    # TODO: chat state notification for groupchat
                    pass

if __name__ == '__main__':
    pyjd.setup("http://localhost:8080/libervia.html")
    app = SatWebFrontend()
    app.onModuleLoad()
    pyjd.run()