# HG changeset patch # User Goffi # Date 1422060339 -3600 # Node ID a5019e62c3e9831b2213afb64516c39739437d53 # Parent bade589dbd5a6ae1f9fc1498bcf0797a8832fe29 browser side: big refactoring to base Libervia on QuickFrontend, first draft: /!\ not finished, partially working and highly instable - add collections module with an OrderedDict like class - SatWebFrontend inherit from QuickApp - general sat_frontends tools.jid module is used - bridge/json methods have moved to json module - UniBox is partially removed (should be totally removed before merge to trunk) - Signals are now register with the generic registerSignal method (which is called mainly in QuickFrontend) - the generic getOrCreateWidget method from QuickWidgetsManager is used instead of Libervia's specific methods - all Widget are now based more or less directly on QuickWidget - with the new QuickWidgetsManager.getWidgets method, it's no more necessary to check all widgets which are instance of a particular class - ChatPanel and related moved to chat module - MicroblogPanel and related moved to blog module - global and overcomplicated send method has been disabled: each class should manage its own sending - for consistency with other frontends, former ContactPanel has been renamed to ContactList and vice versa - for the same reason, ChatPanel has been renamed to Chat - for compatibility with QuickFrontend, a fake profile is used in several places, it is set to C.PROF_KEY_NONE (real profile is managed server side for obvious security reasons) - changed default url for web panel to SàT website, and contact address to generic SàT contact address - ContactList is based on QuickContactList, UI changes are done in update method - bride call (now json module) have been greatly improved, in particular call can be done in the same way as for other frontends (bridge.method_name(arg1, arg2, ..., callback=cb, errback=eb). Blocking method must be called like async methods due to javascript architecture - in bridge calls, a callback can now exists without errback - hard reload on BridgeSignals remote error has been disabled, a better option should be implemented - use of constants where that make sens, some style improvments - avatars are temporarily disabled - lot of code disabled, will be fixed or removed before merge - various other changes, check diff for more details server side: manage remote exception on getEntityData, removed getProfileJid call, added getWaitingConf, added getRoomsSubjects diff -r bade589dbd5a -r a5019e62c3e9 .hgignore --- a/.hgignore Thu Oct 23 16:56:36 2014 +0200 +++ b/.hgignore Sat Jan 24 01:45:39 2015 +0100 @@ -7,7 +7,6 @@ tags twistd.log twistd.pid -output sat.egg-info *.un~ dist @@ -16,3 +15,4 @@ build/ Session.vim ctags_links/ +html/ diff -r bade589dbd5a -r a5019e62c3e9 src/browser/collections.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/browser/collections.py Sat Jan 24 01:45:39 2015 +0100 @@ -0,0 +1,142 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Libervia: a Salut à Toi frontend +# Copyright (C) 2014 Jérôme Poisson + +# 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 . + +class OrderedDict(object): + """Naive implementation of OrderedDict which is compatible with pyjamas""" + + def __init__(self, *args, **kwargs): + self.__internal_dict = {} + self.__keys = [] # this list keep the keys in order + if args: + if len(args)>1: + raise TypeError("OrderedDict expected at most 1 arguments, got {}".format(len(args))) + if isinstance(args[0], (dict, OrderedDict)): + for key, value in args[0].iteritems(): + self[key] = value + for key, value in args[0]: + self[key] = value + + def __setitem__(self, key, value): + self.__keys.append(key) + self.__internal_dict[key] = value + + def __getitem__(self, key): + return self.__internal_dict[key] + + def __delitem__(self, key): + del self.__internal_dict[key] + self.__keys.remove(key) + + def clear(self): + self.__internal_dict.clear() + del self.__keys[:] + + def copy(self): + return OrderedDict(self) + + @classmethod + def fromkeys(cls, seq, value=None): + ret = OrderedDict() + for key in seq: + ret[key] = value + return ret + + def get(self, key, default=None): + try: + return self.__internal_dict[key] + except KeyError: + return default + + def has_key(self, key): + return key in self.__keys + + def keys(self): + return self.__keys[:] + + def iterkeys(self): + for key in self.__keys: + yield key + + def items(self): + ret = [] + for key in self.__keys: + ret.append((key, self.__internal_dict[key])) + return ret + + def iteritems(self): + for key in self.__keys: + yield (key, self.__internal_dict[key]) + + def values(self): + ret = [] + for key in self.__keys: + ret.append(self.__internal_dict[key]) + return ret + + def itervalues(self): + for key in self.__keys: + yield (self.__internal_dict[key]) + + def popitem(self, last=True): + try: + key = self.__keys.pop(-1 if last else 0) + except IndexError: + raise KeyError('dictionnary is empty') + value = self.__internal_dict.pop(key) + return((key, value)) + + def setdefault(self, key, default=None): + try: + return self.__internal_dict[key] + except KeyError: + self.__internal_dict[key] = default + return default + + def update(self, *args, **kwargs): + if len(args) > 1: + raise TypeError('udpate expected at most 1 argument, got {}'.format(len(args))) + if args: + if hasattr(args[0], 'keys'): + for k in args[0]: + self[k] = args[0][k] + else: + for (k, v) in args[0]: + self[k] = v + for k, v in kwargs.items(): + self[k] = v + + def pop(self, *args): + if not args: + raise TypeError('pop expected at least 1 argument, got 0') + try: + self.__internal_dict.pop(args[0]) + except KeyError: + if len(args) == 2: + return args[1] + raise KeyError(args[0]) + self.__keys.remove(args[0]) + + def viewitems(): + raise NotImplementedError + + def viewkeys(): + raise NotImplementedError + + def viewvalues(): + raise NotImplementedError diff -r bade589dbd5a -r a5019e62c3e9 src/browser/libervia_main.py --- a/src/browser/libervia_main.py Thu Oct 23 16:56:36 2014 +0200 +++ b/src/browser/libervia_main.py Sat Jan 24 01:45:39 2015 +0100 @@ -17,7 +17,6 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -import pyjd # this is dummy in pyjs ### logging configuration ### from sat_browser import logging @@ -26,8 +25,11 @@ log = getLogger(__name__) ### +from sat_frontends.quick_frontend.quick_app import QuickApp + from sat_frontends.tools.misc import InputHistory from sat_frontends.tools import strings +from sat_frontends.tools import jid from sat.core.i18n import _ from pyjamas.ui.RootPanel import RootPanel @@ -35,26 +37,30 @@ from pyjamas.ui.KeyboardListener import KEY_ESCAPE from pyjamas.Timer import Timer from pyjamas import Window, DOM -from pyjamas.JSONService import JSONProxy +from sat_browser import json from sat_browser import register -from sat_browser import contact +from sat_browser.contact_list import ContactList from sat_browser import base_widget from sat_browser import panels +from sat_browser import chat +from sat_browser import blog from sat_browser import dialog -from sat_browser import jid from sat_browser import xmlui from sat_browser import html_tools from sat_browser import notification from sat_browser.constants import Const as C + try: # FIXME: import plugin dynamically from sat_browser import plugin_sec_otr except ImportError: pass +unicode = lambda s: str(s) + MAX_MBLOG_CACHE = 500 # Max microblog entries kept in memories # Set to true to not create a new LiberviaWidget when a similar one @@ -65,138 +71,25 @@ REUSE_EXISTING_LIBERVIA_WIDGETS = True -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: - 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): - 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: - log.error("Internal server error") - """for o in code, error, request_info: - dump(o)""" - else: - if isinstance(errobj['message'], dict): - log.error("Error %s: %s" % (errobj['message']['faultCode'], errobj['message']['faultString'])) - else: - log.error("%s" % errobj['message']) - - -class RegisterCall(LiberviaJsonProxy): - def __init__(self): - LiberviaJsonProxy.__init__(self, "/register_api", - ["isRegistered", "isConnected", "asyncConnect", "registerParams", "getMenus"]) - - -class BridgeCall(LiberviaJsonProxy): - def __init__(self): - LiberviaJsonProxy.__init__(self, "/json_api", - ["getContacts", "addContact", "sendMessage", "sendMblog", "sendMblogComment", - "getLastMblogs", "getMassiveLastMblogs", "getMblogComments", "getProfileJid", - "getHistory", "getPresenceStatuses", "joinMUC", "mucLeave", "getRoomsJoined", - "inviteMUC", "launchTarotGame", "getTarotCardsPaths", "tarotGameReady", - "tarotGamePlayCards", "launchRadioCollective", "getMblogs", "getMblogsWithComments", - "getWaitingSub", "subscription", "delContact", "updateContact", "getCard", - "getEntityData", "getParamsUI", "asyncGetParamA", "setParam", "launchAction", - "disconnect", "chatStateComposing", "getNewAccountDomain", "confirmationAnswer", - "syntaxConvert", "getAccountDialogUI", "getLastResource" - ]) - - -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(timer): - self.host.bridge_signals.call('getSignals', self.host._getSignalsCB) - Timer(notify=_timerCb).schedule(self.retry_delay) - self.retry_delay *= 2 - - -class SatWebFrontend(InputHistory): +class SatWebFrontend(InputHistory, QuickApp): def onModuleLoad(self): log.info("============ 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.bridge_signals = json.BridgeSignals(self) + QuickApp.__init__(self, json.BridgeCall) + self.uni_box = None # FIXME: to be removed self.status_panel = HTML('
') - self.contact_panel = contact.ContactPanel(self) + # self.contact_panel = contact.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 = [] # list 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._register_box = None RootPanel().add(self.panel) + self.notification = notification.Notification() DOM.addEventPreview(self) self.importPlugins() - self._register = RegisterCall() + self._register = json.RegisterCall() self._register.call('getMenus', self.gotMenus) self._register.call('registerParams', None) self._register.call('isRegistered', self._isRegisteredCB) @@ -204,6 +97,36 @@ self.init_cache = [] # used to cache events until initialisation is done self.cached_params = {} + #FIXME: to be removed (managed with cache and in quick_frontend + self.avatars_cache = {} # keep track of jid's avatar hash (key=jid, value=file) + #FIXME: microblog cache should be managed directly in blog module + self.mblog_cache = [] # used to keep our own blog entries in memory, to show them in new mblog panel + + + + # panels.ChatPanel.registerClass() + # panels.MicroblogPanel.registerClass() + # self._selected_listeners = set() + # # self.avatars_cache = {} # keep track of jid's avatar hash (key=jid, value=file) + + @property + def whoami(self): + # XXX: works because Libervia is mono-profile + # if one day Libervia manage several profiles at once, this must be deleted + return self.profiles[C.PROF_KEY_NONE].whoami + + @property + def contact_list(self): + return self.contact_lists[C.PROF_KEY_NONE] + + def registerSignal(self, functionName, handler=None, iface="core", with_profile=True): + if handler is None: + callback = getattr(self, "{}{}".format(functionName, "Handler")) + else: + callback = handler + + self.bridge_signals.register(functionName, callback, with_profile=with_profile) + def importPlugins(self): self.plugins = {} inhibited_menus = [] @@ -219,8 +142,8 @@ self.plugins['dummy_plugin'] = DummyPlugin() - def addSelectedListener(self, callback): - self._selected_listeners.add(callback) + # def addSelectedListener(self, callback): + # self._selected_listeners.add(callback) def getSelected(self): wid = self.tab_panel.getCurrentPanel() @@ -244,12 +167,14 @@ selected.removeStyleName('selected_widget') widgets_panel.selected = widget + self.selected_widget = widget if widget: widgets_panel.selected.addStyleName('selected_widget') - for callback in self._selected_listeners: - callback(widget) + # FIXME: + # for callback in self._selected_listeners: + # callback(widget) def resize(self): """Resize elements""" @@ -260,8 +185,9 @@ def onTabSelected(self, sender, tab_index): selected = self.getSelected() - for callback in self._selected_listeners: - callback(selected) + # FIXME: + # for callback in self._selected_listeners: + # callback(selected) def onEventPreview(self, event): if event.type in ["keydown", "keypress", "keyup"] and event.keyCode == KEY_ESCAPE: @@ -269,26 +195,28 @@ event.preventDefault() return True - def getAvatar(self, jid_str): - """Return avatar of a jid if in cache, else ask for it. + # FIXME: must not call _entityDataUpdatedCb by itself + # should not get VCard, backend plugin must be fixed too + # def getAvatar(self, jid_str): + # """Return avatar of a jid if in cache, else ask for it. - @param jid_str (str): JID of the contact - @return: the URL to the avatar (str) - """ - def dataReceived(result): - if 'avatar' in result: - self._entityDataUpdatedCb(jid_str, 'avatar', result['avatar']) - else: - self.bridge.call("getCard", None, jid_str) + # @param jid_str (str): JID of the contact + # @return: the URL to the avatar (str) + # """ + # 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) + # 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] = C.DEFAULT_AVATAR - return self.avatars_cache[jid_str] + # if jid_str not in self.avatars_cache: + # self.bridge.call('getEntityData', (dataReceived, avatarError), jid_str, ['avatar']) + # self.avatars_cache[jid_str] = C.DEFAULT_AVATAR + # return self.avatars_cache[jid_str] def registerWidget(self, wid): log.debug("Registering %s" % wid.getDebugName()) @@ -303,10 +231,6 @@ def refresh(self): """Refresh the general display.""" self.panel.refresh() - if self.getCachedParam(C.COMPOSITION_KEY, C.ENABLE_UNIBOX_PARAM) == 'true': - self.uni_box = self.panel.unibox_panel.unibox - else: - self.uni_box = None for lib_wid in self.libervia_widgets: lib_wid.refresh() self.resize() @@ -326,8 +250,10 @@ def addWidget(self, wid, tab_index=None): """ Add a widget at the bottom of the current or specified tab + @param wid: LiberviaWidget to add - @param tab_index: index of the tab to add the widget to""" + @param tab_index: index of the tab to add the widget to + """ if tab_index is None or tab_index < 0 or tab_index >= self.tab_panel.getWidgetCount(): panel = self.tab_panel.getCurrentPanel() else: @@ -351,7 +277,7 @@ menus_data.append((id_, path, path_i18n)) self.menus = {} - inhibited = set() + inhibited = set() # FIXME extras = [] for plugin in self.plugins.values(): if hasattr(plugin, "inhibitMenus"): @@ -390,12 +316,14 @@ self.status_panel = panels.PresenceStatusPanel(self) self.panel.header.add(self.status_panel) + self.bridge_signals.call('getSignals', self.bridge_signals.signalHandler) + #it's time to fill the page - self.bridge.call('getContacts', self._getContactsCB) - self.bridge.call('getParamsUI', self._getParamsUICB) - self.bridge_signals.call('getSignals', self._getSignalsCB) - #We want to know our own jid - self.bridge.call('getProfileJid', self._getProfileJidCB) + # self.bridge.call('getContacts', self._getContactsCB) + # self.bridge.call('getParamsUI', self._getParamsUICB) + # self.bridge_signals.call('getSignals', self._getSignalsCB) + # #We want to know our own jid + # self.bridge.call('getProfileJid', self._getProfileJidCB) def domain_cb(value): self._defaultDomain = value @@ -404,18 +332,50 @@ def domain_eb(value): self._defaultDomain = "libervia.org" - self.bridge.call("getNewAccountDomain", (domain_cb, domain_eb)) - self.discuss_panel.addWidget(panels.MicroblogPanel(self, [])) + self.bridge.getNewAccountDomain(callback=domain_cb, errback=domain_eb) + self.plug_profiles([C.PROF_KEY_NONE]) # XXX: None was used intitially, but pyjamas bug when using variable arguments and None is the only arg. + microblog_widget = self.widgets.getOrCreateWidget(blog.MicroblogPanel, (), profile=C.PROF_KEY_NONE) + self.setSelected(microblog_widget) + # self.discuss_panel.addWidget(panels.MicroblogPanel(self, [])) + + # # get cached params and refresh the display + # def param_cb(cat, name, count): + # count[0] += 1 + # refresh = count[0] == len(C.CACHED_PARAMS) + # return lambda value: self._paramUpdate(name, value, cat, refresh) + + # count = [0] # used to do something similar to DeferredList + # for cat, name in C.CACHED_PARAMS: + # self.bridge.call('asyncGetParamA', param_cb(cat, name, count), name, cat) - # get cached params and refresh the display - def param_cb(cat, name, count): - count[0] += 1 - refresh = count[0] == len(C.CACHED_PARAMS) - return lambda value: self._paramUpdate(name, value, cat, refresh) + def profilePlugged(self, profile): + #we fill the panels already here + for widget in self.widgets.getWidgets(blog.MicroblogPanel): + if widget.accept_all(): + self.bridge.getMassiveLastMblogs('ALL', [], 10, profile=C.PROF_KEY_NONE, callback=widget.massiveInsert) + else: + self.bridge.getMassiveLastMblogs('GROUP', widget.accepted_groups, 10, profile=C.PROF_KEY_NONE, callback=widget.massiveInsert) + + #we ask for our own microblogs: + self.bridge.getMassiveLastMblogs('JID', [unicode(self.whoami.bare)], 10, profile=C.PROF_KEY_NONE, callback=self._ownBlogsFills) - count = [0] # used to do something similar to DeferredList - for cat, name in C.CACHED_PARAMS: - self.bridge.call('asyncGetParamA', param_cb(cat, name, count), name, cat) + # initialize plugins which waited for the connection to be done + for plugin in self.plugins.values(): + if hasattr(plugin, 'profileConnected'): + plugin.profileConnected() + + def addContactList(self, dummy): + contact_list = ContactList(self) + self.contact_lists[C.PROF_KEY_NONE] = contact_list + self.panel.addContactList(contact_list) + return contact_list + + def newWidget(self, widget): + log.debug("newWidget: {}".format(widget)) + self.addWidget(widget) + + def setStatusOnline(self, online=True, show="", statuses={}, profile=C.PROF_KEY_NONE): + log.warning("setStatusOnline is not implemented, as session are for unique profile which is always online for now") def _tryAutoConnect(self, skip_validation=False): """This method retrieve the eventual URL parameters to auto-connect the user. @@ -471,71 +431,6 @@ jid, attributes, groups = contact_ self._newContactCb(jid, attributes, groups) - def _getSignalsCB(self, signal_data): - self.bridge_signals.call('getSignals', self._getSignalsCB) - if len(signal_data) == 1: - signal_data.append([]) - log.debug("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 == 'paramUpdate': - self._paramUpdate(*args) - elif name == 'roomJoined': - self._roomJoinedCb(*args) - elif name == 'roomLeft': - self._roomLeftCb(*args) - elif name == 'roomUserJoined': - self._roomUserJoinedCb(*args) - elif name == 'roomUserLeft': - self._roomUserLeftCb(*args) - elif name == 'roomUserChangedNick': - self._roomUserChangedNickCb(*args) - elif name == 'askConfirmation': - self._askConfirmation(*args) - elif name == 'newAlert': - self._newAlert(*args) - elif name == 'tarotGamePlayers': - self._tarotGameStartedCb(True, *args) - elif name == 'tarotGameStarted': - self._tarotGameStartedCb(False, *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 == 'radiocolPlayers': - self._radioColStartedCb(True, *args) - elif name == 'radiocolStarted': - self._radioColStartedCb(False, *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 _getParamsUICB(self, xml_ui): """Hide the parameters item if there's nothing to display""" if not xml_ui: @@ -552,42 +447,45 @@ _groups = set(mblog['groups'].split() if mblog['groups'] else []) else: _groups = None - mblog_entry = panels.MicroblogItem(mblog) + mblog_entry = blog.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) + for widget in self.widgets.getWidgets(blog.MicroblogPanel): + self.FillMicroblogPanel(widget) + + # FIXME 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_s): - self.whoami = jid.JID(jid_s) - #we can now ask our status - self.bridge.call('getPresenceStatuses', self._getPresenceStatusesCb) - #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) + # FIXME + raise Exception("should not be here !") + # self.whoami = jid.JID(jid_s) + # #we can now ask our status + # self.bridge.call('getPresenceStatuses', self._getPresenceStatusesCb) + # #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) + # #we ask for our own microblogs: + # self.bridge.call('getMassiveLastMblogs', self._ownBlogsFills, 'JID', [self.whoami.bare], 10) - # initialize plugins which waited for the connection to be done - for plugin in self.plugins.values(): - if hasattr(plugin, 'profileConnected'): - plugin.profileConnected() + # # initialize plugins which waited for the connection to be done + # for plugin in self.plugins.values(): + # if hasattr(plugin, 'profileConnected'): + # plugin.profileConnected() ## Signals callbacks ## @@ -604,11 +502,10 @@ _groups = set(data['groups'].split() if data['groups'] else []) else: _groups = None - mblog_entry = panels.MicroblogItem(data) + mblog_entry = blog.MicroblogItem(data) - for lib_wid in self.libervia_widgets: - if isinstance(lib_wid, panels.MicroblogPanel): - self.addBlogEntry(lib_wid, sender, _groups, mblog_entry) + for widget in self.widgets.getWidgets(blog.MicroblogPanel): + self.addBlogEntry(widget, sender, _groups, mblog_entry) if sender == self.whoami.bare: found = False @@ -625,9 +522,8 @@ if len(self.mblog_cache) > MAX_MBLOG_CACHE: del self.mblog_cache[0:len(self.mblog_cache - MAX_MBLOG_CACHE)] elif event_type == 'MICROBLOG_DELETE': - for lib_wid in self.libervia_widgets: - if isinstance(lib_wid, panels.MicroblogPanel): - lib_wid.removeEntry(data['type'], data['id']) + for widget in self.widgets.getWidgets(blog.MicroblogPanel): + widget.removeEntry(data['type'], data['id']) log.debug("%s %s %s" % (self.whoami.bare, sender, data['type'])) if sender == self.whoami.bare and data['type'] == 'main_item': @@ -663,209 +559,209 @@ if lib_wid.isJidAccepted(entity): self.bridge.call('getMassiveLastMblogs', lib_wid.massiveInsert, 'JID', [entity], 10) - def getLiberviaWidget(self, class_, entity, ignoreOtherTabs=True): - """Get the corresponding panel if it exists. - @param class_ (class): class of the panel (ChatPanel, MicroblogPanel...) - @param entity (dict): dictionnary to define the entity. - @param ignoreOtherTabs (bool): if True, the widgets that are not - contained by the currently selected tab will be ignored - @return: the existing widget that has been found or None.""" - selected_tab = self.tab_panel.getCurrentPanel() - for lib_wid in self.libervia_widgets: - parent = lib_wid.getWidgetsPanel(expect=False) - if parent is None or (ignoreOtherTabs and parent != selected_tab): - # do not return a widget that is not in the currently selected tab - continue - if isinstance(lib_wid, class_): - try: - if lib_wid.matchEntity(*(entity.values())): # XXX: passing **entity bugs! - log.debug("existing widget found: %s" % lib_wid.getDebugName()) - return lib_wid - except AttributeError as e: - e.stack_list() - return None - return None + # def getLiberviaWidget(self, class_, entity, ignoreOtherTabs=True): + # """Get the corresponding panel if it exists. + # @param class_ (class): class of the panel (ChatPanel, MicroblogPanel...) + # @param entity (dict): dictionnary to define the entity. + # @param ignoreOtherTabs (bool): if True, the widgets that are not + # contained by the currently selected tab will be ignored + # @return: the existing widget that has been found or None.""" + # selected_tab = self.tab_panel.getCurrentPanel() + # for lib_wid in self.libervia_widgets: + # parent = lib_wid.getWidgetsPanel(expect=False) + # if parent is None or (ignoreOtherTabs and parent != selected_tab): + # # do not return a widget that is not in the currently selected tab + # continue + # if isinstance(lib_wid, class_): + # try: + # if lib_wid.matchEntity(*(entity.values())): # XXX: passing **entity bugs! + # log.debug("existing widget found: %s" % lib_wid.getDebugName()) + # return lib_wid + # except AttributeError as e: + # e.stack_list() + # return None + # return None - def getOrCreateLiberviaWidget(self, class_, entity, select=True, new_tab=None): - """Get the matching LiberviaWidget if it exists, or create a new one. - @param class_ (class): class of the panel (ChatPanel, MicroblogPanel...) - @param entity (dict): dictionnary to define the entity. - @param select (bool): if True, select the widget that has been found or created - @param new_tab (str): if not None, a widget which is created is created in - a new tab. In that case new_tab is a unicode to label that new tab. - If new_tab is not None and a widget is found, no tab is created. - @return: the newly created wigdet if REUSE_EXISTING_LIBERVIA_WIDGETS - is set to False or if the widget has not been found, the existing - widget that has been found otherwise.""" - lib_wid = None - tab = None - if REUSE_EXISTING_LIBERVIA_WIDGETS: - lib_wid = self.getLiberviaWidget(class_, entity, new_tab is None) - if lib_wid is None: # create a new widget - lib_wid = class_.createPanel(self, *(entity.values())) # XXX: passing **entity bugs! - if new_tab is None: - self.addWidget(lib_wid) - else: - tab = self.addTab(new_tab, lib_wid, False) - else: # reuse existing widget - tab = lib_wid.getWidgetsPanel(expect=False) - if new_tab is None: - if tab is not None: - tab.removeWidget(lib_wid) - self.addWidget(lib_wid) - if select: - if new_tab is not None: - self.tab_panel.selectTab(tab) - # must be done after the widget is added, - # for example to scroll to the bottom - self.setSelected(lib_wid) - lib_wid.refresh() - return lib_wid + # def getOrCreateLiberviaWidget(self, class_, entity, select=True, new_tab=None): + # """Get the matching LiberviaWidget if it exists, or create a new one. + # @param class_ (class): class of the panel (ChatPanel, MicroblogPanel...) + # @param entity (dict): dictionnary to define the entity. + # @param select (bool): if True, select the widget that has been found or created + # @param new_tab (str): if not None, a widget which is created is created in + # a new tab. In that case new_tab is a unicode to label that new tab. + # If new_tab is not None and a widget is found, no tab is created. + # @return: the newly created wigdet if REUSE_EXISTING_LIBERVIA_WIDGETS + # is set to False or if the widget has not been found, the existing + # widget that has been found otherwise.""" + # lib_wid = None + # tab = None + # if REUSE_EXISTING_LIBERVIA_WIDGETS: + # lib_wid = self.getLiberviaWidget(class_, entity, new_tab is None) + # if lib_wid is None: # create a new widget + # lib_wid = class_.createPanel(self, *(entity.values())) # XXX: passing **entity bugs! + # if new_tab is None: + # self.addWidget(lib_wid) + # else: + # tab = self.addTab(new_tab, lib_wid, False) + # else: # reuse existing widget + # tab = lib_wid.getWidgetsPanel(expect=False) + # if new_tab is None: + # if tab is not None: + # tab.removeWidget(lib_wid) + # self.addWidget(lib_wid) + # if select: + # if new_tab is not None: + # self.tab_panel.selectTab(tab) + # # must be done after the widget is added, + # # for example to scroll to the bottom + # self.setSelected(lib_wid) + # lib_wid.refresh() + # return lib_wid - def getRoomWidget(self, target): - """Get the MUC widget for the given target. + # def getRoomWidget(self, target): + # """Get the MUC widget for the given target. - @param target (jid.JID): BARE jid of the MUC - @return: panels.ChatPanel instance or None - """ - entity = {'item': target, 'type_': 'group'} - if target.full() in self.room_list or target in self.room_list: # as JID is a string-based class, we don't know what will please Pyjamas... - return self.getLiberviaWidget(panels.ChatPanel, entity, ignoreOtherTabs=False) - return None + # @param target (jid.JID): BARE jid of the MUC + # @return: panels.ChatPanel instance or None + # """ + # entity = {'item': target, 'type_': 'group'} + # if target.full() in self.room_list or target in self.room_list: # as JID is a string-based class, we don't know what will please Pyjamas... + # return self.getLiberviaWidget(panels.ChatPanel, entity, ignoreOtherTabs=False) + # return None - def getOrCreateRoomWidget(self, target): - """Get the MUC widget for the given target, create it if necessary. + # def getOrCreateRoomWidget(self, target): + # """Get the MUC widget for the given target, create it if necessary. - @param target (jid.JID): BARE jid of the MUC - @return: panels.ChatPanel instance - """ - lib_wid = self.getRoomWidget(target) - if lib_wid: - return lib_wid + # @param target (jid.JID): BARE jid of the MUC + # @return: panels.ChatPanel instance + # """ + # lib_wid = self.getRoomWidget(target) + # if lib_wid: + # return lib_wid - # XXX: target.node.startwith(...) raises an error "startswith is not a function" - # This happens when node a is property defined in the JID class - # FIXME: pyjamas doesn't handle the properties well - node = target.node + # # XXX: target.node.startwith(...) raises an error "startswith is not a function" + # # This happens when node a is property defined in the JID class + # # FIXME: pyjamas doesn't handle the properties well + # node = target.node - # XXX: it's not really beautiful, but it works :) - if node.startswith('sat_tarot_'): - tab_name = "Tarot" - elif node.startswith('sat_radiocol_'): - tab_name = "Radio collective" - else: - tab_name = target.node + # # XXX: it's not really beautiful, but it works :) + # if node.startswith('sat_tarot_'): + # tab_name = "Tarot" + # elif node.startswith('sat_radiocol_'): + # tab_name = "Radio collective" + # else: + # tab_name = target.node - self.room_list.append(target) - entity = {'item': target, 'type_': 'group'} - return self.getOrCreateLiberviaWidget(panels.ChatPanel, entity, new_tab=tab_name) + # self.room_list.append(target) + # entity = {'item': target, 'type_': 'group'} + # return self.getOrCreateLiberviaWidget(panels.ChatPanel, entity, new_tab=tab_name) - def _newMessageCb(self, from_jid_s, msg, msg_type, to_jid_s, extra): - from_jid = jid.JID(from_jid_s) - to_jid = jid.JID(to_jid_s) - for plugin in self.plugins.values(): - if hasattr(plugin, 'messageReceivedTrigger'): - if not plugin.messageReceivedTrigger(from_jid, msg, msg_type, to_jid, extra): - return # plugin returned False to interrupt the process - self.newMessageCb(from_jid, msg, msg_type, to_jid, extra) + # def _newMessageCb(self, from_jid_s, msg, msg_type, to_jid_s, extra): + # from_jid = jid.JID(from_jid_s) + # to_jid = jid.JID(to_jid_s) + # for plugin in self.plugins.values(): + # if hasattr(plugin, 'messageReceivedTrigger'): + # if not plugin.messageReceivedTrigger(from_jid, msg, msg_type, to_jid, extra): + # return # plugin returned False to interrupt the process + # self.newMessageCb(from_jid, msg, msg_type, to_jid, extra) - def newMessageCb(self, from_jid, msg, msg_type, to_jid, extra): - other = to_jid if from_jid.bare == self.whoami.bare else from_jid - lib_wid = self.getLiberviaWidget(panels.ChatPanel, {'item': other}, ignoreOtherTabs=False) - self.displayNotification(from_jid, msg) - if msg_type == 'headline' and from_jid.full() == self._defaultDomain: - try: - assert extra['subject'] # subject is defined and not empty - title = extra['subject'] - except (KeyError, AssertionError): - title = _('Announcement from %s') % from_jid.full() - msg = strings.addURLToText(html_tools.XHTML2Text(msg)) - dialog.InfoDialog(title, msg).show() - return - if lib_wid is not None: - if msg_type == C.MESS_TYPE_INFO: - lib_wid.printInfo(msg) - else: - lib_wid.printMessage(from_jid, msg, extra) - if 'header_info' in extra: - lib_wid.setHeaderInfo(extra['header_info']) - else: - # FIXME: "info" message and header info will be lost here - if not self.contact_panel.isContactInRoster(other.bare): - self.contact_panel.updateContact(other.bare, {}, [C.GROUP_NOT_IN_ROSTER]) - # The message has not been shown, we must indicate it - self.contact_panel.setContactMessageWaiting(other.bare, True) + # def newMessageCb(self, from_jid, msg, msg_type, to_jid, extra): + # other = to_jid if from_jid.bare == self.whoami.bare else from_jid + # lib_wid = self.getLiberviaWidget(panels.ChatPanel, {'item': other}, ignoreOtherTabs=False) + # self.displayNotification(from_jid, msg) + # if msg_type == 'headline' and from_jid.full() == self._defaultDomain: + # try: + # assert extra['subject'] # subject is defined and not empty + # title = extra['subject'] + # except (KeyError, AssertionError): + # title = _('Announcement from %s') % from_jid.full() + # msg = strings.addURLToText(html_tools.XHTML2Text(msg)) + # dialog.InfoDialog(title, msg).show() + # return + # if lib_wid is not None: + # if msg_type == C.MESS_TYPE_INFO: + # lib_wid.printInfo(msg) + # else: + # lib_wid.printMessage(from_jid, msg, extra) + # if 'header_info' in extra: + # lib_wid.setHeaderInfo(extra['header_info']) + # else: + # # FIXME: "info" message and header info will be lost here + # if not self.contact_panel.isContactInRoster(other.bare): + # self.contact_panel.updateContact(other.bare, {}, [C.GROUP_NOT_IN_ROSTER]) + # # The message has not been shown, we must indicate it + # self.contact_panel.setContactMessageWaiting(other.bare, True) - def _presenceUpdateCb(self, entity, show, priority, statuses): - entity_jid = jid.JID(entity) - if self.whoami and self.whoami == entity_jid: # XXX: QnD way to get our presence/status - assert(isinstance(self.status_panel, panels.PresenceStatusPanel)) - self.status_panel.setPresence(show) # pylint: disable=E1103 - if statuses: - self.status_panel.setStatus(statuses.values()[0]) # pylint: disable=E1103 - else: - bare_jid = entity_jid.bareJID() - if bare_jid.full() in self.room_list or bare_jid in self.room_list: # as JID is a string-based class, we don't know what will please Pyjamas... - wid = self.getRoomWidget(bare_jid) - else: - wid = self.contact_panel - if show == 'unavailable': # XXX: save some resources as for now we only need 'unavailable' - for plugin in self.plugins.values(): - if hasattr(plugin, 'presenceReceivedTrigger'): - plugin.presenceReceivedTrigger(entity_jid, show, priority, statuses) - if wid: - wid.setConnected(entity_jid.bare, entity_jid.resource, show, priority, statuses) + # def _presenceUpdateCb(self, entity, show, priority, statuses): + # entity_jid = jid.JID(entity) + # if self.whoami and self.whoami == entity_jid: # XXX: QnD way to get our presence/status + # assert(isinstance(self.status_panel, panels.PresenceStatusPanel)) + # self.status_panel.setPresence(show) # pylint: disable=E1103 + # if statuses: + # self.status_panel.setStatus(statuses.values()[0]) # pylint: disable=E1103 + # else: + # bare_jid = entity_jid.bareJID() + # if bare_jid.full() in self.room_list or bare_jid in self.room_list: # as JID is a string-based class, we don't know what will please Pyjamas... + # wid = self.getRoomWidget(bare_jid) + # else: + # wid = self.contact_panel + # if show == 'unavailable': # XXX: save some resources as for now we only need 'unavailable' + # for plugin in self.plugins.values(): + # if hasattr(plugin, 'presenceReceivedTrigger'): + # plugin.presenceReceivedTrigger(entity_jid, show, priority, statuses) + # if wid: + # wid.setConnected(entity_jid.bare, entity_jid.resource, show, priority, statuses) - def _roomJoinedCb(self, room_jid_s, room_nicks, user_nick): - chat_panel = self.getOrCreateRoomWidget(jid.JID(room_jid_s)) - chat_panel.setUserNick(user_nick) - chat_panel.setPresents(room_nicks) - chat_panel.refresh() + # def _roomJoinedCb(self, room_jid_s, room_nicks, user_nick): + # chat_panel = self.getOrCreateRoomWidget(jid.JID(room_jid_s)) + # chat_panel.setUserNick(user_nick) + # chat_panel.setPresents(room_nicks) + # chat_panel.refresh() - def _roomLeftCb(self, room_jid_s, room_nicks, user_nick): - try: - del self.room_list[room_jid_s] - except KeyError: - try: # as JID is a string-based class, we don't know what will please Pyjamas... - del self.room_list[jid.JID(room_jid_s)] - except KeyError: - pass + # def _roomLeftCb(self, room_jid_s, room_nicks, user_nick): + # try: + # del self.room_list[room_jid_s] + # except KeyError: + # try: # as JID is a string-based class, we don't know what will please Pyjamas... + # del self.room_list[jid.JID(room_jid_s)] + # except KeyError: + # pass - def _roomUserJoinedCb(self, room_jid_s, user_nick, user_data): - lib_wid = self.getOrCreateRoomWidget(jid.JID(room_jid_s)) - if lib_wid: - lib_wid.userJoined(user_nick, user_data) + # def _roomUserJoinedCb(self, room_jid_s, user_nick, user_data): + # lib_wid = self.getOrCreateRoomWidget(jid.JID(room_jid_s)) + # if lib_wid: + # lib_wid.userJoined(user_nick, user_data) - def _roomUserLeftCb(self, room_jid_s, user_nick, user_data): - lib_wid = self.getRoomWidget(jid.JID(room_jid_s)) - if lib_wid: - lib_wid.userLeft(user_nick, user_data) + # def _roomUserLeftCb(self, room_jid_s, user_nick, user_data): + # lib_wid = self.getRoomWidget(jid.JID(room_jid_s)) + # if lib_wid: + # lib_wid.userLeft(user_nick, user_data) - def _roomUserChangedNickCb(self, room_jid_s, old_nick, new_nick): - """Called when an user joined a MUC room""" - lib_wid = self.getRoomWidget(jid.JID(room_jid_s)) - if lib_wid: - lib_wid.changeUserNick(old_nick, new_nick) + # def _roomUserChangedNickCb(self, room_jid_s, old_nick, new_nick): + # """Called when an user joined a MUC room""" + # lib_wid = self.getRoomWidget(jid.JID(room_jid_s)) + # if lib_wid: + # lib_wid.changeUserNick(old_nick, new_nick) - def _tarotGameStartedCb(self, waiting, room_jid_s, referee, players): - lib_wid = self.getRoomWidget(jid.JID(room_jid_s)) - if lib_wid: - lib_wid.startGame("Tarot", waiting, referee, players) + # def _tarotGameStartedCb(self, waiting, room_jid_s, referee, players): + # lib_wid = self.getRoomWidget(jid.JID(room_jid_s)) + # if lib_wid: + # lib_wid.startGame("Tarot", waiting, referee, players) - def _tarotGameGenericCb(self, event_name, room_jid_s, args): - lib_wid = self.getRoomWidget(jid.JID(room_jid_s)) - if lib_wid: - getattr(lib_wid.getGame("Tarot"), event_name)(*args) + # def _tarotGameGenericCb(self, event_name, room_jid_s, args): + # lib_wid = self.getRoomWidget(jid.JID(room_jid_s)) + # if lib_wid: + # getattr(lib_wid.getGame("Tarot"), event_name)(*args) - def _radioColStartedCb(self, waiting, room_jid_s, referee, players, queue_data): - lib_wid = self.getRoomWidget(jid.JID(room_jid_s)) - if lib_wid: - lib_wid.startGame("RadioCol", waiting, referee, players, queue_data) + # def _radioColStartedCb(self, waiting, room_jid_s, referee, players, queue_data): + # lib_wid = self.getRoomWidget(jid.JID(room_jid_s)) + # if lib_wid: + # lib_wid.startGame("RadioCol", waiting, referee, players, queue_data) - def _radioColGenericCb(self, event_name, room_jid_s, args): - lib_wid = self.getRoomWidget(jid.JID(room_jid_s)) - if lib_wid: - getattr(lib_wid.getGame("RadioCol"), event_name)(*args) + # def _radioColGenericCb(self, event_name, room_jid_s, args): + # lib_wid = self.getRoomWidget(jid.JID(room_jid_s)) + # if lib_wid: + # getattr(lib_wid.getGame("RadioCol"), event_name)(*args) def _getPresenceStatusesCb(self, presence_data): for entity in presence_data: @@ -923,19 +819,19 @@ 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): - """Callback when a new chat state is received. - @param from_jid_s: JID of the contact who sent his state, or '@ALL@' - @param state: new state (string) - """ - if from_jid_s == '@ALL@': - for lib_wid in self.libervia_widgets: - if isinstance(lib_wid, panels.ChatPanel): - lib_wid.setState(state, nick=C.ALL_OCCUPANTS) - return - from_jid = jid.JID(from_jid_s) - lib_wid = self.getLiberviaWidget(panels.ChatPanel, {'item': from_jid}, ignoreOtherTabs=False) - lib_wid.setState(state, nick=from_jid.resource) + # def _chatStateReceivedCb(self, from_jid_s, state): + # """Callback when a new chat state is received. + # @param from_jid_s: JID of the contact who sent his state, or '@ALL@' + # @param state: new state (string) + # """ + # if from_jid_s == '@ALL@': + # for lib_wid in self.libervia_widgets: + # if isinstance(lib_wid, panels.ChatPanel): + # lib_wid.setState(state, nick=C.ALL_OCCUPANTS) + # return + # from_jid = jid.JID(from_jid_s) + # lib_wid = self.getLiberviaWidget(panels.ChatPanel, {'item': from_jid}, ignoreOtherTabs=False) + # lib_wid.setState(state, nick=from_jid.resource) def _askConfirmation(self, confirmation_id, confirmation_type, data): answer_data = {} @@ -974,41 +870,43 @@ "Your message can't be sent", Width="400px").center() log.error("sendError: %s" % str(errorData)) - def send(self, targets, text, extra={}): - """Send a message to any target type. - @param targets: list of tuples (type, entities, addr) with: - - type in ("PUBLIC", "GROUP", "COMMENT", "STATUS" , "groupchat" , "chat") - - entities could be a JID, a list groups, a node hash... depending the target - - addr in ("To", "Cc", "Bcc") - ignore case - @param text: the message content - @param extra: options - """ - # FIXME: too many magic strings, we should use constants instead - addresses = [] - for target in targets: - type_, entities, addr = target[0], target[1], 'to' if len(target) < 3 else target[2].lower() - if type_ in ("PUBLIC", "GROUP"): - self.bridge.call("sendMblog", None, type_, entities if type_ == "GROUP" else None, text, extra) - elif type_ == "COMMENT": - self.bridge.call("sendMblogComment", None, entities, text, extra) - elif type_ == "STATUS": - assert(isinstance(self.status_panel, panels.PresenceStatusPanel)) - self.bridge.call('setStatus', None, self.status_panel.presence, text) # pylint: disable=E1103 - elif type_ in ("groupchat", "chat"): - addresses.append((addr, entities)) - else: - log.error("Unknown target type") - if addresses: - if len(addresses) == 1 and addresses[0][0] == 'to': - to_jid_s = addresses[0][1] - for plugin in self.plugins.values(): - if hasattr(plugin, 'sendMessageTrigger'): - if not plugin.sendMessageTrigger(jid.JID(to_jid_s), text, type_, extra): - return # plugin returned False to interrupt the process - self.bridge.call('sendMessage', (None, self.sendError), to_jid_s, text, '', type_, extra) - else: - extra.update({'address': '\n'.join([('%s:%s' % entry) for entry in addresses])}) - self.bridge.call('sendMessage', (None, self.sendError), self.whoami.domain, text, '', type_, extra) + # FIXME: this method is fat too complicated and depend of widget type + # must be refactored and moved to each widget instead + # def send(self, targets, text, extra={}): + # """Send a message to any target type. + # @param targets: list of tuples (type, entities, addr) with: + # - type in ("PUBLIC", "GROUP", "COMMENT", "STATUS" , "groupchat" , "chat") + # - entities could be a JID, a list groups, a node hash... depending the target + # - addr in ("To", "Cc", "Bcc") - ignore case + # @param text: the message content + # @param extra: options + # """ + # # FIXME: too many magic strings, we should use constants instead + # addresses = [] + # for target in targets: + # type_, entities, addr = target[0], target[1], 'to' if len(target) < 3 else target[2].lower() + # if type_ in ("PUBLIC", "GROUP"): + # self.bridge.call("sendMblog", None, type_, entities if type_ == "GROUP" else None, text, extra) + # elif type_ == "COMMENT": + # self.bridge.call("sendMblogComment", None, entities, text, extra) + # elif type_ == "STATUS": + # assert(isinstance(self.status_panel, panels.PresenceStatusPanel)) + # self.bridge.call('setStatus', None, self.status_panel.presence, text) # pylint: disable=E1103 + # elif type_ in ("groupchat", "chat"): + # addresses.append((addr, entities)) + # else: + # log.error("Unknown target type") + # if addresses: + # if len(addresses) == 1 and addresses[0][0] == 'to': + # to_jid_s = addresses[0][1] + # for plugin in self.plugins.values(): + # if hasattr(plugin, 'sendMessageTrigger'): + # if not plugin.sendMessageTrigger(jid.JID(to_jid_s), text, type_, extra): + # return # plugin returned False to interrupt the process + # self.bridge.call('sendMessage', (None, self.sendError), to_jid_s, text, '', type_, extra) + # else: + # extra.update({'address': '\n'.join([('%s:%s' % entry) for entry in addresses])}) + # self.bridge.call('sendMessage', (None, self.sendError), self.whoami.domain, text, '', type_, extra) def showWarning(self, type_=None, msg=None): """Display a popup information message, e.g. to notify the recipient of a message being composed. @@ -1024,4 +922,3 @@ if __name__ == '__main__': app = SatWebFrontend() app.onModuleLoad() - pyjd.run() diff -r bade589dbd5a -r a5019e62c3e9 src/browser/public/libervia.css --- a/src/browser/public/libervia.css Thu Oct 23 16:56:36 2014 +0200 +++ b/src/browser/public/libervia.css Sat Jan 24 01:45:39 2015 +0100 @@ -420,7 +420,7 @@ } /* Contact List */ -div.contactPanel { +div.contactList { width: 100%; /* We want the contact panel to not use all the available height when displayed in the unibox panel (grey part), because the dialogs panels (white part) should @@ -449,11 +449,11 @@ font-size: large; } -.groupList { +.groupPanel { width: 100%; } -.groupList tr:first-child td { +.groupPanel tr:first-child td { padding-top: 10px; } diff -r bade589dbd5a -r a5019e62c3e9 src/browser/sat_browser/base_menu.py --- a/src/browser/sat_browser/base_menu.py Thu Oct 23 16:56:36 2014 +0200 +++ b/src/browser/sat_browser/base_menu.py Sat Jan 24 01:45:39 2015 +0100 @@ -54,6 +54,7 @@ self.data = data def execute(self): + log.debug("execute %s" % self.callback) self.callback(self.data) if self.data else self.callback() @@ -307,7 +308,7 @@ def addMenuItem(self, path, path_i18n, types, menu_cmd, asHTML=False): return self.node.addMenuItem(path, path_i18n, types, menu_cmd, asHTML).item - def addCategory(self, path, path_i18n, types, menu_bar): + def addCategory(self, path, path_i18n, types, menu_bar=None): return self.node.addCategory(path, path_i18n, types, menu_bar).item def addItem(self, item, asHTML=None, popup=None): diff -r bade589dbd5a -r a5019e62c3e9 src/browser/sat_browser/base_panels.py --- a/src/browser/sat_browser/base_panels.py Thu Oct 23 16:56:36 2014 +0200 +++ b/src/browser/sat_browser/base_panels.py Sat Jan 24 01:45:39 2015 +0100 @@ -26,7 +26,6 @@ from pyjamas.ui.AbsolutePanel import AbsolutePanel from pyjamas.ui.VerticalPanel import VerticalPanel from pyjamas.ui.HorizontalPanel import HorizontalPanel -from pyjamas.ui.HTMLPanel import HTMLPanel from pyjamas.ui.Button import Button from pyjamas.ui.HTML import HTML from pyjamas.ui.SimplePanel import SimplePanel @@ -39,29 +38,10 @@ from pyjamas.ui.ClickListener import ClickHandler from pyjamas import DOM -from datetime import datetime -from time import time - import html_tools from constants import Const as C -class ChatText(HTMLPanel): - - def __init__(self, timestamp, nick, mymess, msg, xhtml=None): - _date = datetime.fromtimestamp(float(timestamp or time())) - _msg_class = ["chat_text_msg"] - if mymess: - _msg_class.append("chat_text_mymess") - HTMLPanel.__init__(self, "%(timestamp)s %(nick)s %(msg)s" % - {"timestamp": _date.strftime("%H:%M"), - "nick": "[%s]" % html_tools.html_sanitize(nick), - "msg_class": ' '.join(_msg_class), - "msg": strings.addURLToText(html_tools.html_sanitize(msg)) if not xhtml else html_tools.inlineRoot(xhtml)} # FIXME: images and external links must be removed according to preferences - ) - self.setStyleName('chatText') - - class Occupant(HTML): """Occupant of a MUC room""" diff -r bade589dbd5a -r a5019e62c3e9 src/browser/sat_browser/base_widget.py --- a/src/browser/sat_browser/base_widget.py Thu Oct 23 16:56:36 2014 +0200 +++ b/src/browser/sat_browser/base_widget.py Sat Jan 24 01:45:39 2015 +0100 @@ -177,12 +177,13 @@ menu_styles.update(styles) base_menu.GenericMenuBar.__init__(self, host, vertical=vertical, styles=menu_styles) - if hasattr(parent, 'addMenus'): - # regroup all the dynamic menu categories in a sub-menu - sub_menu = WidgetSubMenuBar(host, vertical=True) - parent.addMenus(sub_menu) - if len(sub_menu.getCategories()) > 0: - self.addCategory('', '', 'plugins', sub_menu) + # FIXME + # if hasattr(parent, 'addMenus'): + # # regroup all the dynamic menu categories in a sub-menu + # sub_menu = WidgetSubMenuBar(host, vertical=True) + # parent.addMenus(sub_menu) + # if len(sub_menu.getCategories()) > 0: + # self.addCategory('', '', 'plugins', sub_menu) @classmethod def getCategoryHTML(cls, menu_name_i18n, type_): @@ -231,35 +232,37 @@ def __init__(self, host, title='', info=None, selectable=False): """Init the widget + @param host (SatWebFrontend): SatWebFrontend instance @param title (str): title shown in the header of the widget @param info (str, callable): info shown in the header of the widget - @param selectable (bool): True is widget can be selected by user""" + @param selectable (bool): True is widget can be selected by user + """ VerticalPanel.__init__(self) DropCell.__init__(self, host) ClickHandler.__init__(self) - self.__selectable = selectable - self.__title_id = HTMLPanel.createUniqueId() - self.__setting_button_id = HTMLPanel.createUniqueId() - self.__close_button_id = HTMLPanel.createUniqueId() - self.__title = Label(title) - self.__title.setStyleName('widgetHeader_title') + self._selectable = selectable + self._title_id = HTMLPanel.createUniqueId() + self._setting_button_id = HTMLPanel.createUniqueId() + self._close_button_id = HTMLPanel.createUniqueId() + self._title = Label(title) + self._title.setStyleName('widgetHeader_title') if info is not None: if isinstance(info, str): - self.__info = HTML(info) + self._info = HTML(info) else: # the info will be set by a callback - assert(callable(info)) - self.__info = HTML() - info(self.__info.setHTML) - self.__info.setStyleName('widgetHeader_info') + assert callable(info) + self._info = HTML() + info(self._info.setHTML) + self._info.setStyleName('widgetHeader_info') else: - self.__info = None + self._info = None self._close_listeners = [] - header = WidgetHeader(self, host, self.__title, self.__info) + header = WidgetHeader(self, host, self._title, self._info) self.add(header) self.setSize('100%', '100%') self.addStyleName('widget') - if self.__selectable: + if self._selectable: self.addClickListener(self) def onClose(sender): @@ -268,10 +271,10 @@ self.host.uni_box.onWidgetClosed(sender) self.addCloseListener(onClose) - self.host.registerWidget(self) + # self.host.registerWidget(self) # FIXME def getDebugName(self): - return "%s (%s)" % (self, self.__title.getText()) + return "%s (%s)" % (self, self._title.getText()) def getWidgetsPanel(self, expect=True): return self.getParent(WidgetsPanel, expect) @@ -392,28 +395,28 @@ def setTitle(self, text): """change the title in the header of the widget @param text: text of the new title""" - self.__title.setText(text) + self._title.setText(text) def setHeaderInfo(self, text): """change the info in the header of the widget @param text: text of the new title""" try: - self.__info.setHTML(text) + self._info.setHTML(text) except TypeError: log.error("LiberviaWidget.setInfo: info widget has not been initialized!") def isSelectable(self): - return self.__selectable + return self._selectable def setSelectable(self, selectable): - if not self.__selectable: + if not self._selectable: try: self.removeClickListener(self) except ValueError: pass if self.selectable and not self in self._clickListeners: self.addClickListener(self) - self.__selectable = selectable + self._selectable = selectable def getWarningData(self): """ Return exposition warning level when this widget is selected and something is sent to it @@ -425,7 +428,7 @@ - MISC - NONE """ - if not self.__selectable: + if not self._selectable: log.error("getWarningLevel must not be called for an unselectable widget") raise Exception # TODO: cleaner warning types (more general constants) diff -r bade589dbd5a -r a5019e62c3e9 src/browser/sat_browser/blog.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/browser/sat_browser/blog.py Sat Jan 24 01:45:39 2015 +0100 @@ -0,0 +1,698 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Libervia: a Salut à Toi frontend +# Copyright (C) 2011, 2012, 2013, 2014 Jérôme Poisson + +# 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 . + +import pyjd # this is dummy in pyjs +from sat.core.log import getLogger +log = getLogger(__name__) + +from sat.core.i18n import _ + +from pyjamas.ui.SimplePanel import SimplePanel +from pyjamas.ui.VerticalPanel import VerticalPanel +from pyjamas.ui.HorizontalPanel import HorizontalPanel +from pyjamas.ui.HTMLPanel import HTMLPanel +from pyjamas.ui.Label import Label +from pyjamas.ui.Button import Button +from pyjamas.ui.HTML import HTML +from pyjamas.ui.Image import Image +from pyjamas.ui.ClickListener import ClickHandler +from pyjamas.ui.FlowPanel import FlowPanel +from pyjamas.ui.KeyboardListener import KEY_ENTER, KeyboardHandler +from pyjamas.ui.FocusListener import FocusHandler +from pyjamas.Timer import Timer + +from datetime import datetime +from time import time + +import html_tools +import base_panels +import dialog +import base_widget +import richtext +from constants import Const as C +from sat_frontends.quick_frontend import quick_widgets + +# TODO: at some point we should decide which behaviors to keep and remove these two constants +TOGGLE_EDITION_USE_ICON = False # set to True to use an icon inside the "toggle syntax" button +NEW_MESSAGE_USE_BUTTON = False # set to True to display the "New message" button instead of an empty entry + + +class MicroblogItem(): + # XXX: should be moved in a separated module + + def __init__(self, data): + self.id = data['id'] + self.type = data.get('type', 'main_item') + self.empty = data.get('new', False) + self.title = data.get('title', '') + self.title_xhtml = data.get('title_xhtml', '') + self.content = data.get('content', '') + self.content_xhtml = data.get('content_xhtml', '') + self.author = data['author'] + self.updated = float(data.get('updated', 0)) # XXX: int doesn't work here + self.published = float(data.get('published', self.updated)) # XXX: int doesn't work here + self.service = data.get('service', '') + self.node = data.get('node', '') + self.comments = data.get('comments', False) + self.comments_service = data.get('comments_service', '') + self.comments_node = data.get('comments_node', '') + + +class MicroblogEntry(SimplePanel, ClickHandler, FocusHandler, KeyboardHandler): + + def __init__(self, blog_panel, data): + """ + @param blog_panel: the parent panel + @param data: dict containing the blog item data, or a MicroblogItem instance. + """ + self._base_item = data if isinstance(data, MicroblogItem) else MicroblogItem(data) + for attr in ['id', 'type', 'empty', 'title', 'title_xhtml', 'content', 'content_xhtml', + 'author', 'updated', 'published', 'comments', 'service', 'node', + 'comments_service', 'comments_node']: + getter = lambda attr: lambda inst: getattr(inst._base_item, attr) + setter = lambda attr: lambda inst, value: setattr(inst._base_item, attr, value) + setattr(MicroblogEntry, attr, property(getter(attr), setter(attr))) + + SimplePanel.__init__(self) + self._blog_panel = blog_panel + + self.panel = FlowPanel() + self.panel.setStyleName('mb_entry') + + self.header = HTMLPanel('') + self.panel.add(self.header) + + self.entry_actions = VerticalPanel() + self.entry_actions.setStyleName('mb_entry_actions') + self.panel.add(self.entry_actions) + + entry_avatar = SimplePanel() + entry_avatar.setStyleName('mb_entry_avatar') + # FIXME + self.avatar = Image(C.DEFAULT_AVATAR) # self._blog_panel.host.getAvatar(self.author)) + entry_avatar.add(self.avatar) + self.panel.add(entry_avatar) + + if TOGGLE_EDITION_USE_ICON: + self.entry_dialog = HorizontalPanel() + else: + self.entry_dialog = VerticalPanel() + self.entry_dialog.setStyleName('mb_entry_dialog') + self.panel.add(self.entry_dialog) + + self.add(self.panel) + ClickHandler.__init__(self) + self.addClickListener(self) + + self.__pub_data = (self.service, self.node, self.id) + self.__setContent() + + def __setContent(self): + """Actually set the entry content (header, icons, bubble...)""" + self.delete_label = self.update_label = self.comment_label = None + self.bubble = self._current_comment = None + self.__setHeader() + self.__setBubble() + self.__setIcons() + + def __setHeader(self): + """Set the entry header""" + if self.empty: + return + update_text = u" — ✍ " + "" % datetime.fromtimestamp(self.updated) + self.header.setHTML("""
+ on + %(updated)s +
""" % {'author': html_tools.html_sanitize(self.author), + 'published': datetime.fromtimestamp(self.published), + 'updated': update_text if self.published != self.updated else '' + } + ) + + def __setIcons(self): + """Set the entry icons (delete, update, comment)""" + if self.empty: + return + + def addIcon(label, title): + label = Label(label) + label.setTitle(title) + label.addClickListener(self) + self.entry_actions.add(label) + return label + + if self.comments: + self.comment_label = addIcon(u"↶", "Comment this message") + self.comment_label.setStyleName('mb_entry_action_larger') + is_publisher = self.author == self._blog_panel.host.whoami.bare + if is_publisher: + self.update_label = addIcon(u"✍", "Edit this message") + if is_publisher or str(self.node).endswith(self._blog_panel.host.whoami.bare): + self.delete_label = addIcon(u"✗", "Delete this message") + + def updateAvatar(self, new_avatar): + """Change the avatar of the entry + @param new_avatar: path to the new image""" + self.avatar.setUrl(new_avatar) + + def onClick(self, sender): + if sender == self: + self._blog_panel.setSelectedEntry(self) + elif sender == self.delete_label: + self._delete() + elif sender == self.update_label: + self.edit(True) + elif sender == self.comment_label: + self._comment() + + def __modifiedCb(self, content): + """Send the new content to the backend + @return: False to restore the original content if a deletion has been cancelled + """ + if not content['text']: # previous content has been emptied + self._delete(True) + return False + extra = {'published': str(self.published)} + if isinstance(self.bubble, richtext.RichTextEditor): + # TODO: if the user change his parameters after the message edition started, + # the message syntax could be different then the current syntax: pass the + # message syntax in extra for the frontend to use it instead of current syntax. + extra.update({'content_rich': content['text'], 'title': content['title']}) + if self.empty: + if self.type == 'main_item': + self._blog_panel.host.bridge.call('sendMblog', None, None, self._blog_panel.accepted_groups, content['text'], extra) + else: + self._blog_panel.host.bridge.call('sendMblogComment', None, self._parent_entry.comments, content['text'], extra) + else: + self._blog_panel.host.bridge.call('updateMblog', None, self.__pub_data, self.comments, content['text'], extra) + return True + + def __afterEditCb(self, content): + """Remove the entry if it was an empty one (used for creating a new blog post). + Data for the actual new blog post will be received from the bridge""" + if self.empty: + self._blog_panel.removeEntry(self.type, self.id) + if self.type == 'main_item': # restore the "New message" button + self._blog_panel.refresh() + else: # allow to create a new comment + self._parent_entry._current_comment = None + self.entry_dialog.setWidth('auto') + try: + self.toggle_syntax_button.removeFromParent() + except TypeError: + pass + + def __setBubble(self, edit=False): + """Set the bubble displaying the initial content.""" + content = {'text': self.content_xhtml if self.content_xhtml else self.content, + 'title': self.title_xhtml if self.title_xhtml else self.title} + if self.content_xhtml: + content.update({'syntax': C.SYNTAX_XHTML}) + if self.author != self._blog_panel.host.whoami.bare: + options = ['read_only'] + else: + options = [] if self.empty else ['update_msg'] + self.bubble = richtext.RichTextEditor(self._blog_panel.host, content, self.__modifiedCb, self.__afterEditCb, options) + else: # assume raw text message have no title + self.bubble = base_panels.LightTextEditor(content, self.__modifiedCb, self.__afterEditCb, options={'no_xhtml': True}) + self.bubble.addStyleName("bubble") + try: + self.toggle_syntax_button.removeFromParent() + except TypeError: + pass + self.entry_dialog.add(self.bubble) + self.edit(edit) + self.bubble.addEditListener(self.__showWarning) + + def __showWarning(self, sender, keycode): + if keycode == KEY_ENTER: + self._blog_panel.host.showWarning(None, None) + else: + self._blog_panel.host.showWarning(*self._blog_panel.getWarningData(self.type == 'comment')) + + def _delete(self, empty=False): + """Ask confirmation for deletion. + @return: False if the deletion has been cancelled.""" + def confirm_cb(answer): + if answer: + self._blog_panel.host.bridge.call('deleteMblog', None, self.__pub_data, self.comments) + else: # restore the text if it has been emptied during the edition + self.bubble.setContent(self.bubble._original_content) + + if self.empty: + text = _("New ") + (_("message") if self.comments else _("comment")) + _(" without body has been cancelled.") + dialog.InfoDialog(_("Information"), text).show() + return + text = "" + if empty: + text = (_("Message") if self.comments else _("Comment")) + _(" body has been emptied.
") + target = _('message and all its comments') if self.comments else _('comment') + text += _("Do you really want to delete this %s?") % target + dialog.ConfirmDialog(confirm_cb, text=text).show() + + def _comment(self): + """Add an empty entry for a new comment""" + if self._current_comment: + self._current_comment.bubble.setFocus(True) + self._blog_panel.setSelectedEntry(self._current_comment, True) + return + data = {'id': str(time()), + 'new': True, + 'type': 'comment', + 'author': self._blog_panel.host.whoami.bare, + 'service': self.comments_service, + 'node': self.comments_node + } + entry = self._blog_panel.addEntry(data) + if entry is None: + log.info("The entry of id %s can not be commented" % self.id) + return + entry._parent_entry = self + self._current_comment = entry + self.edit(True, entry) + self._blog_panel.setSelectedEntry(entry, True) + + def edit(self, edit, entry=None): + """Toggle the bubble between display and edit mode + @edit: boolean value + @entry: MicroblogEntry instance, or None to use self + """ + if entry is None: + entry = self + try: + entry.toggle_syntax_button.removeFromParent() + except TypeError: + pass + entry.bubble.edit(edit) + if edit: + if isinstance(entry.bubble, richtext.RichTextEditor): + image = 'A' + html = 'raw text' + title = _('Switch to raw text edition') + else: + image = '' + html = 'rich text' + title = _('Switch to rich text edition') + if TOGGLE_EDITION_USE_ICON: + entry.entry_dialog.setWidth('80%') + entry.toggle_syntax_button = Button(image, entry.toggleContentSyntax) + entry.toggle_syntax_button.setTitle(title) + entry.entry_dialog.add(entry.toggle_syntax_button) + else: + entry.toggle_syntax_button = HTML(html) + entry.toggle_syntax_button.addClickListener(entry.toggleContentSyntax) + entry.toggle_syntax_button.addStyleName('mb_entry_toggle_syntax') + entry.entry_dialog.add(entry.toggle_syntax_button) + entry.toggle_syntax_button.setStyleAttribute('top', '-20px') # XXX: need to force CSS + entry.toggle_syntax_button.setStyleAttribute('left', '-20px') + + def toggleContentSyntax(self): + """Toggle the editor between raw and rich text""" + original_content = self.bubble.getOriginalContent() + rich = not isinstance(self.bubble, richtext.RichTextEditor) + if rich: + original_content['syntax'] = C.SYNTAX_XHTML + + def setBubble(text): + self.content = text + self.content_xhtml = text if rich else '' + self.content_title = self.content_title_xhtml = '' + self.bubble.removeFromParent() + self.__setBubble(True) + self.bubble.setOriginalContent(original_content) + if rich: + self.bubble.setDisplayContent() # needed in case the edition is aborted, to not end with an empty bubble + + text = self.bubble.getContent()['text'] + if not text: + setBubble(' ') # something different than empty string is needed to initialize the rich text editor + return + if not rich: + def confirm_cb(answer): + if answer: + self._blog_panel.host.bridge.call('syntaxConvert', setBubble, text, C.SYNTAX_CURRENT, C.SYNTAX_TEXT) + dialog.ConfirmDialog(confirm_cb, text=_("Do you really want to lose the title and text formatting?")).show() + else: + self._blog_panel.host.bridge.call('syntaxConvert', setBubble, text, C.SYNTAX_TEXT, C.SYNTAX_XHTML) + + +class MicroblogPanel(quick_widgets.QuickWidget, base_widget.LiberviaWidget): + warning_msg_public = "This message will be PUBLIC and everybody will be able to see it, even people you don't know" + warning_msg_group = "This message will be published for all the people of the group %s" + # FIXME: all the generic parts must be moved to quick_frontends + + def __init__(self, host, accepted_groups, profiles=None): + """Panel used to show microblog + + @param accepted_groups: groups displayed in this panel, if empty, show all microblogs from all contacts + """ + self.setAcceptedGroup(accepted_groups) + quick_widgets.QuickWidget.__init__(self, host, self.target, C.PROF_KEY_NONE) + base_widget.LiberviaWidget.__init__(self, host, ", ".join(accepted_groups), selectable=True) + self.entries = {} + self.comments = {} + self.selected_entry = None + self.vpanel = VerticalPanel() + self.vpanel.setStyleName('microblogPanel') + self.setWidget(self.vpanel) + + @property + def target(self): + return tuple(self.accepted_groups) + + def refresh(self): + """Refresh the display of this widget. If the unibox is disabled, + display the 'New message' button or an empty bubble on top of the panel""" + if hasattr(self, 'new_button'): + self.new_button.setVisible(self.host.uni_box is None) + return + if self.host.uni_box is None: + def addBox(): + if hasattr(self, 'new_button'): + self.new_button.setVisible(False) + data = {'id': str(time()), + 'new': True, + 'author': self.host.whoami.bare, + } + entry = self.addEntry(data) + entry.edit(True) + if NEW_MESSAGE_USE_BUTTON: + self.new_button = Button("New message", listener=addBox) + self.new_button.setStyleName("microblogNewButton") + self.vpanel.insert(self.new_button, 0) + elif not self.getNewMainEntry(): + addBox() + + def getNewMainEntry(self): + """Get the new entry being edited, or None if it doesn't exists. + + @return (MicroblogEntry): the new entry being edited. + """ + try: + first = self.vpanel.children[0] + except IndexError: + return None + assert(first.type == 'main_item') + return first if first.empty else None + + @classmethod + def registerClass(cls): + base_widget.LiberviaWidget.addDropKey("GROUP", cls.createPanel) + base_widget.LiberviaWidget.addDropKey("CONTACT_TITLE", cls.createMetaPanel) + + @classmethod + def createPanel(cls, host, item): + """Generic panel creation for one, several or all groups (meta). + @parem host: the SatWebFrontend instance + @param item: single group as a string, list of groups + (as an array) or None (for the meta group = "all groups") + @return: the created MicroblogPanel + """ + _items = item if isinstance(item, list) else ([] if item is None else [item]) + _type = 'ALL' if _items == [] else 'GROUP' + # XXX: pyjamas doesn't support use of cls directly + _new_panel = MicroblogPanel(host, _items) + host.FillMicroblogPanel(_new_panel) + host.bridge.call('getMassiveLastMblogs', _new_panel.massiveInsert, _type, _items, 10) + host.setSelected(_new_panel) + _new_panel.refresh() + return _new_panel + + @classmethod + def createMetaPanel(cls, host, item): + """Needed for the drop keys to not be mixed between meta panel and panel for "Contacts" group""" + return MicroblogPanel.createPanel(host, None) + + @property + def accepted_groups(self): + return self._accepted_groups + + def matchEntity(self, item): + """ + @param item: single group as a string, list of groups + (as an array) or None (for the meta group = "all groups") + @return: True if self matches the given entity + """ + groups = item if isinstance(item, list) else ([] if item is None else [item]) + groups.sort() # sort() do not return the sorted list: do it here, not on the "return" line + return self.accepted_groups == groups + + def getWarningData(self, comment=None): + """ + @param comment: True if the composed message is a comment. If None, consider we are + composing from the unibox and guess the message type from self.selected_entry + @return: a couple (type, msg) for calling self.host.showWarning""" + if comment is None: # composing from the unibox + if self.selected_entry and not self.selected_entry.comments: + log.error("an item without comment is selected") + return ("NONE", None) + comment = self.selected_entry is not None + if comment: + return ("PUBLIC", "This is a comment and keep the initial post visibility, so it is potentialy public") + elif not self._accepted_groups: + # we have a meta MicroblogPanel, we publish publicly + return ("PUBLIC", self.warning_msg_public) + else: + # we only accept one group at the moment + # FIXME: manage several groups + return ("GROUP", self.warning_msg_group % self._accepted_groups[0]) + + def onTextEntered(self, text): + if self.selected_entry: + # we are entering a comment + comments_url = self.selected_entry.comments + if not comments_url: + raise Exception("ERROR: the comments URL is empty") + target = ("COMMENT", comments_url) + elif not self._accepted_groups: + # we are entering a public microblog + target = ("PUBLIC", None) + else: + # we are entering a microblog restricted to a group + # FIXME: manage several groups + target = ("GROUP", self._accepted_groups[0]) + self.host.send([target], text) + + def accept_all(self): + return not self._accepted_groups # we accept every microblog only if we are not filtering by groups + + def getEntries(self): + """Ask all the entries for the currenly accepted groups, + and fill the panel""" + + def massiveInsert(self, mblogs): + """Insert several microblogs at once + @param mblogs: dictionary of microblogs, as the result of getMassiveLastGroupBlogs + """ + count = sum([len(value) for value in mblogs.values()]) + log.debug("Massive insertion of %d microblogs" % count) + for publisher in mblogs: + log.debug("adding blogs for [%s]" % publisher) + for mblog in mblogs[publisher]: + if not "content" in mblog: + log.warning("No content found in microblog [%s]" % mblog) + continue + self.addEntry(mblog) + + def mblogsInsert(self, mblogs): + """ Insert several microblogs at once + @param mblogs: list of microblogs + """ + for mblog in mblogs: + if not "content" in mblog: + log.warning("No content found in microblog [%s]" % mblog) + continue + self.addEntry(mblog) + + def _chronoInsert(self, vpanel, entry, reverse=True): + """ Insert an entry in chronological order + @param vpanel: VerticalPanel instance + @param entry: MicroblogEntry + @param reverse: more recent entry on top if True, chronological order else""" + assert(isinstance(reverse, bool)) + if entry.empty: + entry.published = time() + # we look for the right index to insert our entry: + # if reversed, we insert the entry above the first entry + # in the past + idx = 0 + + for child in vpanel.children: + if not isinstance(child, MicroblogEntry): + idx += 1 + continue + condition_to_stop = child.empty or (child.published > entry.published) + if condition_to_stop != reverse: # != is XOR + break + idx += 1 + + vpanel.insert(entry, idx) + + def addEntry(self, data): + """Add an entry to the panel + @param data: dict containing the item data + @return: the added entry, or None + """ + _entry = MicroblogEntry(self, data) + if _entry.type == "comment": + comments_hash = (_entry.service, _entry.node) + if not comments_hash in self.comments: + # The comments node is not known in this panel + return None + parent = self.comments[comments_hash] + parent_idx = self.vpanel.getWidgetIndex(parent) + # we find or create the panel where the comment must be inserted + try: + sub_panel = self.vpanel.getWidget(parent_idx + 1) + except IndexError: + sub_panel = None + if not sub_panel or not isinstance(sub_panel, VerticalPanel): + sub_panel = VerticalPanel() + sub_panel.setStyleName('microblogPanel') + sub_panel.addStyleName('subPanel') + self.vpanel.insert(sub_panel, parent_idx + 1) + for idx in xrange(0, len(sub_panel.getChildren())): + comment = sub_panel.getIndexedChild(idx) + if comment.id == _entry.id: + # update an existing comment + sub_panel.remove(comment) + sub_panel.insert(_entry, idx) + return _entry + # we want comments to be inserted in chronological order + self._chronoInsert(sub_panel, _entry, reverse=False) + return _entry + + if _entry.id in self.entries: # update + idx = self.vpanel.getWidgetIndex(self.entries[_entry.id]) + self.vpanel.remove(self.entries[_entry.id]) + self.vpanel.insert(_entry, idx) + else: # new entry + self._chronoInsert(self.vpanel, _entry) + self.entries[_entry.id] = _entry + + if _entry.comments: + # entry has comments, we keep the comments service/node as a reference + comments_hash = (_entry.comments_service, _entry.comments_node) + self.comments[comments_hash] = _entry + self.host.bridge.call('getMblogComments', self.mblogsInsert, _entry.comments_service, _entry.comments_node) + + return _entry + + def removeEntry(self, type_, id_): + """Remove an entry from the panel + @param type_: entry type ('main_item' or 'comment') + @param id_: entry id + """ + for child in self.vpanel.getChildren(): + if isinstance(child, MicroblogEntry) and type_ == 'main_item': + if child.id == id_: + main_idx = self.vpanel.getWidgetIndex(child) + try: + sub_panel = self.vpanel.getWidget(main_idx + 1) + if isinstance(sub_panel, VerticalPanel): + sub_panel.removeFromParent() + except IndexError: + pass + child.removeFromParent() + self.selected_entry = None + break + elif isinstance(child, VerticalPanel) and type_ == 'comment': + for comment in child.getChildren(): + if comment.id == id_: + comment.removeFromParent() + self.selected_entry = None + break + + def ensureVisible(self, entry): + """Scroll to an entry to ensure its visibility + + @param entry (MicroblogEntry): the entry + """ + try: + self.vpanel.getParent().ensureVisible(entry) # scroll to the clicked entry + except AttributeError: + log.warning("FIXME: MicroblogPanel.vpanel should be wrapped in a ScrollPanel!") + + def setSelectedEntry(self, entry, ensure_visible=False): + """Select an entry. + + @param entry (MicroblogEntry): the entry to select + @param ensure_visible (boolean): if True, also scroll to the entry + """ + if ensure_visible: + self.ensureVisible(entry) + + if not self.host.uni_box or not entry.comments: + entry.addStyleName('selected_entry') # blink the clicked entry + clicked_entry = entry # entry may be None when the timer is done + Timer(500, lambda timer: clicked_entry.removeStyleName('selected_entry')) + if not self.host.uni_box: + return # unibox is disabled + + # from here the previous behavior (toggle main item selection) is conserved + entry = entry if entry.comments else None + if self.selected_entry == entry: + entry = None + if self.selected_entry: + self.selected_entry.removeStyleName('selected_entry') + if entry: + log.debug("microblog entry selected (author=%s)" % entry.author) + entry.addStyleName('selected_entry') + self.selected_entry = entry + + def updateValue(self, type_, jid, value): + """Update a jid value in entries + @param type_: one of 'avatar', 'nick' + @param jid: jid concerned + @param value: new value""" + def updateVPanel(vpanel): + for child in vpanel.children: + if isinstance(child, MicroblogEntry) and child.author == jid: + child.updateAvatar(value) + elif isinstance(child, VerticalPanel): + updateVPanel(child) + if type_ == 'avatar': + updateVPanel(self.vpanel) + + def setAcceptedGroup(self, group): + """Add one or more group(s) which can be displayed in this panel. + + Prevent from duplicate values and keep the list sorted. + @param group: string of the group, or list of string + """ + if isinstance(group, basestring): + groups = [group] + else: + groups = list(group) + try: + self._accepted_groups.extend(groups) + except (AttributeError, TypeError): # XXX: should be AttributeError, but pyjamas bugs here + self._accepted_groups = groups + self._accepted_groups.sort() + + def isJidAccepted(self, jid_s): + """Tell if a jid is actepted and shown in this panel + @param jid_s: jid + @return: True if the jid is accepted""" + if self.accept_all(): + return True + for group in self._accepted_groups: + if self.host.contact_panel.isContactInGroup(group, jid_s): + return True + return False diff -r bade589dbd5a -r a5019e62c3e9 src/browser/sat_browser/chat.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/browser/sat_browser/chat.py Sat Jan 24 01:45:39 2015 +0100 @@ -0,0 +1,364 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Libervia: a Salut à Toi frontend +# Copyright (C) 2011, 2012, 2013, 2014 Jérôme Poisson + +# 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 . + +from sat.core.log import getLogger +log = getLogger(__name__) + +from sat_frontends.tools.games import SYMBOLS +from sat_frontends.tools import strings +from sat_frontends.tools import jid +from sat_frontends.quick_frontend import quick_widgets +from sat_frontends.quick_frontend.quick_chat import QuickChat +from sat.core.i18n import _ + +from pyjamas.ui.AbsolutePanel import AbsolutePanel +from pyjamas.ui.VerticalPanel import VerticalPanel +from pyjamas.ui.HorizontalPanel import HorizontalPanel +from pyjamas.ui.Label import Label +from pyjamas.ui.HTML import HTML +from pyjamas.ui.KeyboardListener import KEY_ENTER, KeyboardHandler +from pyjamas.ui.HTMLPanel import HTMLPanel + +from datetime import datetime +from time import time + +import html_tools +import base_panels +import panels +import card_game +import radiocol +import base_widget +import contact_list +from constants import Const as C +import plugin_xep_0085 + + +class ChatText(HTMLPanel): + + def __init__(self, nick, mymess, msg, extra): + try: + timestamp = float(extra['timestamp']) + except KeyError: + timestamp=None + xhtml = extra.get('xhtml') + _date = datetime.fromtimestamp(float(timestamp or time())) + _msg_class = ["chat_text_msg"] + if mymess: + _msg_class.append("chat_text_mymess") + HTMLPanel.__init__(self, "%(timestamp)s %(nick)s %(msg)s" % + {"timestamp": _date.strftime("%H:%M"), + "nick": "[%s]" % html_tools.html_sanitize(nick), + "msg_class": ' '.join(_msg_class), + "msg": strings.addURLToText(html_tools.html_sanitize(msg)) if not xhtml else html_tools.inlineRoot(xhtml)} # FIXME: images and external links must be removed according to preferences + ) + self.setStyleName('chatText') + + +class Chat(QuickChat, base_widget.LiberviaWidget, KeyboardHandler): + + def __init__(self, host, target, type_=C.CHAT_ONE2ONE, profiles=None): + """Panel used for conversation (one 2 one or group chat) + + @param host: SatWebFrontend instance + @param target: entity (jid.JID) with who we have a conversation (contact's jid for one 2 one chat, or MUC room) + @param type: one2one for simple conversation, group for MUC""" + QuickChat.__init__(self, host, target, type_, profiles=profiles) + self.vpanel = VerticalPanel() + self.vpanel.setSize('100%', '100%') + + # FIXME: temporary dirty initialization to display the OTR state + def header_info_cb(cb): + host.plugins['otr'].infoTextCallback(target, cb) + header_info = header_info_cb if (type_ == C.CHAT_ONE2ONE and 'otr' in host.plugins) else None + + base_widget.LiberviaWidget.__init__(self, host, title=target.bare, info=header_info, selectable=True) + self._body = AbsolutePanel() + self._body.setStyleName('chatPanel_body') + chat_area = HorizontalPanel() + chat_area.setStyleName('chatArea') + if type_ == C.CHAT_GROUP: + self.occupants_list = base_panels.OccupantsList() + self.occupants_initialised = False + chat_area.add(self.occupants_list) + self._body.add(chat_area) + self.content = AbsolutePanel() + self.content.setStyleName('chatContent') + self.content_scroll = base_widget.ScrollPanelWrapper(self.content) + chat_area.add(self.content_scroll) + chat_area.setCellWidth(self.content_scroll, '100%') + self.vpanel.add(self._body) + self.vpanel.setCellHeight(self._body, '100%') + self.addStyleName('chatPanel') + self.setWidget(self.vpanel) + self.state_machine = plugin_xep_0085.ChatStateMachine(self.host, str(self.target)) + self._state = None + self.refresh() + if type_ == C.CHAT_ONE2ONE: + self.historyPrint(profile=self.profile) + + @property + def target(self): + # FIXME: for unknow reason, pyjamas doesn't use the method inherited from QuickChat + # FIXME: must remove this when either pyjamas is fixed, or we use an alternative + if self.type == C.CHAT_GROUP: + return self.current_target.bare + return self.current_target + + @property + def profile(self): + # FIXME: for unknow reason, pyjamas doesn't use the method inherited from QuickWidget + # FIXME: must remove this when either pyjamas is fixed, or we use an alternative + assert len(self.profiles) == 1 and not self.PROFILES_MULTIPLE and not self.PROFILES_ALLOW_NONE + return list(self.profiles)[0] + + @classmethod + def registerClass(cls): + base_widget.LiberviaWidget.addDropKey("CONTACT", cls.createPanel) + + @classmethod + def createPanel(cls, host, item, type_=C.CHAT_ONE2ONE): + assert(item) + _contact = item if isinstance(item, jid.JID) else jid.JID(item) + host.contact_panel.setContactMessageWaiting(_contact.bare, False) + _new_panel = Chat(host, _contact, type_) # XXX: pyjamas doesn't seems to support creating with cls directly + _new_panel.historyPrint() + host.setSelected(_new_panel) + _new_panel.refresh() + return _new_panel + + def refresh(self): + """Refresh the display of this widget. If the unibox is disabled, + add a message box at the bottom of the panel""" + # FIXME: must be checked + # self.host.contact_panel.setContactMessageWaiting(self.target.bare, False) + # self.content_scroll.scrollToBottom() + + enable_box = self.host.uni_box is None + if hasattr(self, 'message_box'): + self.message_box.setVisible(enable_box) + elif enable_box: + self.message_box = panels.MessageBox(self.host) + self.message_box.onSelectedChange(self) + self.message_box.addKeyboardListener(self) + self.vpanel.add(self.message_box) + + def onKeyDown(self, sender, keycode, modifiers): + if keycode == KEY_ENTER: + self.host.showWarning(None, None) + else: + self.host.showWarning(*self.getWarningData()) + + def matchEntity(self, item, type_=None): + """ + @param entity: target jid as a string or jid.JID instance. + @return: True if self matches the given entity + """ + if type_ is None: + type_ = self.type + entity = item if isinstance(item, jid.JID) else jid.JID(item) + try: + return self.target.bare == entity.bare and self.type == type_ + except AttributeError as e: + e.include_traceback() + return False + + def addMenus(self, menu_bar): + """Add cached menus to the header. + + @param menu_bar (GenericMenuBar): menu bar of the widget's header + """ + if self.type == C.CHAT_GROUP: + menu_bar.addCachedMenus(C.MENU_ROOM, {'room_jid': self.target.bare}) + elif self.type == C.CHAT_ONE2ONE: + menu_bar.addCachedMenus(C.MENU_SINGLE, {'jid': self.target}) + + def getWarningData(self): + if self.type not in [C.CHAT_ONE2ONE, C.CHAT_GROUP]: + raise Exception("Unmanaged type !") + if self.type == C.CHAT_ONE2ONE: + msg = "This message will be sent to your contact %s" % self.target + elif self.type == C.CHAT_GROUP: + msg = "This message will be sent to all the participants of the multi-user room %s" % self.target + return ("ONE2ONE" if self.type == C.CHAT_ONE2ONE else "GROUP", msg) + + def onTextEntered(self, text): + self.host.sendMessage(str(self.target), + text, + mess_type = C.MESS_TYPE_GROUPCHAT if self.type == C.CHAT_GROUP else C.MESS_TYPE_CHAT, + errback=self.host.sendError, + profile_key=C.PROF_KEY_NONE + ) + self.state_machine._onEvent("active") + + def onQuit(self): + base_widget.LiberviaWidget.onQuit(self) + if self.type == C.CHAT_GROUP: + self.host.bridge.call('mucLeave', None, self.target.bare) + + def setUserNick(self, nick): + """Set the nick of the user, usefull for e.g. change the color of the user""" + self.nick = nick + + def setPresents(self, nicks): + """Set the users presents in this room + @param occupants: list of nicks (string)""" + for nick in nicks: + self.occupants_list.addOccupant(nick) + self.occupants_initialised = True + + # def userJoined(self, nick, data): + # if self.occupants_list.getOccupantBox(nick): + # return # user is already displayed + # self.occupants_list.addOccupant(nick) + # if self.occupants_initialised: + # self.printInfo("=> %s has joined the room" % nick) + + # def userLeft(self, nick, data): + # self.occupants_list.removeOccupant(nick) + # self.printInfo("<= %s has left the room" % nick) + + def changeUserNick(self, old_nick, new_nick): + assert(self.type == C.CHAT_GROUP) + self.occupants_list.removeOccupant(old_nick) + self.occupants_list.addOccupant(new_nick) + self.printInfo(_("%(old_nick)s is now known as %(new_nick)s") % {'old_nick': old_nick, 'new_nick': new_nick}) + + # def historyPrint(self, size=C.HISTORY_LIMIT_DEFAULT): + # """Print the initial history""" + # def getHistoryCB(history): + # # display day change + # day_format = "%A, %d %b %Y" + # previous_day = datetime.now().strftime(day_format) + # for line in history: + # timestamp, from_jid_s, to_jid_s, message, mess_type, extra = line + # message_day = datetime.fromtimestamp(float(timestamp or time())).strftime(day_format) + # if previous_day != message_day: + # self.printInfo("* " + message_day) + # previous_day = message_day + # self.printMessage(jid.JID(from_jid_s), message, extra, timestamp) + # self.host.bridge.call('getHistory', getHistoryCB, self.host.whoami.bare, self.target.bare, size, True) + + def printInfo(self, msg, type_='normal', extra=None, link_cb=None): + """Print general info + @param msg: message to print + @param type_: one of: + "normal": general info like "toto has joined the room" (will be sanitized) + "link": general info that is clickable like "click here to join the main room" (no sanitize done) + "me": "/me" information like "/me clenches his fist" ==> "toto clenches his fist" (will stay on one line) + @param extra (dict): message data + @param link_cb: method to call when the info is clicked, ignored if type_ is not 'link' + """ + if extra is None: + extra = {} + if type_ == 'normal': + _wid = HTML(strings.addURLToText(html_tools.XHTML2Text(msg))) + _wid.setStyleName('chatTextInfo') + elif type_ == 'link': + _wid = HTML(msg) + _wid.setStyleName('chatTextInfo-link') + if link_cb: + _wid.addClickListener(link_cb) + elif type_ == 'me': + _wid = Label(msg) + _wid.setStyleName('chatTextMe') + else: + raise ValueError("Unknown printInfo type %s" % type_) + self.content.add(_wid) + self.content_scroll.scrollToBottom() + + def printMessage(self, from_jid, msg, extra=None, profile=C.PROF_KEY_NONE): + if extra is None: + extra = {} + try: + nick, mymess = QuickChat.printMessage(self, from_jid, msg, extra, profile) + except TypeError: + # None is returned, the message is managed + return + self.content.add(ChatText(nick, mymess, msg, extra)) + self.content_scroll.scrollToBottom() + + def startGame(self, game_type, waiting, referee, players, *args): + """Configure the chat window to start a game""" + classes = {"Tarot": card_game.CardPanel, "RadioCol": radiocol.RadioColPanel} + if game_type not in classes.keys(): + return # unknown game + attr = game_type.lower() + self.occupants_list.updateSpecials(players, SYMBOLS[attr]) + if waiting or not self.nick in players: + return # waiting for player or not playing + attr = "%s_panel" % attr + if hasattr(self, attr): + return + log.info("%s Game Started \o/" % game_type) + panel = classes[game_type](self, referee, self.nick, players, *args) + setattr(self, attr, panel) + self.vpanel.insert(panel, 0) + self.vpanel.setCellHeight(panel, panel.getHeight()) + + def getGame(self, game_type): + """Return class managing the game type""" + # TODO: check that the game is launched, and manage errors + if game_type == "Tarot": + return self.tarot_panel + elif game_type == "RadioCol": + return self.radiocol_panel + + def setState(self, state, nick=None): + """Set the chat state (XEP-0085) of the contact. Leave nick to None + to set the state for a one2one conversation, or give a nickname or + C.ALL_OCCUPANTS to set the state of a participant within a MUC. + @param state: the new chat state + @param nick: ignored for one2one, otherwise the MUC user nick or C.ALL_OCCUPANTS + """ + if self.type == C.CHAT_GROUP: + assert(nick) + if nick == C.ALL_OCCUPANTS: + occupants = self.occupants_list.occupants_list.keys() + else: + occupants = [nick] if nick in self.occupants_list.occupants_list else [] + for occupant in occupants: + self.occupants_list.occupants_list[occupant].setState(state) + else: + self._state = state + self.refreshTitle() + self.state_machine.started = not not state # start to send "composing" state from now + + def refreshTitle(self): + """Refresh the title of this ChatPanel dialog""" + if self._state: + self.setTitle(self.target.bare + " (" + self._state + ")") + else: + self.setTitle(self.target.bare) + + def setConnected(self, jid_s, resource, availability, priority, statuses): + """Set connection status + @param jid_s (str): JID userhost as unicode + """ + assert(jid_s == self.target.bare) + if self.type != C.CHAT_GROUP: + return + box = self.occupants_list.getOccupantBox(resource) + if box: + contact_list.setPresenceStyle(box, availability) + + def updateChatState(self, from_jid, state): + #TODO + pass + +quick_widgets.register(QuickChat, Chat) diff -r bade589dbd5a -r a5019e62c3e9 src/browser/sat_browser/constants.py --- a/src/browser/sat_browser/constants.py Thu Oct 23 16:56:36 2014 +0200 +++ b/src/browser/sat_browser/constants.py Sat Jan 24 01:45:39 2015 +0100 @@ -37,3 +37,4 @@ # Empty avatar EMPTY_AVATAR = "/media/misc/empty_avatar" + WEB_PANEL_DEFAULT_URL = "http://salut-a-toi.org" diff -r bade589dbd5a -r a5019e62c3e9 src/browser/sat_browser/contact.py --- a/src/browser/sat_browser/contact.py Thu Oct 23 16:56:36 2014 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,538 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Libervia: a Salut à Toi frontend -# Copyright (C) 2011, 2012, 2013, 2014 Jérôme Poisson - -# 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 . - -import pyjd # this is dummy in pyjs -from sat.core.log import getLogger -log = getLogger(__name__) -from pyjamas.ui.SimplePanel import SimplePanel -from pyjamas.ui.ScrollPanel import ScrollPanel -from pyjamas.ui.VerticalPanel import VerticalPanel -from pyjamas.ui.HorizontalPanel import HorizontalPanel -from pyjamas.ui.ClickListener import ClickHandler -from pyjamas.ui.Label import Label -from pyjamas.ui.HTML import HTML -from pyjamas.ui.Image import Image -from pyjamas import Window -from pyjamas import DOM -from __pyjamas__ import doc - -from constants import Const as C -from jid import JID -import base_widget -import panels -import html_tools - - -def buildPresenceStyle(presence, base_style=None): - """Return the CSS classname to be used for displaying the given presence information. - @param presence (str): presence is a value in ('', 'chat', 'away', 'dnd', 'xa') - @param base_style (str): base classname - @return: str - """ - if not base_style: - base_style = "contactLabel" - return '%s-%s' % (base_style, presence or 'connected') - - -def setPresenceStyle(widget, presence, base_style=None): - """ - Set the CSS style of a contact's element according to its presence. - - @param widget (Widget): the UI element of the contact - @param presence (str): a value in ("", "chat", "away", "dnd", "xa"). - @param base_style (str): the base name of the style to apply - """ - if not hasattr(widget, 'presence_style'): - widget.presence_style = None - style = buildPresenceStyle(presence, base_style) - if style == widget.presence_style: - return - if widget.presence_style is not None: - widget.removeStyleName(widget.presence_style) - widget.addStyleName(style) - widget.presence_style = style - - -class GroupLabel(base_widget.DragLabel, Label, ClickHandler): - def __init__(self, host, group): - self.group = group - self.host = host - Label.__init__(self, group) # , Element=DOM.createElement('div') - self.setStyleName('group') - base_widget.DragLabel.__init__(self, group, "GROUP") - ClickHandler.__init__(self) - self.addClickListener(self) - - def onClick(self, sender): - self.host.getOrCreateLiberviaWidget(panels.MicroblogPanel, {'item': self.group}) - - -class ContactLabel(HTML): - def __init__(self, jid, name=None): - HTML.__init__(self) - self.name = name or jid - self.waiting = False - self.refresh() - self.setStyleName('contactLabel') - - def refresh(self): - if self.waiting: - wait_html = "(*) " - self.setHTML("%(wait)s%(name)s" % {'wait': wait_html, - 'name': html_tools.html_sanitize(self.name)}) - - def setMessageWaiting(self, waiting): - """Show a visual indicator if message are waiting - - @param waiting: True if message are waiting""" - self.waiting = waiting - self.refresh() - - -class ContactMenuBar(base_widget.WidgetMenuBar): - - def onBrowserEvent(self, event): - base_widget.WidgetMenuBar.onBrowserEvent(self, event) - event.stopPropagation() # prevent opening the chat dialog - - @classmethod - def getCategoryHTML(cls, menu_name_i18n, type_): - return '' % C.DEFAULT_AVATAR - - def setUrl(self, url): - """Set the URL of the contact avatar.""" - self.items[0].setHTML('' % url) - - -class ContactBox(VerticalPanel, ClickHandler, base_widget.DragLabel): - - def __init__(self, host, jid, name=None, click_listener=None, handle_menu=None): - VerticalPanel.__init__(self, StyleName='contactBox', VerticalAlignment='middle') - base_widget.DragLabel.__init__(self, jid, "CONTACT") - self.host = host - self.jid = jid - self.label = ContactLabel(jid, name) - self.avatar = ContactMenuBar(self, host) if handle_menu else Image() - self.updateAvatar(host.getAvatar(jid)) - self.add(self.avatar) - self.add(self.label) - if click_listener: - ClickHandler.__init__(self) - self.addClickListener(self) - self.click_listener = click_listener - - def addMenus(self, menu_bar): - menu_bar.addCachedMenus(C.MENU_ROSTER_JID_CONTEXT, {'jid': self.jid}) - menu_bar.addCachedMenus(C.MENU_JID_CONTEXT, {'jid': self.jid}) - - def setMessageWaiting(self, waiting): - """Show a visual indicator if message are waiting - - @param waiting: True if message are waiting""" - self.label.setMessageWaiting(waiting) - - def updateAvatar(self, url): - """Update the avatar. - - @param url (str): image url - """ - self.avatar.setUrl(url) - - def onClick(self, sender): - self.click_listener(self.jid) - - -class GroupList(VerticalPanel): - - def __init__(self, parent): - VerticalPanel.__init__(self) - self.setStyleName('groupList') - self._parent = parent - - def add(self, group): - _item = GroupLabel(self._parent.host, group) - _item.addMouseListener(self._parent) - DOM.setStyleAttribute(_item.getElement(), "cursor", "pointer") - index = 0 - for group_ in [child.group for child in self.getChildren()]: - if group_ > group: - break - index += 1 - VerticalPanel.insert(self, _item, index) - - def remove(self, group): - for wid in self: - if isinstance(wid, GroupLabel) and wid.group == group: - VerticalPanel.remove(self, wid) - return - - def getGroupBox(self, group): - """get the widget of a group - - @param group (str): the group - @return: GroupLabel instance if present, else None""" - for wid in self: - if isinstance(wid, GroupLabel) and wid.group == group: - return wid - return None - - -class GenericContactList(VerticalPanel): - """Class that can be used to represent a contact list, but not necessarily - the one that is displayed on the left side. Special features like popup menu - panel or changing the contact states must be done in a sub-class.""" - - def __init__(self, host, handle_click=False, handle_menu=False): - VerticalPanel.__init__(self) - self.host = host - self.contacts = [] - self.click_listener = None - self.handle_menu = handle_menu - - if handle_click: - def cb(contact_jid): - self.host.getOrCreateLiberviaWidget(panels.ChatPanel, {'item': contact_jid}) - self.click_listener = cb - - def add(self, jid_s, name=None): - """Add a contact to the list. - - @param jid (str): JID of the contact - @param name (str): optional name of the contact - """ - assert(isinstance(jid_s, str)) - if jid_s in self.contacts: - return - index = 0 - for contact_ in self.contacts: - if contact_ > jid_s: - break - index += 1 - self.contacts.insert(index, jid_s) - box = ContactBox(self.host, jid_s, name, self.click_listener, self.handle_menu) - VerticalPanel.insert(self, box, index) - - def remove(self, jid_s): - box = self.getContactBox(jid_s) - if not box: - return - VerticalPanel.remove(self, box) - self.contacts.remove(jid_s) - - def isContactPresent(self, contact_jid): - """Return True if a contact is present in the panel""" - return contact_jid in self.contacts - - def getContacts(self): - return self.contacts - - def getContactBox(self, contact_jid_s): - """get the widget of a contact - - @param contact_jid_s (str): the contact - @return: ContactBox instance if present, else None""" - for wid in self: - if isinstance(wid, ContactBox) and wid.jid == contact_jid_s: - return wid - return None - - def updateAvatar(self, jid_s, url): - """Update the avatar of the given contact - - @param jid_s (str): contact jid - @param url (str): image url - """ - try: - self.getContactBox(jid_s).updateAvatar(url) - except TypeError: - pass - - -class ContactList(GenericContactList): - """The contact list that is displayed on the left side.""" - - def __init__(self, host): - GenericContactList.__init__(self, host, handle_click=True, handle_menu=True) - - def setState(self, jid, type_, state): - """Change the appearance of the contact, according to the state - @param jid: jid which need to change state - @param type_: one of availability, messageWaiting - @param state: - - for messageWaiting type: - True if message are waiting - - for availability type: - 'unavailable' if not connected, else presence like RFC6121 #4.7.2.1""" - contact_box = self.getContactBox(jid) - if contact_box: - if type_ == 'availability': - setPresenceStyle(contact_box.label, state) - elif type_ == 'messageWaiting': - contact_box.setMessageWaiting(state) - - -class ContactTitleLabel(base_widget.DragLabel, Label, ClickHandler): - def __init__(self, host, text): - Label.__init__(self, text) # , Element=DOM.createElement('div') - self.host = host - self.setStyleName('contactTitle') - base_widget.DragLabel.__init__(self, text, "CONTACT_TITLE") - ClickHandler.__init__(self) - self.addClickListener(self) - - def onClick(self, sender): - self.host.getOrCreateLiberviaWidget(panels.MicroblogPanel, {'item': None}) - - -class ContactPanel(SimplePanel): - """Manage the contacts and groups""" - - def __init__(self, host): - SimplePanel.__init__(self) - - self.scroll_panel = ScrollPanel() - - self.host = host - self.groups = {} - self.connected = {} # jid connected as key and their status - - self.vPanel = VerticalPanel() - _title = ContactTitleLabel(host, 'Contacts') - DOM.setStyleAttribute(_title.getElement(), "cursor", "pointer") - - self._contact_list = ContactList(host) - self._contact_list.setStyleName('contactList') - self._groupList = GroupList(self) - self._groupList.setStyleName('groupList') - - self.vPanel.add(_title) - self.vPanel.add(self._groupList) - self.vPanel.add(self._contact_list) - self.scroll_panel.add(self.vPanel) - self.add(self.scroll_panel) - self.setStyleName('contactPanel') - Window.addWindowResizeListener(self) - - def onWindowResized(self, width, height): - contact_panel_elt = self.getElement() - classname = 'widgetsPanel' if isinstance(self.getParent().getParent(), panels.UniBoxPanel) else'gwt-TabBar' - _elts = doc().getElementsByClassName(classname) - if not _elts.length: - log.error("no element of class %s found, it should exist !" % classname) - tab_bar_h = height - else: - tab_bar_h = DOM.getAbsoluteTop(_elts.item(0)) or height # getAbsoluteTop can be 0 if tabBar is hidden - - ideal_height = tab_bar_h - DOM.getAbsoluteTop(contact_panel_elt) - 5 - self.scroll_panel.setHeight("%s%s" % (ideal_height, "px")) - - def updateContact(self, jid_s, attributes, groups): - """Add a contact to the panel if it doesn't exist, update it else - @param jid_s: jid userhost as unicode - @param attributes: cf SàT Bridge API's newContact - @param groups: list of groups""" - _current_groups = self.getContactGroups(jid_s) - _new_groups = set(groups) - _key = "@%s: " - - for group in _current_groups.difference(_new_groups): - # We remove the contact from the groups where he isn't anymore - self.groups[group].remove(jid_s) - if not self.groups[group]: - # The group is now empty, we must remove it - del self.groups[group] - self._groupList.remove(group) - if self.host.uni_box: - self.host.uni_box.removeKey(_key % group) - - for group in _new_groups.difference(_current_groups): - # We add the contact to the groups he joined - if group not in self.groups.keys(): - self.groups[group] = set() - self._groupList.add(group) - if self.host.uni_box: - self.host.uni_box.addKey(_key % group) - self.groups[group].add(jid_s) - - # We add the contact to contact list, it will check if contact already exists - self._contact_list.add(jid_s) - self.updateVisibility([jid_s], self.getContactGroups(jid_s)) - - def removeContact(self, jid): - """Remove contacts from groups where he is and contact list""" - self.updateContact(jid, {}, []) # we remove contact from every group - self._contact_list.remove(jid) - - def setConnected(self, jid_s, resource, availability, priority, statuses): - """Set connection status - @param jid_s (str): JID userhost as unicode - """ - if availability == 'unavailable': - if jid_s in self.connected: - if resource in self.connected[jid_s]: - del self.connected[jid_s][resource] - if not self.connected[jid_s]: - del self.connected[jid_s] - else: - if jid_s not in self.connected: - self.connected[jid_s] = {} - self.connected[jid_s][resource] = (availability, priority, statuses) - - # check if the contact is connected with another resource, use the one with highest priority - if jid_s in self.connected: - max_resource = max_priority = None - for tmp_resource in self.connected[jid_s]: - if max_priority is None or self.connected[jid_s][tmp_resource][1] >= max_priority: - max_resource = tmp_resource - max_priority = self.connected[jid_s][tmp_resource][1] - if availability == "unavailable": # do not check the priority here, because 'unavailable' has a dummy one - priority = max_priority - availability = self.connected[jid_s][max_resource][0] - if jid_s not in self.connected or priority >= max_priority: - # case 1: jid not in self.connected means all resources are disconnected, update with 'unavailable' - # case 2: update (or confirm) with the values of the resource which takes precedence - self._contact_list.setState(jid_s, "availability", availability) - - # update the connected contacts chooser live - if hasattr(self.host, "room_contacts_chooser") and self.host.room_contacts_chooser is not None: - self.host.room_contacts_chooser.resetContacts() - - self.updateVisibility([jid_s], self.getContactGroups(jid_s)) - - def setContactMessageWaiting(self, jid, waiting): - """Show an visual indicator that contact has send a message - @param jid: jid of the contact - @param waiting: True if message are waiting""" - self._contact_list.setState(jid, "messageWaiting", waiting) - - def getConnected(self, filter_muc=False): - """return a list of all jid (bare jid) connected - @param filter_muc: if True, remove the groups from the list - """ - contacts = self.connected.keys() - contacts.sort() - return contacts if not filter_muc else list(set(contacts).intersection(set(self.getContacts()))) - - def getContactGroups(self, contact_jid_s): - """Get groups where contact is - @param group: string of single group, or list of string - @param contact_jid_s: jid to test, as unicode - """ - result = set() - for group in self.groups: - if self.isContactInGroup(group, contact_jid_s): - result.add(group) - return result - - def isContactInGroup(self, group, contact_jid): - """Test if the contact_jid is in the group - @param group: string of single group, or list of string - @param contact_jid: jid to test - @return: True if contact_jid is in on of the groups""" - if group in self.groups and contact_jid in self.groups[group]: - return True - return False - - def isContactInRoster(self, contact_jid): - """Test if the contact is in our roster list""" - for contact_box in self._contact_list: - if contact_jid == contact_box.jid: - return True - return False - - def getContacts(self): - return self._contact_list.getContacts() - - def getGroups(self): - return self.groups.keys() - - def onMouseMove(self, sender, x, y): - pass - - def onMouseDown(self, sender, x, y): - pass - - def onMouseUp(self, sender, x, y): - pass - - def onMouseEnter(self, sender): - if isinstance(sender, GroupLabel): - for contact in self._contact_list: - if contact.jid in self.groups[sender.group]: - contact.label.addStyleName("selected") - - def onMouseLeave(self, sender): - if isinstance(sender, GroupLabel): - for contact in self._contact_list: - if contact.jid in self.groups[sender.group]: - contact.label.removeStyleName("selected") - - def updateAvatar(self, jid_s, url): - """Update the avatar of the given contact - - @param jid_s (str): contact jid - @param url (str): image url - """ - self._contact_list.updateAvatar(jid_s, url) - - def hasVisibleMembers(self, group): - """Tell if the given group actually has visible members - - @param group (str): the group to check - @return: boolean - """ - for jid in self.groups[group]: - if self._contact_list.getContactBox(jid).isVisible(): - return True - return False - - def offlineContactsToShow(self): - """Tell if offline contacts should be visible according to the user settings - - @return: boolean - """ - return self.host.getCachedParam('General', C.SHOW_OFFLINE_CONTACTS) == 'true' - - def emtyGroupsToShow(self): - """Tell if empty groups should be visible according to the user settings - - @return: boolean - """ - return self.host.getCachedParam('General', C.SHOW_EMPTY_GROUPS) == 'true' - - def updateVisibility(self, jids, groups): - """Set the widgets visibility for the given contacts and groups - - @param jids (list[str]): list of JID - @param groups (list[str]): list of groups - """ - for jid_s in jids: - try: - self._contact_list.getContactBox(jid_s).setVisible(jid_s in self.connected or self.offlineContactsToShow()) - except TypeError: - log.warning('No box for contact %s: this code line should not be reached' % jid_s) - for group in groups: - try: - self._groupList.getGroupBox(group).setVisible(self.hasVisibleMembers(group) or self.emtyGroupsToShow()) - except TypeError: - log.warning('No box for group %s: this code line should not be reached' % group) - - def refresh(self): - """Show or hide disconnected contacts and empty groups""" - self.updateVisibility(self._contact_list.contacts, self.groups.keys()) diff -r bade589dbd5a -r a5019e62c3e9 src/browser/sat_browser/contact_group.py --- a/src/browser/sat_browser/contact_group.py Thu Oct 23 16:56:36 2014 +0200 +++ b/src/browser/sat_browser/contact_group.py Sat Jan 24 01:45:39 2015 +0100 @@ -28,14 +28,14 @@ import dialog import list_manager -import contact +import contact_list class ContactGroupManager(list_manager.ListManager): """A manager for sub-panels to assign contacts to each group.""" - def __init__(self, parent, keys_dict, contact_list, offsets, style): - list_manager.ListManager.__init__(self, parent, keys_dict, contact_list, offsets, style) + def __init__(self, parent, keys_dict, contacts, offsets, style): + list_manager.ListManager.__init__(self, parent, keys_dict, contacts, offsets, style) self.registerPopupMenuPanel(entries={"Remove group": {}}, callback=lambda sender, key: Timer(5, lambda timer: self.removeContactKey(sender, key))) @@ -153,7 +153,7 @@ """Add the contact list to the DockPanel""" self.toggle = Button("", self.toggleContacts) self.toggle.addStyleName("toggleAssignedContacts") - self.contacts = contact.GenericContactList(self.host) + self.contacts = contact_list.BaseContactPanel(self.host) for contact_ in self.all_contacts: self.contacts.add(contact_) contact_panel = VerticalPanel() diff -r bade589dbd5a -r a5019e62c3e9 src/browser/sat_browser/contact_list.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/browser/sat_browser/contact_list.py Sat Jan 24 01:45:39 2015 +0100 @@ -0,0 +1,590 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Libervia: a Salut à Toi frontend +# Copyright (C) 2011, 2012, 2013, 2014 Jérôme Poisson + +# 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 . + +import pyjd # this is dummy in pyjs +from sat.core.log import getLogger +log = getLogger(__name__) +from sat_frontends.quick_frontend.quick_contact_list import QuickContactList +from pyjamas.ui.SimplePanel import SimplePanel +from pyjamas.ui.ScrollPanel import ScrollPanel +from pyjamas.ui.VerticalPanel import VerticalPanel +from pyjamas.ui.ClickListener import ClickHandler +from pyjamas.ui.Label import Label +from pyjamas.ui.HTML import HTML +from pyjamas.ui.Image import Image +from pyjamas import Window +from pyjamas import DOM +from __pyjamas__ import doc + +from sat_frontends.tools import jid +from constants import Const as C +import base_widget +import panels +import html_tools +import chat + + +def buildPresenceStyle(presence, base_style=None): + """Return the CSS classname to be used for displaying the given presence information. + @param presence (str): presence is a value in ('', 'chat', 'away', 'dnd', 'xa') + @param base_style (str): base classname + @return: str + """ + if not base_style: + base_style = "contactLabel" + return '%s-%s' % (base_style, presence or 'connected') + + +def setPresenceStyle(widget, presence, base_style=None): + """ + Set the CSS style of a contact's element according to its presence. + + @param widget (Widget): the UI element of the contact + @param presence (str): a value in ("", "chat", "away", "dnd", "xa"). + @param base_style (str): the base name of the style to apply + """ + if not hasattr(widget, 'presence_style'): + widget.presence_style = None + style = buildPresenceStyle(presence, base_style) + if style == widget.presence_style: + return + if widget.presence_style is not None: + widget.removeStyleName(widget.presence_style) + widget.addStyleName(style) + widget.presence_style = style + + +class GroupLabel(base_widget.DragLabel, Label, ClickHandler): + def __init__(self, host, group): + self.group = group + self.host = host + Label.__init__(self, group) # , Element=DOM.createElement('div') + self.setStyleName('group') + base_widget.DragLabel.__init__(self, group, "GROUP") + ClickHandler.__init__(self) + self.addClickListener(self) + + def onClick(self, sender): + self.host.getOrCreateLiberviaWidget(panels.MicroblogPanel, {'item': self.group}) + + +class ContactLabel(HTML): + def __init__(self, jid, name=None): + HTML.__init__(self) + self.name = name or str(jid) + self.waiting = False + self.refresh() + self.setStyleName('contactLabel') + + def refresh(self): + if self.waiting: + wait_html = "(*) " + self.setHTML("%(wait)s%(name)s" % {'wait': wait_html, + 'name': html_tools.html_sanitize(self.name)}) + + def setMessageWaiting(self, waiting): + """Show a visual indicator if message are waiting + + @param waiting: True if message are waiting""" + self.waiting = waiting + self.refresh() + + +class ContactMenuBar(base_widget.WidgetMenuBar): + + def onBrowserEvent(self, event): + base_widget.WidgetMenuBar.onBrowserEvent(self, event) + event.stopPropagation() # prevent opening the chat dialog + + @classmethod + def getCategoryHTML(cls, menu_name_i18n, type_): + return '' % C.DEFAULT_AVATAR + + def setUrl(self, url): + """Set the URL of the contact avatar.""" + self.items[0].setHTML('' % url) + + +class ContactBox(VerticalPanel, ClickHandler, base_widget.DragLabel): + + def __init__(self, host, jid_, name=None, click_listener=None, handle_menu=None): + VerticalPanel.__init__(self, StyleName='contactBox', VerticalAlignment='middle') + base_widget.DragLabel.__init__(self, jid_, "CONTACT") + self.host = host + self.jid = jid_ + self.label = ContactLabel(jid_, name) + self.avatar = ContactMenuBar(self, host) if handle_menu else Image() + # self.updateAvatar(host.getAvatar(jid_)) # FIXME + self.add(self.avatar) + self.add(self.label) + if click_listener: + ClickHandler.__init__(self) + self.addClickListener(self) + self.click_listener = click_listener + + def addMenus(self, menu_bar): + menu_bar.addCachedMenus(C.MENU_ROSTER_JID_CONTEXT, {'jid': self.jid}) + menu_bar.addCachedMenus(C.MENU_JID_CONTEXT, {'jid': self.jid}) + + def setMessageWaiting(self, waiting): + """Show a visual indicator if message are waiting + + @param waiting: True if message are waiting""" + self.label.setMessageWaiting(waiting) + + def updateAvatar(self, url): + """Update the avatar. + + @param url (str): image url + """ + self.avatar.setUrl(url) + + def onClick(self, sender): + self.click_listener(self.jid) + + +class GroupPanel(VerticalPanel): + + def __init__(self, parent): + VerticalPanel.__init__(self) + self.setStyleName('groupPanel') + self._parent = parent + self._groups = set() + + def add(self, group): + if group in self._groups: + log.warning("trying to add an already existing group") + return + _item = GroupLabel(self._parent.host, group) + _item.addMouseListener(self._parent) + DOM.setStyleAttribute(_item.getElement(), "cursor", "pointer") + index = 0 + for group_ in [child.group for child in self.getChildren()]: + if group_ > group: + break + index += 1 + VerticalPanel.insert(self, _item, index) + self._groups.add(group) + + def remove(self, group): + for wid in self: + if isinstance(wid, GroupLabel) and wid.group == group: + VerticalPanel.remove(self, wid) + self._groups.remove(group) + return + log.warning("Trying to remove a non existent group") + + def getGroupBox(self, group): + """get the widget of a group + + @param group (str): the group + @return: GroupLabel instance if present, else None""" + for wid in self: + if isinstance(wid, GroupLabel) and wid.group == group: + return wid + return None + + def getGroups(self): + return self._groups + + +class BaseContactsPanel(VerticalPanel): + """Class that can be used to represent a contact list, but not necessarily + the one that is displayed on the left side. Special features like popup menu + panel or changing the contact states must be done in a sub-class.""" + + def __init__(self, host, handle_click=False, handle_menu=False): + VerticalPanel.__init__(self) + self.host = host + self.contacts = [] + self.click_listener = None + self.handle_menu = handle_menu + + if handle_click: + def cb(contact_jid): + host.widgets.getOrCreateWidget(chat.Chat, contact_jid, type_=C.CHAT_ONE2ONE, profile=C.PROF_KEY_NONE) + self.click_listener = cb + + def add(self, jid_, name=None): + """Add a contact to the list. + + @param jid_ (jid.JID): jid_ of the contact + @param name (str): optional name of the contact + """ + assert isinstance(jid_, jid.JID) + if jid_ in self.contacts: + return + index = 0 + for contact_ in self.contacts: + if contact_ > jid_: + break + index += 1 + self.contacts.insert(index, jid_) + box = ContactBox(self.host, jid_, name, self.click_listener, self.handle_menu) + VerticalPanel.insert(self, box, index) + + def remove(self, jid_): + box = self.getContactBox(jid_) + if not box: + return + VerticalPanel.remove(self, box) + self.contacts.remove(jid_) + + def isContactPresent(self, contact_jid): + """Return True if a contact is present in the panel""" + return contact_jid in self.contacts + + def getContacts(self): + return self.contacts + + def getContactBox(self, contact_jid): + """get the widget of a contact + + @param contact_jid (jid.JID): the contact + @return: ContactBox instance if present, else None""" + for wid in self: + if isinstance(wid, ContactBox) and wid.jid == contact_jid: + return wid + return None + + def updateAvatar(self, jid_, url): + """Update the avatar of the given contact + + @param jid_ (jid.JID): contact jid + @param url (str): image url + """ + try: + self.getContactBox(jid_).updateAvatar(url) + except TypeError: + pass + + +class ContactsPanel(BaseContactsPanel): + """The contact list that is displayed on the left side.""" + + def __init__(self, host): + BaseContactsPanel.__init__(self, host, handle_click=True, handle_menu=True) + + def setState(self, jid_, type_, state): + """Change the appearance of the contact, according to the state + @param jid_ (jid.JID): jid.JID which need to change state + @param type_ (str): one of "availability", "messageWaiting" + @param state: + - for messageWaiting type: + True if message are waiting + - for availability type: + C.PRESENCE_UNAVAILABLE or None if not connected, else presence like RFC6121 #4.7.2.1""" + assert type_ in ('availability', 'messageWaiting') + contact_box = self.getContactBox(jid_) + if not contact_box: + log.warning("No contact box found for {}".format(jid_)) + else: + if type_ == 'availability': + if state is None: + state = C.PRESENCE_UNAVAILABLE + setPresenceStyle(contact_box.label, state) + elif type_ == 'messageWaiting': + contact_box.setMessageWaiting(state) + + +class ContactTitleLabel(base_widget.DragLabel, Label, ClickHandler): + def __init__(self, host, text): + Label.__init__(self, text) # , Element=DOM.createElement('div') + self.host = host + self.setStyleName('contactTitle') + base_widget.DragLabel.__init__(self, text, "CONTACT_TITLE") + ClickHandler.__init__(self) + self.addClickListener(self) + + def onClick(self, sender): + self.host.getOrCreateLiberviaWidget(panels.MicroblogPanel, {'item': None}) + + +class ContactList(SimplePanel, QuickContactList): + """Manage the contacts and groups""" + + def __init__(self, host): + QuickContactList.__init__(self, host, C.PROF_KEY_NONE) + SimplePanel.__init__(self) + self.scroll_panel = ScrollPanel() + self.vPanel = VerticalPanel() + _title = ContactTitleLabel(host, 'Contacts') + DOM.setStyleAttribute(_title.getElement(), "cursor", "pointer") + self._contacts_panel = ContactsPanel(host) + self._contacts_panel.setStyleName('contactPanel') # FIXME: style doesn't exists ! + self._group_panel = GroupPanel(self) + + self.vPanel.add(_title) + self.vPanel.add(self._group_panel) + self.vPanel.add(self._contacts_panel) + self.scroll_panel.add(self.vPanel) + self.add(self.scroll_panel) + self.setStyleName('contactList') + Window.addWindowResizeListener(self) + + @property + def profile(self): + return C.PROF_KEY_NONE + + def update(self): + ### GROUPS ### + _keys = self._groups.keys() + try: + # XXX: Pyjamas doesn't do the set casting if None is present + _keys.remove(None) + except KeyError: + pass + current_groups = set(_keys) + shown_groups = self._group_panel.getGroups() + new_groups = current_groups.difference(shown_groups) + removed_groups = shown_groups.difference(current_groups) + for group in new_groups: + self._group_panel.add(group) + for group in removed_groups: + self._group_panel.remove(group) + + ### JIDS ### + current_contacts = set(self._cache.keys()) + shown_contacts = set(self._contacts_panel.getContacts()) + new_contacts = current_contacts.difference(shown_contacts) + removed_contacts = shown_contacts.difference(current_contacts) + + for contact in new_contacts: + self._contacts_panel.add(contact) + for contact in removed_contacts: + self._contacts_panel.remove(contact) + + def onWindowResized(self, width, height): + contact_panel_elt = self.getElement() + # FIXME: still needed ? + # classname = 'widgetsPanel' if isinstance(self.getParent().getParent(), panels.UniBoxPanel) else 'gwt-TabBar' + classname = 'gwt-TabBar' + _elts = doc().getElementsByClassName(classname) + if not _elts.length: + log.error("no element of class %s found, it should exist !" % classname) + tab_bar_h = height + else: + tab_bar_h = DOM.getAbsoluteTop(_elts.item(0)) or height # getAbsoluteTop can be 0 if tabBar is hidden + + ideal_height = tab_bar_h - DOM.getAbsoluteTop(contact_panel_elt) - 5 + self.scroll_panel.setHeight("%s%s" % (ideal_height, "px")) + + # def updateContact(self, jid_s, attributes, groups): + # """Add a contact to the panel if it doesn't exist, update it else + + # @param jid_s: jid userhost as unicode + # @param attributes: cf SàT Bridge API's newContact + # @param groups: list of groups""" + # _current_groups = self.getContactGroups(jid_s) + # _new_groups = set(groups) + # _key = "@%s: " + + # for group in _current_groups.difference(_new_groups): + # # We remove the contact from the groups where he isn't anymore + # self.groups[group].remove(jid_s) + # if not self.groups[group]: + # # The group is now empty, we must remove it + # del self.groups[group] + # self._group_panel.remove(group) + # if self.host.uni_box: + # self.host.uni_box.removeKey(_key % group) + + # for group in _new_groups.difference(_current_groups): + # # We add the contact to the groups he joined + # if group not in self.groups.keys(): + # self.groups[group] = set() + # self._group_panel.add(group) + # if self.host.uni_box: + # self.host.uni_box.addKey(_key % group) + # self.groups[group].add(jid_s) + + # # We add the contact to contact list, it will check if contact already exists + # self._contacts_panel.add(jid_s) + # self.updateVisibility([jid_s], self.getContactGroups(jid_s)) + + # def removeContact(self, jid): + # """Remove contacts from groups where he is and contact list""" + # self.updateContact(jid, {}, []) # we remove contact from every group + # self._contacts_panel.remove(jid) + + # def setConnected(self, jid_s, resource, availability, priority, statuses): + # """Set connection status + # @param jid_s (str): JID userhost as unicode + # """ + # if availability == 'unavailable': + # if jid_s in self.connected: + # if resource in self.connected[jid_s]: + # del self.connected[jid_s][resource] + # if not self.connected[jid_s]: + # del self.connected[jid_s] + # else: + # if jid_s not in self.connected: + # self.connected[jid_s] = {} + # self.connected[jid_s][resource] = (availability, priority, statuses) + + # # check if the contact is connected with another resource, use the one with highest priority + # if jid_s in self.connected: + # max_resource = max_priority = None + # for tmp_resource in self.connected[jid_s]: + # if max_priority is None or self.connected[jid_s][tmp_resource][1] >= max_priority: + # max_resource = tmp_resource + # max_priority = self.connected[jid_s][tmp_resource][1] + # if availability == "unavailable": # do not check the priority here, because 'unavailable' has a dummy one + # priority = max_priority + # availability = self.connected[jid_s][max_resource][0] + # if jid_s not in self.connected or priority >= max_priority: + # # case 1: jid not in self.connected means all resources are disconnected, update with 'unavailable' + # # case 2: update (or confirm) with the values of the resource which takes precedence + # self._contacts_panel.setState(jid_s, "availability", availability) + + # # update the connected contacts chooser live + # if hasattr(self.host, "room_contacts_chooser") and self.host.room_contacts_chooser is not None: + # self.host.room_contacts_chooser.resetContacts() + + # self.updateVisibility([jid_s], self.getContactGroups(jid_s)) + + def setContactMessageWaiting(self, jid, waiting): + """Show an visual indicator that contact has send a message + @param jid: jid of the contact + @param waiting: True if message are waiting""" + self._contacts_panel.setState(jid, "messageWaiting", waiting) + + # def getConnected(self, filter_muc=False): + # """return a list of all jid (bare jid) connected + # @param filter_muc: if True, remove the groups from the list + # """ + # contacts = self.connected.keys() + # contacts.sort() + # return contacts if not filter_muc else list(set(contacts).intersection(set(self.getContacts()))) + + # def getContactGroups(self, contact_jid_s): + # """Get groups where contact is + # @param group: string of single group, or list of string + # @param contact_jid_s: jid to test, as unicode + # """ + # result = set() + # for group in self.groups: + # if self.isContactInGroup(group, contact_jid_s): + # result.add(group) + # return result + + # def isContactInGroup(self, group, contact_jid): + # """Test if the contact_jid is in the group + # @param group: string of single group, or list of string + # @param contact_jid: jid to test + # @return: True if contact_jid is in on of the groups""" + # if group in self.groups and contact_jid in self.groups[group]: + # return True + # return False + + def isContactInRoster(self, contact_jid): + """Test if the contact is in our roster list""" + for contact_box in self._contacts_panel: + if contact_jid == contact_box.jid: + return True + return False + + # def getContacts(self): + # return self._contacts_panel.getContacts() + + def getGroups(self): + return self.groups.keys() + + def onMouseMove(self, sender, x, y): + pass + + def onMouseDown(self, sender, x, y): + pass + + def onMouseUp(self, sender, x, y): + pass + + def onMouseEnter(self, sender): + if isinstance(sender, GroupLabel): + jids = self.getGroupData(sender.group, "jids") + for contact in self._contacts_panel: + if contact.jid in jids: + contact.label.addStyleName("selected") + + def onMouseLeave(self, sender): + if isinstance(sender, GroupLabel): + jids = self.getGroupData(sender.group, "jids") + for contact in self._contacts_panel: + if contact.jid in jids: + contact.label.removeStyleName("selected") + + def updateAvatar(self, jid_s, url): + """Update the avatar of the given contact + + @param jid_s (str): contact jid + @param url (str): image url + """ + self._contacts_panel.updateAvatar(jid_s, url) + + def hasVisibleMembers(self, group): + """Tell if the given group actually has visible members + + @param group (str): the group to check + @return: boolean + """ + for jid_ in self.groups[group]: + if self._contacts_panel.getContactBox(jid_).isVisible(): + return True + return False + + def offlineContactsToShow(self): + """Tell if offline contacts should be visible according to the user settings + + @return: boolean + """ + return self.host.getCachedParam('General', C.SHOW_OFFLINE_CONTACTS) == 'true' + + def emtyGroupsToShow(self): + """Tell if empty groups should be visible according to the user settings + + @return: boolean + """ + return self.host.getCachedParam('General', C.SHOW_EMPTY_GROUPS) == 'true' + + def updatePresence(self, entity, show, priority, statuses): + QuickContactList.updatePresence(self, entity, show, priority, statuses) + entity_bare = entity.bare + show = self.getCache(entity_bare, C.PRESENCE_SHOW) # we use cache to have the show nformation of main resource only + self._contacts_panel.setState(entity_bare, "availability", show) + + # def updateVisibility(self, jids, groups): + # """Set the widgets visibility for the given contacts and groups + + # @param jids (list[str]): list of JID + # @param groups (list[str]): list of groups + # """ + # for jid_s in jids: + # try: + # self._contacts_panel.getContactBox(jid_s).setVisible(jid_s in self.connected or self.offlineContactsToShow()) + # except TypeError: + # log.warning('No box for contact %s: this code line should not be reached' % jid_s) + # for group in groups: + # try: + # self._group_panel.getGroupBox(group).setVisible(self.hasVisibleMembers(group) or self.emtyGroupsToShow()) + # except TypeError: + # log.warning('No box for group %s: this code line should not be reached' % group) + + # def refresh(self): + # """Show or hide disconnected contacts and empty groups""" + # self.updateVisibility(self._contacts_panel.contacts, self.groups.keys()) diff -r bade589dbd5a -r a5019e62c3e9 src/browser/sat_browser/jid.py --- a/src/browser/sat_browser/jid.py Thu Oct 23 16:56:36 2014 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,118 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Libervia: a Salut à Toi frontend -# Copyright (C) 2011, 2012, 2013, 2014 Jérôme Poisson - -# 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 . - -# FIXME: Libervia should use sat_frontends.tools.jid but pyjamas doesn't -# know the unicode type and also experiences issues with __new__. - - -class JID(object): - """This class help manage JID (Node@Domaine/Resource)""" - - def __init__(self, jid): - assert(isinstance(jid, str)) - self.__raw = str(JID.__normalize(jid)) - self.__parse() - - @classmethod - def __normalize(cls, jid): - """Naive normalization before instantiating and parsing the JID""" - if not jid: - return jid - tokens = jid.split('/') - tokens[0] = tokens[0].lower() # force node and domain to lower-case - return '/'.join(tokens) - - @property - def node(self): - return self.__node - - @property - def domain(self): - return self.__domain - - @property - def resource(self): - return self.__resource - - @property - def bare(self): - return self.domain if not self.node else (self.node + "@" + self.domain) - - # XXX: Avoid property setters, Pyjamas doesn't seem to handle them well. It seems - # that it will just naively assign the value without actually calling the method. - # FIXME: find a way to raise an error if the (undefined!) setter is called. - def setNode(self, node): - self.__node = node - self.__build() - - def setDomain(self, domain): - self.__domain = domain - self.__build() - - def setResource(self, resource): - self.__resource = resource - self.__build() - - def setBare(self, bare): - self.__parseBare(bare) - self.__build() - - def __build(self): - """Build the JID string from the node, domain and resource""" - self.__raw = self.bare if not self.resource else (self.bare + '/' + self.resource) - - def __parse(self): - """Parse the JID string to extract the node, domain and resource""" - tokens = self.__raw.split('/') - bare, self.__resource = (tokens[0], tokens[1]) if len(tokens) > 1 else (self.__raw, '') - self.__parseBare(bare) - - def __parseBare(self, bare): - """Parse the given JID bare to extract the node and domain - - @param bare (str): JID bare to parse - """ - tokens = bare.split('@') - self.__node, self.__domain = (tokens[0], tokens[1]) if len(tokens) > 1 else ('', bare) - - def __str__(self): - try: - return self.__raw - except AttributeError: - raise AttributeError("Trying to output a JID which has not been parsed yet") - - def is_valid(self): - """ - @return: True if the JID is XMPP compliant - """ - return self.domain != '' - - def __eq__(self, other): - """Redefine equality operator to implement the naturally expected behavior""" - return self.__raw == other.__raw - - def __hash__(self): - """Redefine hash to implement the naturally expected behavior""" - return hash(self.__raw) - - def full(self): - return str(self) - - def bareJID(self): - return JID(self.bare) diff -r bade589dbd5a -r a5019e62c3e9 src/browser/sat_browser/json.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/browser/sat_browser/json.py Sat Jan 24 01:45:39 2015 +0100 @@ -0,0 +1,268 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Libervia: a Salut à Toi frontend +# Copyright (C) 2011, 2012, 2013, 2014 Jérôme Poisson + +# 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 . + + +### logging configuration ### +from sat.core.log import getLogger +log = getLogger(__name__) +### + +from pyjamas.Timer import Timer +from pyjamas import Window +from pyjamas import JSONService + +from sat_browser.constants import Const as C + + +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 iwith 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("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("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("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: + # 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] + + 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("Error %s: %s" % (errobj['message']['faultCode'], errobj['message']['faultString'])) + else: + log.error("%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", + ["isRegistered", "isConnected", "asyncConnect", "registerParams", "getMenus"]) + + +class BridgeCall(LiberviaJsonProxy): + def __init__(self): + LiberviaJsonProxy.__init__(self, "/json_api", + ["getContacts", "addContact", "sendMessage", "sendMblog", "sendMblogComment", + "getLastMblogs", "getMassiveLastMblogs", "getMblogComments", + "getHistory", "getPresenceStatuses", "joinMUC", "mucLeave", "getRoomsJoined", + "getRoomsSubjects", "inviteMUC", "launchTarotGame", "getTarotCardsPaths", "tarotGameReady", + "tarotGamePlayCards", "launchRadioCollective", "getMblogs", "getMblogsWithComments", + "getWaitingSub", "subscription", "delContact", "updateContact", "getCard", + "getEntityData", "getParamsUI", "asyncGetParamA", "setParam", "launchAction", + "disconnect", "chatStateComposing", "getNewAccountDomain", "confirmationAnswer", + "syntaxConvert", "getAccountDialogUI", "getLastResource", "getWaitingConf", + ]) + 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): # FIXME + log.warning("isConnected is not implemeted in Libervia as for now profile is connected if session is opened") + return True + + def getAvatarFile(self, hash_, callback=None): + log.warning("getAvatarFile only return hash in Libervia") + if callback is not None: + callback(hash_) + return hash_ + + +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"]) + self._signals = {} # key: signal name, value: callback + + def onRemoteResponse(self, response, request_info): + if self.retry_delay != self.RETRY_BASE_DELAY: + log.info("Connection with server restablished") + 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.alert (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(timer): + log.info("Trying to reconnect to server...") + self.getSignals(callback=self.signalHandler) + log.warning("Lost connection, trying to reconnect in {} s".format(self.retry_delay/1000)) + Timer(notify=_timerCb).schedule(self.retry_delay) + self.retry_delay *= 2 + + def register(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("Registering signal {}".format(name)) + if name in self._signals: + log.error("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) + if len(signal_data) == 1: + signal_data.append([]) + log.debug("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("Ignoring {} signal: no handler registered !".format(name)) + return + if with_profile: + args.append(C.PROF_KEY_NONE) + callback(*args) diff -r bade589dbd5a -r a5019e62c3e9 src/browser/sat_browser/menu.py --- a/src/browser/sat_browser/menu.py Thu Oct 23 16:56:36 2014 +0200 +++ b/src/browser/sat_browser/menu.py Sat Jan 24 01:45:39 2015 +0100 @@ -23,13 +23,14 @@ from sat.core.i18n import _ +from sat_frontends.tools import jid + from pyjamas.ui.SimplePanel import SimplePanel from pyjamas.ui.HTML import HTML from pyjamas.ui.Frame import Frame from pyjamas import Window from constants import Const as C -import jid import file_tools import xmlui import panels @@ -97,9 +98,8 @@ # General menu def onWebWidget(self): - web_panel = panels.WebPanel(self.host, "http://www.goffi.org") - self.host.addWidget(web_panel) - self.host.setSelected(web_panel) + web_widget = self.host.widgets.getOrCreateWidget(panels.WebPanel, C.WEB_PANEL_DEFAULT_URL, profile=C.PROF_KEY_NONE) + self.host.setSelected(web_widget) def onDisconnect(self): def confirm_cb(answer): @@ -124,9 +124,9 @@ def onAbout(self): _about = HTML("""Libervia, a Salut à Toi project

-You can contact the author at goffi@goffi.org
+You can contact the authors at contact@salut-a-toi.org
Blog available (mainly in french) at http://www.goffi.org
-Project page: http://sat.goffi.org
+Project page: http://salut-a-toi.org

Any help welcome :)

This project is dedicated to Roger Poisson

diff -r bade589dbd5a -r a5019e62c3e9 src/browser/sat_browser/nativedom.py --- a/src/browser/sat_browser/nativedom.py Thu Oct 23 16:56:36 2014 +0200 +++ b/src/browser/sat_browser/nativedom.py Sat Jan 24 01:45:39 2015 +0100 @@ -34,7 +34,7 @@ ret = [] for i in range(len(js_nodes_list)): #ret.append(Element(js_nodes_list.item(i))) - ret.append(self.__class__(js_nodes_list.item(i))) # XXX: Ugly, but used to word around a Pyjamas's bug + ret.append(self.__class__(js_nodes_list.item(i))) # XXX: Ugly, but used to work around a Pyjamas's bug return ret def __getattr__(self, name): @@ -42,7 +42,7 @@ return getattr(self._node, name) return object.__getattribute__(self, name) - @property + @property # XXX: doesn't work in --strict mode in pyjs def childNodes(self): return self._jsNodesList2List(self._node.childNodes) diff -r bade589dbd5a -r a5019e62c3e9 src/browser/sat_browser/panels.py --- a/src/browser/sat_browser/panels.py Thu Oct 23 16:56:36 2014 +0200 +++ b/src/browser/sat_browser/panels.py Sat Jan 24 01:45:39 2015 +0100 @@ -22,102 +22,82 @@ log = getLogger(__name__) from sat_frontends.tools.strings import addURLToText -from sat_frontends.tools.games import SYMBOLS -from sat.core.i18n import _ -from pyjamas.ui.SimplePanel import SimplePanel from pyjamas.ui.AbsolutePanel import AbsolutePanel from pyjamas.ui.VerticalPanel import VerticalPanel from pyjamas.ui.HorizontalPanel import HorizontalPanel -from pyjamas.ui.HTMLPanel import HTMLPanel -from pyjamas.ui.Frame import Frame from pyjamas.ui.TextArea import TextArea -from pyjamas.ui.Label import Label from pyjamas.ui.Button import Button from pyjamas.ui.HTML import HTML -from pyjamas.ui.Image import Image from pyjamas.ui.ClickListener import ClickHandler -from pyjamas.ui.FlowPanel import FlowPanel -from pyjamas.ui.KeyboardListener import KEY_ENTER, KEY_UP, KEY_DOWN, KeyboardHandler +from pyjamas.ui.KeyboardListener import KEY_ENTER, KEY_UP, KEY_DOWN from pyjamas.ui.MouseListener import MouseHandler -from pyjamas.ui.FocusListener import FocusHandler +from pyjamas.ui.Frame import Frame from pyjamas.Timer import Timer +from pyjamas import Window from pyjamas import DOM -from pyjamas import Window from __pyjamas__ import doc -from datetime import datetime -from time import time -import jid -import html_tools import base_panels import base_menu -import card_game -import radiocol import menu import dialog import base_widget -import richtext -import contact +import contact_list from constants import Const as C -import plugin_xep_0085 - - -# TODO: at some point we should decide which behaviors to keep and remove these two constants -TOGGLE_EDITION_USE_ICON = False # set to True to use an icon inside the "toggle syntax" button -NEW_MESSAGE_USE_BUTTON = False # set to True to display the "New message" button instead of an empty entry +from sat_frontends.quick_frontend import quick_widgets -class UniBoxPanel(HorizontalPanel): - """Panel containing the UniBox""" - - def __init__(self, host): - HorizontalPanel.__init__(self) - self.host = host - self.setStyleName('uniBoxPanel') - self.unibox = None - - def refresh(self): - """Enable or disable this panel. Contained widgets are created when necessary.""" - enable = self.host.getCachedParam(C.COMPOSITION_KEY, C.ENABLE_UNIBOX_PARAM) == 'true' - self.setVisible(enable) - if enable and not self.unibox: - self.button = Button('') - self.button.setTitle('Open the rich text editor') - self.button.addStyleName('uniBoxButton') - self.add(self.button) - self.unibox = UniBox(self.host) - self.add(self.unibox) - self.setCellWidth(self.unibox, '100%') - self.button.addClickListener(self.openRichMessageEditor) - self.unibox.addKey("@@: ") - self.unibox.onSelectedChange(self.host.getSelected()) - - def openRichMessageEditor(self): - """Open the rich text editor.""" - self.button.setVisible(False) - self.unibox.setVisible(False) - self.setCellWidth(self.unibox, '0px') - self.host.panel._contactsMove(self) - - def afterEditCb(): - Window.removeWindowResizeListener(self) - self.host.panel._contactsMove(self.host.panel._hpanel) - self.setCellWidth(self.unibox, '100%') - self.button.setVisible(True) - self.unibox.setVisible(True) - self.host.resize() - - richtext.RichMessageEditor.getOrCreate(self.host, self, afterEditCb) - Window.addWindowResizeListener(self) - self.host.resize() - - def onWindowResized(self, width, height): - right = self.host.panel.menu.getAbsoluteLeft() + self.host.panel.menu.getOffsetWidth() - left = self.host.panel._contacts.getAbsoluteLeft() + self.host.panel._contacts.getOffsetWidth() - ideal_width = right - left - 40 - self.host.richtext.setWidth("%spx" % ideal_width) +# class UniBoxPanel(HorizontalPanel): +# """Panel containing the UniBox""" +# +# def __init__(self, host): +# HorizontalPanel.__init__(self) +# self.host = host +# self.setStyleName('uniBoxPanel') +# self.unibox = None +# +# def refresh(self): +# """Enable or disable this panel. Contained widgets are created when necessary.""" +# enable = self.host.getCachedParam(C.COMPOSITION_KEY, C.ENABLE_UNIBOX_PARAM) == 'true' +# self.setVisible(enable) +# if enable and not self.unibox: +# self.button = Button('') +# self.button.setTitle('Open the rich text editor') +# self.button.addStyleName('uniBoxButton') +# self.add(self.button) +# self.unibox = UniBox(self.host) +# self.add(self.unibox) +# self.setCellWidth(self.unibox, '100%') +# self.button.addClickListener(self.openRichMessageEditor) +# self.unibox.addKey("@@: ") +# self.unibox.onSelectedChange(self.host.getSelected()) +# +# def openRichMessageEditor(self): +# """Open the rich text editor.""" +# self.button.setVisible(False) +# self.unibox.setVisible(False) +# self.setCellWidth(self.unibox, '0px') +# self.host.panel._contactsMove(self) +# +# def afterEditCb(): +# Window.removeWindowResizeListener(self) +# self.host.panel._contactsMove(self.host.panel._hpanel) +# self.setCellWidth(self.unibox, '100%') +# self.button.setVisible(True) +# self.unibox.setVisible(True) +# self.host.resize() +# +# richtext.RichMessageEditor.getOrCreate(self.host, self, afterEditCb) +# Window.addWindowResizeListener(self) +# self.host.resize() +# +# def onWindowResized(self, width, height): +# right = self.host.panel.menu.getAbsoluteLeft() + self.host.panel.menu.getOffsetWidth() +# left = self.host.panel._contacts.getAbsoluteLeft() + self.host.panel._contacts.getOffsetWidth() +# ideal_width = right - left - 40 +# self.host.richtext.setWidth("%spx" % ideal_width) class MessageBox(TextArea): @@ -126,12 +106,11 @@ def __init__(self, host): TextArea.__init__(self) self.host = host - self.__size = (0, 0) + self.size = (0, 0) self.setStyleName('messageBox') self.addKeyboardListener(self) MouseHandler.__init__(self) self.addMouseListener(self) - self._selected_cache = None def onBrowserEvent(self, event): # XXX: woraroung a pyjamas bug: self.currentEvent is not set @@ -149,8 +128,8 @@ if keycode == KEY_ENTER: if _txt: - self._selected_cache.onTextEntered(_txt) - self.host._updateInputHistory(_txt) + self.host.selected_widget.onTextEntered(_txt) + self.host._updateInputHistory(_txt) # FIXME: why using a global variable ? self.setText('') sender.cancelKey() elif keycode == KEY_UP: @@ -158,140 +137,140 @@ elif keycode == KEY_DOWN: self.host._updateInputHistory(_txt, +1, history_cb) else: - self.__onComposing() + self._onComposing() - def __onComposing(self): + def _onComposing(self): """Callback when the user is composing a text.""" - if hasattr(self._selected_cache, "target"): - self._selected_cache.state_machine._onEvent("composing") + self.host.selected_widget.state_machine._onEvent("composing") def onMouseUp(self, sender, x, y): size = (self.getOffsetWidth(), self.getOffsetHeight()) - if size != self.__size: - self.__size = size + if size != self.size: + self.size = size self.host.resize() def onSelectedChange(self, selected): self._selected_cache = selected -class UniBox(MessageBox, MouseHandler): # AutoCompleteTextBox): - """This text box is used as a main typing point, for message, microblog, etc""" - - def __init__(self, host): - MessageBox.__init__(self, host) - #AutoCompleteTextBox.__init__(self) - self.setStyleName('uniBox') - host.addSelectedListener(self.onSelectedChange) - - def addKey(self, key): - return - #self.getCompletionItems().completions.append(key) - - def removeKey(self, key): - return - # TODO: investigate why AutoCompleteTextBox doesn't work here, - # maybe it can work on a TextBox but no TextArea. Remove addKey - # and removeKey methods if they don't serve anymore. - try: - self.getCompletionItems().completions.remove(key) - except KeyError: - log.warning("trying to remove an unknown key") - - def _getTarget(self, txt): - """ Say who will receive the messsage - @return: a tuple (selected, target_type, target info) with: - - target_hook: None if we use the selected widget, (msg, data) if we have a hook (e.g. "@@: " for a public blog), where msg is the parsed message (i.e. without the "hook key: "@@: bla" become ("bla", None)) - - target_type: one of PUBLIC, GROUP, ONE2ONE, STATUS, MISC - - msg: HTML message which will appear in the privacy warning banner """ - target = self._selected_cache - - def getSelectedOrStatus(): - if target and target.isSelectable(): - _type, msg = target.getWarningData() - target_hook = None # we use the selected widget, not a hook - else: - _type, msg = "STATUS", "This will be your new status message" - target_hook = (txt, None) - return (target_hook, _type, msg) - - if not txt.startswith('@'): - target_hook, _type, msg = getSelectedOrStatus() - elif txt.startswith('@@: '): - _type = "PUBLIC" - msg = MicroblogPanel.warning_msg_public - target_hook = (txt[4:], None) - elif txt.startswith('@'): - _end = txt.find(': ') - if _end == -1: - target_hook, _type, msg = getSelectedOrStatus() - else: - group = txt[1:_end] # only one target group is managed for the moment - if not group or not group in self.host.contact_panel.getGroups(): - # the group doesn't exists, we ignore the key - group = None - target_hook, _type, msg = getSelectedOrStatus() - else: - _type = "GROUP" - msg = MicroblogPanel.warning_msg_group % group - target_hook = (txt[_end + 2:], group) - else: - log.error("Unknown target") - target_hook, _type, msg = getSelectedOrStatus() - - return (target_hook, _type, msg) - - def onKeyPress(self, sender, keycode, modifiers): - _txt = self.getText() - target_hook, type_, msg = self._getTarget(_txt) - - if keycode == KEY_ENTER: - if _txt: - if target_hook: - parsed_txt, data = target_hook - self.host.send([(type_, data)], parsed_txt) - self.host._updateInputHistory(_txt) - self.setText('') - self.host.showWarning(None, None) - else: - self.host.showWarning(type_, msg) - MessageBox.onKeyPress(self, sender, keycode, modifiers) - - def getTargetAndData(self): - """For external use, to get information about the (hypothetical) message - that would be sent if we press Enter right now in the unibox. - @return a tuple (target, data) with: - - data: what would be the content of the message (body) - - target: JID, group with the prefix "@" or the public entity "@@" - """ - _txt = self.getText() - target_hook, _type, _msg = self._getTarget(_txt) - if target_hook: - data, target = target_hook - if target is None: - return target_hook - return (data, "@%s" % (target if target != "" else "@")) - if isinstance(self._selected_cache, MicroblogPanel): - groups = self._selected_cache.accepted_groups - target = "@%s" % (groups[0] if len(groups) > 0 else "@") - if len(groups) > 1: - Window.alert("Sole the first group of the selected panel is taken in consideration: '%s'" % groups[0]) - elif isinstance(self._selected_cache, ChatPanel): - target = self._selected_cache.target - else: - target = None - return (_txt, target) - - def onWidgetClosed(self, lib_wid): - """Called when a libervia widget is closed""" - if self._selected_cache == lib_wid: - self.onSelectedChange(None) - - """def complete(self): - - #self.visible=False #XXX: self.visible is not unset in pyjamas when ENTER is pressed and a completion is done - #XXX: fixed directly on pyjamas, if the patch is accepted, no need to walk around this - return AutoCompleteTextBox.complete(self)""" +# class UniBox(MessageBox, MouseHandler): # AutoCompleteTextBox): +# """This text box is used as a main typing point, for message, microblog, etc""" +# +# def __init__(self, host): +# MessageBox.__init__(self, host) +# #AutoCompleteTextBox.__init__(self) +# self.setStyleName('uniBox') +# # FIXME +# # host.addSelectedListener(self.onSelectedChange) +# +# def addKey(self, key): +# return +# #self.getCompletionItems().completions.append(key) +# +# def removeKey(self, key): +# return +# # TODO: investigate why AutoCompleteTextBox doesn't work here, +# # maybe it can work on a TextBox but no TextArea. Remove addKey +# # and removeKey methods if they don't serve anymore. +# try: +# self.getCompletionItems().completions.remove(key) +# except KeyError: +# log.warning("trying to remove an unknown key") +# +# def _getTarget(self, txt): +# """ Say who will receive the messsage +# @return: a tuple (selected, target_type, target info) with: +# - target_hook: None if we use the selected widget, (msg, data) if we have a hook (e.g. "@@: " for a public blog), where msg is the parsed message (i.e. without the "hook key: "@@: bla" become ("bla", None)) +# - target_type: one of PUBLIC, GROUP, ONE2ONE, STATUS, MISC +# - msg: HTML message which will appear in the privacy warning banner """ +# target = self._selected_cache +# +# def getSelectedOrStatus(): +# if target and target.isSelectable(): +# _type, msg = target.getWarningData() +# target_hook = None # we use the selected widget, not a hook +# else: +# _type, msg = "STATUS", "This will be your new status message" +# target_hook = (txt, None) +# return (target_hook, _type, msg) +# +# if not txt.startswith('@'): +# target_hook, _type, msg = getSelectedOrStatus() +# elif txt.startswith('@@: '): +# _type = "PUBLIC" +# msg = MicroblogPanel.warning_msg_public +# target_hook = (txt[4:], None) +# elif txt.startswith('@'): +# _end = txt.find(': ') +# if _end == -1: +# target_hook, _type, msg = getSelectedOrStatus() +# else: +# group = txt[1:_end] # only one target group is managed for the moment +# if not group or not group in self.host.contact_panel.getGroups(): +# # the group doesn't exists, we ignore the key +# group = None +# target_hook, _type, msg = getSelectedOrStatus() +# else: +# _type = "GROUP" +# msg = MicroblogPanel.warning_msg_group % group +# target_hook = (txt[_end + 2:], group) +# else: +# log.error("Unknown target") +# target_hook, _type, msg = getSelectedOrStatus() +# +# return (target_hook, _type, msg) +# +# def onKeyPress(self, sender, keycode, modifiers): +# _txt = self.getText() +# target_hook, type_, msg = self._getTarget(_txt) +# +# if keycode == KEY_ENTER: +# if _txt: +# if target_hook: +# parsed_txt, data = target_hook +# self.host.send([(type_, data)], parsed_txt) +# self.host._updateInputHistory(_txt) +# self.setText('') +# self.host.showWarning(None, None) +# else: +# self.host.showWarning(type_, msg) +# MessageBox.onKeyPress(self, sender, keycode, modifiers) +# +# def getTargetAndData(self): +# """For external use, to get information about the (hypothetical) message +# that would be sent if we press Enter right now in the unibox. +# @return a tuple (target, data) with: +# - data: what would be the content of the message (body) +# - target: JID, group with the prefix "@" or the public entity "@@" +# """ +# _txt = self.getText() +# target_hook, _type, _msg = self._getTarget(_txt) +# if target_hook: +# data, target = target_hook +# if target is None: +# return target_hook +# return (data, "@%s" % (target if target != "" else "@")) +# if isinstance(self._selected_cache, MicroblogPanel): +# groups = self._selected_cache.accepted_groups +# target = "@%s" % (groups[0] if len(groups) > 0 else "@") +# if len(groups) > 1: +# Window.alert("Sole the first group of the selected panel is taken in consideration: '%s'" % groups[0]) +# # elif isinstance(self._selected_cache, ChatPanel): # FIXME +# # target = self._selected_cache.target +# else: +# target = None +# return (_txt, target) +# +# def onWidgetClosed(self, lib_wid): +# """Called when a libervia widget is closed""" +# if self._selected_cache == lib_wid: +# self.onSelectedChange(None) +# +# """def complete(self): +# +# #self.visible=False #XXX: self.visible is not unset in pyjamas when ENTER is pressed and a completion is done +# #XXX: fixed directly on pyjamas, if the patch is accepted, no need to walk around this +# return AutoCompleteTextBox.complete(self)""" class WarningPopup(): @@ -303,21 +282,21 @@ def showWarning(self, type_=None, msg=None, duration=2000): """Display a popup information message, e.g. to notify the recipient of a message being composed. If type_ is None, a popup being currently displayed will be hidden. - @type_: a type determining the CSS style to be applied (see __showWarning) + @type_: a type determining the CSS style to be applied (see _showWarning) @msg: message to be displayed """ if type_ is None: self.__removeWarning() return if not self._popup: - self.__showWarning(type_, msg) + self._showWarning(type_, msg) elif (type_, msg) != self._popup.target_data: self._timeCb(None) # we remove the popup - self.__showWarning(type_, msg) + self._showWarning(type_, msg) self._timer.schedule(duration) - def __showWarning(self, type_, msg): + def _showWarning(self, type_, msg): """Display a popup information message, e.g. to notify the recipient of a message being composed. @type_: a type determining the CSS style to be applied. For now the defined styles are "NONE" (will do nothing), "PUBLIC", "GROUP", "STATUS" and "ONE2ONE". @@ -364,641 +343,6 @@ self._timeCb(None) -class MicroblogItem(): - # XXX: should be moved in a separated module - - def __init__(self, data): - self.id = data['id'] - self.type = data.get('type', 'main_item') - self.empty = data.get('new', False) - self.title = data.get('title', '') - self.title_xhtml = data.get('title_xhtml', '') - self.content = data.get('content', '') - self.content_xhtml = data.get('content_xhtml', '') - self.author = data['author'] - self.updated = float(data.get('updated', 0)) # XXX: int doesn't work here - self.published = float(data.get('published', self.updated)) # XXX: int doesn't work here - self.service = data.get('service', '') - self.node = data.get('node', '') - self.comments = data.get('comments', False) - self.comments_service = data.get('comments_service', '') - self.comments_node = data.get('comments_node', '') - - -class MicroblogEntry(SimplePanel, ClickHandler, FocusHandler, KeyboardHandler): - - def __init__(self, blog_panel, data): - """ - @param blog_panel: the parent panel - @param data: dict containing the blog item data, or a MicroblogItem instance. - """ - self._base_item = data if isinstance(data, MicroblogItem) else MicroblogItem(data) - for attr in ['id', 'type', 'empty', 'title', 'title_xhtml', 'content', 'content_xhtml', - 'author', 'updated', 'published', 'comments', 'service', 'node', - 'comments_service', 'comments_node']: - getter = lambda attr: lambda inst: getattr(inst._base_item, attr) - setter = lambda attr: lambda inst, value: setattr(inst._base_item, attr, value) - setattr(MicroblogEntry, attr, property(getter(attr), setter(attr))) - - SimplePanel.__init__(self) - self._blog_panel = blog_panel - - self.panel = FlowPanel() - self.panel.setStyleName('mb_entry') - - self.header = HTMLPanel('') - self.panel.add(self.header) - - self.entry_actions = VerticalPanel() - self.entry_actions.setStyleName('mb_entry_actions') - self.panel.add(self.entry_actions) - - entry_avatar = SimplePanel() - entry_avatar.setStyleName('mb_entry_avatar') - self.avatar = Image(self._blog_panel.host.getAvatar(self.author)) - entry_avatar.add(self.avatar) - self.panel.add(entry_avatar) - - if TOGGLE_EDITION_USE_ICON: - self.entry_dialog = HorizontalPanel() - else: - self.entry_dialog = VerticalPanel() - self.entry_dialog.setStyleName('mb_entry_dialog') - self.panel.add(self.entry_dialog) - - self.add(self.panel) - ClickHandler.__init__(self) - self.addClickListener(self) - - self.__pub_data = (self.service, self.node, self.id) - self.__setContent() - - def __setContent(self): - """Actually set the entry content (header, icons, bubble...)""" - self.delete_label = self.update_label = self.comment_label = None - self.bubble = self._current_comment = None - self.__setHeader() - self.__setBubble() - self.__setIcons() - - def __setHeader(self): - """Set the entry header""" - if self.empty: - return - update_text = u" — ✍ " + "" % datetime.fromtimestamp(self.updated) - self.header.setHTML("""
- on - %(updated)s -
""" % {'author': html_tools.html_sanitize(self.author), - 'published': datetime.fromtimestamp(self.published), - 'updated': update_text if self.published != self.updated else '' - } - ) - - def __setIcons(self): - """Set the entry icons (delete, update, comment)""" - if self.empty: - return - - def addIcon(label, title): - label = Label(label) - label.setTitle(title) - label.addClickListener(self) - self.entry_actions.add(label) - return label - - if self.comments: - self.comment_label = addIcon(u"↶", "Comment this message") - self.comment_label.setStyleName('mb_entry_action_larger') - is_publisher = self.author == self._blog_panel.host.whoami.bare - if is_publisher: - self.update_label = addIcon(u"✍", "Edit this message") - if is_publisher or str(self.node).endswith(self._blog_panel.host.whoami.bare): - self.delete_label = addIcon(u"✗", "Delete this message") - - def updateAvatar(self, new_avatar): - """Change the avatar of the entry - @param new_avatar: path to the new image""" - self.avatar.setUrl(new_avatar) - - def onClick(self, sender): - if sender == self: - self._blog_panel.setSelectedEntry(self) - elif sender == self.delete_label: - self._delete() - elif sender == self.update_label: - self.edit(True) - elif sender == self.comment_label: - self._comment() - - def __modifiedCb(self, content): - """Send the new content to the backend - @return: False to restore the original content if a deletion has been cancelled - """ - if not content['text']: # previous content has been emptied - self._delete(True) - return False - extra = {'published': str(self.published)} - if isinstance(self.bubble, richtext.RichTextEditor): - # TODO: if the user change his parameters after the message edition started, - # the message syntax could be different then the current syntax: pass the - # message syntax in extra for the frontend to use it instead of current syntax. - extra.update({'content_rich': content['text'], 'title': content['title']}) - if self.empty: - if self.type == 'main_item': - self._blog_panel.host.bridge.call('sendMblog', None, None, self._blog_panel.accepted_groups, content['text'], extra) - else: - self._blog_panel.host.bridge.call('sendMblogComment', None, self._parent_entry.comments, content['text'], extra) - else: - self._blog_panel.host.bridge.call('updateMblog', None, self.__pub_data, self.comments, content['text'], extra) - return True - - def __afterEditCb(self, content): - """Remove the entry if it was an empty one (used for creating a new blog post). - Data for the actual new blog post will be received from the bridge""" - if self.empty: - self._blog_panel.removeEntry(self.type, self.id) - if self.type == 'main_item': # restore the "New message" button - self._blog_panel.refresh() - else: # allow to create a new comment - self._parent_entry._current_comment = None - self.entry_dialog.setWidth('auto') - try: - self.toggle_syntax_button.removeFromParent() - except TypeError: - pass - - def __setBubble(self, edit=False): - """Set the bubble displaying the initial content.""" - content = {'text': self.content_xhtml if self.content_xhtml else self.content, - 'title': self.title_xhtml if self.title_xhtml else self.title} - if self.content_xhtml: - content.update({'syntax': C.SYNTAX_XHTML}) - if self.author != self._blog_panel.host.whoami.bare: - options = ['read_only'] - else: - options = [] if self.empty else ['update_msg'] - self.bubble = richtext.RichTextEditor(self._blog_panel.host, content, self.__modifiedCb, self.__afterEditCb, options) - else: # assume raw text message have no title - self.bubble = base_panels.LightTextEditor(content, self.__modifiedCb, self.__afterEditCb, options={'no_xhtml': True}) - self.bubble.addStyleName("bubble") - try: - self.toggle_syntax_button.removeFromParent() - except TypeError: - pass - self.entry_dialog.add(self.bubble) - self.edit(edit) - self.bubble.addEditListener(self.__showWarning) - - def __showWarning(self, sender, keycode): - if keycode == KEY_ENTER: - self._blog_panel.host.showWarning(None, None) - else: - self._blog_panel.host.showWarning(*self._blog_panel.getWarningData(self.type == 'comment')) - - def _delete(self, empty=False): - """Ask confirmation for deletion. - @return: False if the deletion has been cancelled.""" - def confirm_cb(answer): - if answer: - self._blog_panel.host.bridge.call('deleteMblog', None, self.__pub_data, self.comments) - else: # restore the text if it has been emptied during the edition - self.bubble.setContent(self.bubble._original_content) - - if self.empty: - text = _("New ") + (_("message") if self.comments else _("comment")) + _(" without body has been cancelled.") - dialog.InfoDialog(_("Information"), text).show() - return - text = "" - if empty: - text = (_("Message") if self.comments else _("Comment")) + _(" body has been emptied.
") - target = _('message and all its comments') if self.comments else _('comment') - text += _("Do you really want to delete this %s?") % target - dialog.ConfirmDialog(confirm_cb, text=text).show() - - def _comment(self): - """Add an empty entry for a new comment""" - if self._current_comment: - self._current_comment.bubble.setFocus(True) - self._blog_panel.setSelectedEntry(self._current_comment, True) - return - data = {'id': str(time()), - 'new': True, - 'type': 'comment', - 'author': self._blog_panel.host.whoami.bare, - 'service': self.comments_service, - 'node': self.comments_node - } - entry = self._blog_panel.addEntry(data) - if entry is None: - log.info("The entry of id %s can not be commented" % self.id) - return - entry._parent_entry = self - self._current_comment = entry - self.edit(True, entry) - self._blog_panel.setSelectedEntry(entry, True) - - def edit(self, edit, entry=None): - """Toggle the bubble between display and edit mode - @edit: boolean value - @entry: MicroblogEntry instance, or None to use self - """ - if entry is None: - entry = self - try: - entry.toggle_syntax_button.removeFromParent() - except TypeError: - pass - entry.bubble.edit(edit) - if edit: - if isinstance(entry.bubble, richtext.RichTextEditor): - image = 'A' - html = 'raw text' - title = _('Switch to raw text edition') - else: - image = '' - html = 'rich text' - title = _('Switch to rich text edition') - if TOGGLE_EDITION_USE_ICON: - entry.entry_dialog.setWidth('80%') - entry.toggle_syntax_button = Button(image, entry.toggleContentSyntax) - entry.toggle_syntax_button.setTitle(title) - entry.entry_dialog.add(entry.toggle_syntax_button) - else: - entry.toggle_syntax_button = HTML(html) - entry.toggle_syntax_button.addClickListener(entry.toggleContentSyntax) - entry.toggle_syntax_button.addStyleName('mb_entry_toggle_syntax') - entry.entry_dialog.add(entry.toggle_syntax_button) - entry.toggle_syntax_button.setStyleAttribute('top', '-20px') # XXX: need to force CSS - entry.toggle_syntax_button.setStyleAttribute('left', '-20px') - - def toggleContentSyntax(self): - """Toggle the editor between raw and rich text""" - original_content = self.bubble.getOriginalContent() - rich = not isinstance(self.bubble, richtext.RichTextEditor) - if rich: - original_content['syntax'] = C.SYNTAX_XHTML - - def setBubble(text): - self.content = text - self.content_xhtml = text if rich else '' - self.content_title = self.content_title_xhtml = '' - self.bubble.removeFromParent() - self.__setBubble(True) - self.bubble.setOriginalContent(original_content) - if rich: - self.bubble.setDisplayContent() # needed in case the edition is aborted, to not end with an empty bubble - - text = self.bubble.getContent()['text'] - if not text: - setBubble(' ') # something different than empty string is needed to initialize the rich text editor - return - if not rich: - def confirm_cb(answer): - if answer: - self._blog_panel.host.bridge.call('syntaxConvert', setBubble, text, C.SYNTAX_CURRENT, C.SYNTAX_TEXT) - dialog.ConfirmDialog(confirm_cb, text=_("Do you really want to lose the title and text formatting?")).show() - else: - self._blog_panel.host.bridge.call('syntaxConvert', setBubble, text, C.SYNTAX_TEXT, C.SYNTAX_XHTML) - - -class MicroblogPanel(base_widget.LiberviaWidget): - warning_msg_public = "This message will be PUBLIC and everybody will be able to see it, even people you don't know" - warning_msg_group = "This message will be published for all the people of the group %s" - - def __init__(self, host, accepted_groups): - """Panel used to show microblog - @param accepted_groups: groups displayed in this panel, if empty, show all microblogs from all contacts - """ - base_widget.LiberviaWidget.__init__(self, host, ", ".join(accepted_groups), selectable=True) - self.setAcceptedGroup(accepted_groups) - self.host = host - self.entries = {} - self.comments = {} - self.selected_entry = None - self.vpanel = VerticalPanel() - self.vpanel.setStyleName('microblogPanel') - self.setWidget(self.vpanel) - - def refresh(self): - """Refresh the display of this widget. If the unibox is disabled, - display the 'New message' button or an empty bubble on top of the panel""" - if hasattr(self, 'new_button'): - self.new_button.setVisible(self.host.uni_box is None) - return - if self.host.uni_box is None: - def addBox(): - if hasattr(self, 'new_button'): - self.new_button.setVisible(False) - data = {'id': str(time()), - 'new': True, - 'author': self.host.whoami.bare, - } - entry = self.addEntry(data) - entry.edit(True) - if NEW_MESSAGE_USE_BUTTON: - self.new_button = Button("New message", listener=addBox) - self.new_button.setStyleName("microblogNewButton") - self.vpanel.insert(self.new_button, 0) - elif not self.getNewMainEntry(): - addBox() - - def getNewMainEntry(self): - """Get the new entry being edited, or None if it doesn't exists. - - @return (MicroblogEntry): the new entry being edited. - """ - try: - first = self.vpanel.children[0] - except IndexError: - return None - assert(first.type == 'main_item') - return first if first.empty else None - - @classmethod - def registerClass(cls): - base_widget.LiberviaWidget.addDropKey("GROUP", cls.createPanel) - base_widget.LiberviaWidget.addDropKey("CONTACT_TITLE", cls.createMetaPanel) - - @classmethod - def createPanel(cls, host, item): - """Generic panel creation for one, several or all groups (meta). - @parem host: the SatWebFrontend instance - @param item: single group as a string, list of groups - (as an array) or None (for the meta group = "all groups") - @return: the created MicroblogPanel - """ - _items = item if isinstance(item, list) else ([] if item is None else [item]) - _type = 'ALL' if _items == [] else 'GROUP' - # XXX: pyjamas doesn't support use of cls directly - _new_panel = MicroblogPanel(host, _items) - host.FillMicroblogPanel(_new_panel) - host.bridge.call('getMassiveLastMblogs', _new_panel.massiveInsert, _type, _items, 10) - host.setSelected(_new_panel) - _new_panel.refresh() - return _new_panel - - @classmethod - def createMetaPanel(cls, host, item): - """Needed for the drop keys to not be mixed between meta panel and panel for "Contacts" group""" - return MicroblogPanel.createPanel(host, None) - - @property - def accepted_groups(self): - return self._accepted_groups - - def matchEntity(self, item): - """ - @param item: single group as a string, list of groups - (as an array) or None (for the meta group = "all groups") - @return: True if self matches the given entity - """ - groups = item if isinstance(item, list) else ([] if item is None else [item]) - groups.sort() # sort() do not return the sorted list: do it here, not on the "return" line - return self.accepted_groups == groups - - def getWarningData(self, comment=None): - """ - @param comment: True if the composed message is a comment. If None, consider we are - composing from the unibox and guess the message type from self.selected_entry - @return: a couple (type, msg) for calling self.host.showWarning""" - if comment is None: # composing from the unibox - if self.selected_entry and not self.selected_entry.comments: - log.error("an item without comment is selected") - return ("NONE", None) - comment = self.selected_entry is not None - if comment: - return ("PUBLIC", "This is a comment and keep the initial post visibility, so it is potentialy public") - elif not self._accepted_groups: - # we have a meta MicroblogPanel, we publish publicly - return ("PUBLIC", self.warning_msg_public) - else: - # we only accept one group at the moment - # FIXME: manage several groups - return ("GROUP", self.warning_msg_group % self._accepted_groups[0]) - - def onTextEntered(self, text): - if self.selected_entry: - # we are entering a comment - comments_url = self.selected_entry.comments - if not comments_url: - raise Exception("ERROR: the comments URL is empty") - target = ("COMMENT", comments_url) - elif not self._accepted_groups: - # we are entering a public microblog - target = ("PUBLIC", None) - else: - # we are entering a microblog restricted to a group - # FIXME: manage several groups - target = ("GROUP", self._accepted_groups[0]) - self.host.send([target], text) - - def accept_all(self): - return not self._accepted_groups # we accept every microblog only if we are not filtering by groups - - def getEntries(self): - """Ask all the entries for the currenly accepted groups, - and fill the panel""" - - def massiveInsert(self, mblogs): - """Insert several microblogs at once - @param mblogs: dictionary of microblogs, as the result of getMassiveLastGroupBlogs - """ - count = sum([len(value) for value in mblogs.values()]) - log.debug("Massive insertion of %d microblogs" % count) - for publisher in mblogs: - log.debug("adding blogs for [%s]" % publisher) - for mblog in mblogs[publisher]: - if not "content" in mblog: - log.warning("No content found in microblog [%s]" % mblog) - continue - self.addEntry(mblog) - - def mblogsInsert(self, mblogs): - """ Insert several microblogs at once - @param mblogs: list of microblogs - """ - for mblog in mblogs: - if not "content" in mblog: - log.warning("No content found in microblog [%s]" % mblog) - continue - self.addEntry(mblog) - - def _chronoInsert(self, vpanel, entry, reverse=True): - """ Insert an entry in chronological order - @param vpanel: VerticalPanel instance - @param entry: MicroblogEntry - @param reverse: more recent entry on top if True, chronological order else""" - assert(isinstance(reverse, bool)) - if entry.empty: - entry.published = time() - # we look for the right index to insert our entry: - # if reversed, we insert the entry above the first entry - # in the past - idx = 0 - - for child in vpanel.children: - if not isinstance(child, MicroblogEntry): - idx += 1 - continue - condition_to_stop = child.empty or (child.published > entry.published) - if condition_to_stop != reverse: # != is XOR - break - idx += 1 - - vpanel.insert(entry, idx) - - def addEntry(self, data): - """Add an entry to the panel - @param data: dict containing the item data - @return: the added entry, or None - """ - _entry = MicroblogEntry(self, data) - if _entry.type == "comment": - comments_hash = (_entry.service, _entry.node) - if not comments_hash in self.comments: - # The comments node is not known in this panel - return None - parent = self.comments[comments_hash] - parent_idx = self.vpanel.getWidgetIndex(parent) - # we find or create the panel where the comment must be inserted - try: - sub_panel = self.vpanel.getWidget(parent_idx + 1) - except IndexError: - sub_panel = None - if not sub_panel or not isinstance(sub_panel, VerticalPanel): - sub_panel = VerticalPanel() - sub_panel.setStyleName('microblogPanel') - sub_panel.addStyleName('subPanel') - self.vpanel.insert(sub_panel, parent_idx + 1) - for idx in xrange(0, len(sub_panel.getChildren())): - comment = sub_panel.getIndexedChild(idx) - if comment.id == _entry.id: - # update an existing comment - sub_panel.remove(comment) - sub_panel.insert(_entry, idx) - return _entry - # we want comments to be inserted in chronological order - self._chronoInsert(sub_panel, _entry, reverse=False) - return _entry - - if _entry.id in self.entries: # update - idx = self.vpanel.getWidgetIndex(self.entries[_entry.id]) - self.vpanel.remove(self.entries[_entry.id]) - self.vpanel.insert(_entry, idx) - else: # new entry - self._chronoInsert(self.vpanel, _entry) - self.entries[_entry.id] = _entry - - if _entry.comments: - # entry has comments, we keep the comments service/node as a reference - comments_hash = (_entry.comments_service, _entry.comments_node) - self.comments[comments_hash] = _entry - self.host.bridge.call('getMblogComments', self.mblogsInsert, _entry.comments_service, _entry.comments_node) - - return _entry - - def removeEntry(self, type_, id_): - """Remove an entry from the panel - @param type_: entry type ('main_item' or 'comment') - @param id_: entry id - """ - for child in self.vpanel.getChildren(): - if isinstance(child, MicroblogEntry) and type_ == 'main_item': - if child.id == id_: - main_idx = self.vpanel.getWidgetIndex(child) - try: - sub_panel = self.vpanel.getWidget(main_idx + 1) - if isinstance(sub_panel, VerticalPanel): - sub_panel.removeFromParent() - except IndexError: - pass - child.removeFromParent() - self.selected_entry = None - break - elif isinstance(child, VerticalPanel) and type_ == 'comment': - for comment in child.getChildren(): - if comment.id == id_: - comment.removeFromParent() - self.selected_entry = None - break - - def ensureVisible(self, entry): - """Scroll to an entry to ensure its visibility - - @param entry (MicroblogEntry): the entry - """ - try: - self.vpanel.getParent().ensureVisible(entry) # scroll to the clicked entry - except AttributeError: - log.warning("FIXME: MicroblogPanel.vpanel should be wrapped in a ScrollPanel!") - - def setSelectedEntry(self, entry, ensure_visible=False): - """Select an entry. - - @param entry (MicroblogEntry): the entry to select - @param ensure_visible (boolean): if True, also scroll to the entry - """ - if ensure_visible: - self.ensureVisible(entry) - - if not self.host.uni_box or not entry.comments: - entry.addStyleName('selected_entry') # blink the clicked entry - clicked_entry = entry # entry may be None when the timer is done - Timer(500, lambda timer: clicked_entry.removeStyleName('selected_entry')) - if not self.host.uni_box: - return # unibox is disabled - - # from here the previous behavior (toggle main item selection) is conserved - entry = entry if entry.comments else None - if self.selected_entry == entry: - entry = None - if self.selected_entry: - self.selected_entry.removeStyleName('selected_entry') - if entry: - log.debug("microblog entry selected (author=%s)" % entry.author) - entry.addStyleName('selected_entry') - self.selected_entry = entry - - def updateValue(self, type_, jid, value): - """Update a jid value in entries - @param type_: one of 'avatar', 'nick' - @param jid: jid concerned - @param value: new value""" - def updateVPanel(vpanel): - for child in vpanel.children: - if isinstance(child, MicroblogEntry) and child.author == jid: - child.updateAvatar(value) - elif isinstance(child, VerticalPanel): - updateVPanel(child) - if type_ == 'avatar': - updateVPanel(self.vpanel) - - def setAcceptedGroup(self, group): - """Add one or more group(s) which can be displayed in this panel. - Prevent from duplicate values and keep the list sorted. - @param group: string of the group, or list of string - """ - if not hasattr(self, "_accepted_groups"): - self._accepted_groups = [] - groups = group if isinstance(group, list) else [group] - for _group in groups: - if _group not in self._accepted_groups: - self._accepted_groups.append(_group) - self._accepted_groups.sort() - - def isJidAccepted(self, jid_s): - """Tell if a jid is actepted and shown in this panel - @param jid_s: jid - @return: True if the jid is accepted""" - if self.accept_all(): - return True - for group in self._accepted_groups: - if self.host.contact_panel.isContactInGroup(group, jid_s): - return True - return False - - class StatusPanel(base_panels.HTMLTextEditor): EMPTY_STATUS = '<click to set a status>' @@ -1047,7 +391,7 @@ base_widget.WidgetMenuBar.__init__(self, None, parent.host, styles=styles) self.button = self.addCategory(u"◉", u"◉", '') for presence, presence_i18n in C.PRESENCE.items(): - html = u' %s' % (contact.buildPresenceStyle(presence), presence_i18n) + html = u' %s' % (contact_list.buildPresenceStyle(presence), presence_i18n) self.addMenuItem([u"◉", presence], [u"◉", html], '', base_menu.MenuCmd(self, 'changePresenceCb', presence), asHTML=True) self.parent_panel = parent @@ -1094,7 +438,7 @@ def setPresence(self, presence): self._presence = presence - contact.setPresenceStyle(self.menu.button, self._presence) + contact_list.setPresenceStyle(self.menu.button, self._presence) def setStatus(self, status): self.status_panel.setContent({'text': status}) @@ -1105,283 +449,20 @@ self.host.setSelected(None) -class ChatPanel(base_widget.LiberviaWidget, KeyboardHandler): - - def __init__(self, host, target, type_='one2one'): - """Panel used for conversation (one 2 one or group chat) - @param host: SatWebFrontend instance - @param target: entity (jid.JID) with who we have a conversation (contact's jid for one 2 one chat, or MUC room) - @param type: one2one for simple conversation, group for MUC""" - self.vpanel = VerticalPanel() - self.vpanel.setSize('100%', '100%') - self.nick = None - if not target: - log.error("Empty target !") - return - self.target = target - self.type = type_ - - # FIXME: temporary dirty initialization to display the OTR state - def header_info_cb(cb): - host.plugins['otr'].infoTextCallback(target, cb) - header_info = header_info_cb if (type_ == 'one2one' and 'otr' in host.plugins) else None - - base_widget.LiberviaWidget.__init__(self, host, title=target.bare, info=header_info, selectable=True) - self.__body = AbsolutePanel() - self.__body.setStyleName('chatPanel_body') - chat_area = HorizontalPanel() - chat_area.setStyleName('chatArea') - if type_ == 'group': - self.occupants_list = base_panels.OccupantsList() - self.occupants_initialised = False - chat_area.add(self.occupants_list) - self.__body.add(chat_area) - self.content = AbsolutePanel() - self.content.setStyleName('chatContent') - self.content_scroll = base_widget.ScrollPanelWrapper(self.content) - chat_area.add(self.content_scroll) - chat_area.setCellWidth(self.content_scroll, '100%') - self.vpanel.add(self.__body) - self.vpanel.setCellHeight(self.__body, '100%') - self.addStyleName('chatPanel') - self.setWidget(self.vpanel) - self.state_machine = plugin_xep_0085.ChatStateMachine(self.host, str(self.target)) - self._state = None - - @classmethod - def registerClass(cls): - base_widget.LiberviaWidget.addDropKey("CONTACT", cls.createPanel) - - @classmethod - def createPanel(cls, host, item, type_='one2one'): - assert(item) - _contact = item if isinstance(item, jid.JID) else jid.JID(item) - host.contact_panel.setContactMessageWaiting(_contact.bare, False) - _new_panel = ChatPanel(host, _contact, type_) # XXX: pyjamas doesn't seems to support creating with cls directly - _new_panel.historyPrint() - host.setSelected(_new_panel) - _new_panel.refresh() - return _new_panel - - def refresh(self): - """Refresh the display of this widget. If the unibox is disabled, - add a message box at the bottom of the panel""" - self.host.contact_panel.setContactMessageWaiting(self.target.bare, False) - self.content_scroll.scrollToBottom() - - enable_box = self.host.uni_box is None - if hasattr(self, 'message_box'): - self.message_box.setVisible(enable_box) - return - if enable_box: - self.message_box = MessageBox(self.host) - self.message_box.onSelectedChange(self) - self.message_box.addKeyboardListener(self) - self.vpanel.add(self.message_box) - - def onKeyDown(self, sender, keycode, modifiers): - if keycode == KEY_ENTER: - self.host.showWarning(None, None) - else: - self.host.showWarning(*self.getWarningData()) - - def matchEntity(self, item, type_=None): - """ - @param entity: target jid as a string or jid.JID instance. - @return: True if self matches the given entity - """ - if type_ is None: - type_ = self.type - entity = item if isinstance(item, jid.JID) else jid.JID(item) - try: - return self.target.bare == entity.bare and self.type == type_ - except AttributeError as e: - e.include_traceback() - return False - - def addMenus(self, menu_bar): - """Add cached menus to the header. - - @param menu_bar (GenericMenuBar): menu bar of the widget's header - """ - if self.type == 'group': - menu_bar.addCachedMenus(C.MENU_ROOM, {'room_jid': self.target.bare}) - elif self.type == 'one2one': - menu_bar.addCachedMenus(C.MENU_SINGLE, {'jid': self.target}) - - def getWarningData(self): - if self.type not in ["one2one", "group"]: - raise Exception("Unmanaged type !") - if self.type == "one2one": - msg = "This message will be sent to your contact %s" % self.target - elif self.type == "group": - msg = "This message will be sent to all the participants of the multi-user room %s" % self.target - return ("ONE2ONE" if self.type == "one2one" else "GROUP", msg) - - def onTextEntered(self, text): - self.host.send([("groupchat" if self.type == 'group' else "chat", str(self.target))], text) - self.state_machine._onEvent("active") - - def onQuit(self): - base_widget.LiberviaWidget.onQuit(self) - if self.type == 'group': - self.host.bridge.call('mucLeave', None, self.target.bare) - - def setUserNick(self, nick): - """Set the nick of the user, usefull for e.g. change the color of the user""" - self.nick = nick - - def setPresents(self, nicks): - """Set the users presents in this room - @param occupants: list of nicks (string)""" - for nick in nicks: - self.occupants_list.addOccupant(nick) - self.occupants_initialised = True - - def userJoined(self, nick, data): - if self.occupants_list.getOccupantBox(nick): - return # user is already displayed - self.occupants_list.addOccupant(nick) - if self.occupants_initialised: - self.printInfo("=> %s has joined the room" % nick) - - def userLeft(self, nick, data): - self.occupants_list.removeOccupant(nick) - self.printInfo("<= %s has left the room" % nick) - - def changeUserNick(self, old_nick, new_nick): - assert(self.type == "group") - self.occupants_list.removeOccupant(old_nick) - self.occupants_list.addOccupant(new_nick) - self.printInfo(_("%(old_nick)s is now known as %(new_nick)s") % {'old_nick': old_nick, 'new_nick': new_nick}) - - def historyPrint(self, size=C.HISTORY_LIMIT_DEFAULT): - """Print the initial history""" - def getHistoryCB(history): - # display day change - day_format = "%A, %d %b %Y" - previous_day = datetime.now().strftime(day_format) - for line in history: - timestamp, from_jid_s, to_jid_s, message, mess_type, extra = line - message_day = datetime.fromtimestamp(float(timestamp or time())).strftime(day_format) - if previous_day != message_day: - self.printInfo("* " + message_day) - previous_day = message_day - self.printMessage(jid.JID(from_jid_s), message, extra, timestamp) - self.host.bridge.call('getHistory', getHistoryCB, self.host.whoami.bare, self.target.bare, size, True) - - def printInfo(self, msg, type_='normal', link_cb=None): - """Print general info - @param msg: message to print - @param type_: one of: - "normal": general info like "toto has joined the room" (will be sanitized) - "link": general info that is clickable like "click here to join the main room" (no sanitize done) - "me": "/me" information like "/me clenches his fist" ==> "toto clenches his fist" (will stay on one line) - @param link_cb: method to call when the info is clicked, ignored if type_ is not 'link' - """ - if type_ == 'normal': - _wid = HTML(addURLToText(html_tools.XHTML2Text(msg))) - _wid.setStyleName('chatTextInfo') - elif type_ == 'link': - _wid = HTML(msg) - _wid.setStyleName('chatTextInfo-link') - if link_cb: - _wid.addClickListener(link_cb) - elif type_ == 'me': - _wid = Label(msg) - _wid.setStyleName('chatTextMe') - else: - raise ValueError("Unknown printInfo type %s" % type_) - self.content.add(_wid) - self.content_scroll.scrollToBottom() - - def printMessage(self, from_jid, msg, extra, timestamp=None): - """Print message in chat window. Must be implemented by child class""" - nick = from_jid.node if self.type == 'one2one' else from_jid.resource - mymess = from_jid.resource == self.nick if self.type == "group" else from_jid.bare == self.host.whoami.bare # mymess = True if message comes from local user - if msg.startswith('/me '): - self.printInfo('* %s %s' % (nick, msg[4:]), type_='me') - return - self.content.add(base_panels.ChatText(timestamp, nick, mymess, msg, extra.get('xhtml'))) - self.content_scroll.scrollToBottom() - - def startGame(self, game_type, waiting, referee, players, *args): - """Configure the chat window to start a game""" - classes = {"Tarot": card_game.CardPanel, "RadioCol": radiocol.RadioColPanel} - if game_type not in classes.keys(): - return # unknown game - attr = game_type.lower() - self.occupants_list.updateSpecials(players, SYMBOLS[attr]) - if waiting or not self.nick in players: - return # waiting for player or not playing - attr = "%s_panel" % attr - if hasattr(self, attr): - return - log.info("%s Game Started \o/" % game_type) - panel = classes[game_type](self, referee, self.nick, players, *args) - setattr(self, attr, panel) - self.vpanel.insert(panel, 0) - self.vpanel.setCellHeight(panel, panel.getHeight()) - - def getGame(self, game_type): - """Return class managing the game type""" - # TODO: check that the game is launched, and manage errors - if game_type == "Tarot": - return self.tarot_panel - elif game_type == "RadioCol": - return self.radiocol_panel - - def setState(self, state, nick=None): - """Set the chat state (XEP-0085) of the contact. Leave nick to None - to set the state for a one2one conversation, or give a nickname or - C.ALL_OCCUPANTS to set the state of a participant within a MUC. - @param state: the new chat state - @param nick: ignored for one2one, otherwise the MUC user nick or C.ALL_OCCUPANTS - """ - if self.type == 'group': - assert(nick) - if nick == C.ALL_OCCUPANTS: - occupants = self.occupants_list.occupants_list.keys() - else: - occupants = [nick] if nick in self.occupants_list.occupants_list else [] - for occupant in occupants: - self.occupants_list.occupants_list[occupant].setState(state) - else: - self._state = state - self.refreshTitle() - self.state_machine.started = not not state # start to send "composing" state from now - - def refreshTitle(self): - """Refresh the title of this ChatPanel dialog""" - if self._state: - self.setTitle(self.target.bare + " (" + self._state + ")") - else: - self.setTitle(self.target.bare) - - def setConnected(self, jid_s, resource, availability, priority, statuses): - """Set connection status - @param jid_s (str): JID userhost as unicode - """ - assert(jid_s == self.target.bare) - if self.type != 'group': - return - box = self.occupants_list.getOccupantBox(resource) - if box: - contact.setPresenceStyle(box, availability) - - -class WebPanel(base_widget.LiberviaWidget): +class WebPanel(quick_widgets.QuickWidget, base_widget.LiberviaWidget): """ (mini)browser like widget """ - def __init__(self, host, url=None): + def __init__(self, host, target, profiles=None): """ @param host: SatWebFrontend instance + @param target: url to open """ + quick_widgets.QuickWidget.__init__(self, host, target, C.PROF_KEY_NONE) base_widget.LiberviaWidget.__init__(self, host) self._vpanel = VerticalPanel() self._vpanel.setSize('100%', '100%') self._url = dialog.ExtTextBox(enter_cb=self.onUrlClick) - self._url.setText(url or "") + self._url.setText(target or "") self._url.setWidth('100%') hpanel = HorizontalPanel() hpanel.add(self._url) @@ -1392,7 +473,7 @@ hpanel.add(btn) self._vpanel.add(hpanel) self._vpanel.setCellHeight(hpanel, '20px') - self._frame = Frame(url or "") + self._frame = Frame(target or "") self._frame.setSize('100%', '100%') DOM.setStyleAttribute(self._frame.getElement(), "position", "relative") self._vpanel.add(self._frame) @@ -1411,9 +492,9 @@ # menu self.menu = menu.MainMenuPanel(host) - # unibox - self.unibox_panel = UniBoxPanel(host) - self.unibox_panel.setVisible(False) + # # unibox + # self.unibox_panel = UniBoxPanel(host) + # self.unibox_panel.setVisible(False) # contacts self._contacts = HorizontalPanel() @@ -1421,7 +502,6 @@ self.contacts_switch = Button(u'«', self._contactsSwitch) self.contacts_switch.addStyleName('contactsSwitch') self._contacts.add(self.contacts_switch) - self._contacts.add(self.host.contact_panel) # tabs self.tab_panel = base_widget.MainTabPanel(host) @@ -1431,7 +511,7 @@ self.header = AbsolutePanel() self.header.add(self.menu) - self.header.add(self.unibox_panel) + # self.header.add(self.unibox_panel) self.header.add(self.host.status_panel) self.header.setStyleName('header') self.add(self.header) @@ -1444,13 +524,16 @@ self.setWidth("100%") Window.addWindowResizeListener(self) + def addContactList(self, contact_list): + self._contacts.add(contact_list) + def _contactsSwitch(self, btn=None): """ (Un)hide contacts panel """ if btn is None: btn = self.contacts_switch - cpanel = self.host.contact_panel - cpanel.setVisible(not cpanel.getVisible()) - btn.setText(u"«" if cpanel.getVisible() else u"»") + clist = self.host.contact_list + clist.setVisible(not clist.getVisible()) + btn.setText(u"«" if clist.getVisible() else u"»") self.host.resize() def _contactsMove(self, parent): diff -r bade589dbd5a -r a5019e62c3e9 src/browser/sat_browser/plugin_sec_otr.py --- a/src/browser/sat_browser/plugin_sec_otr.py Thu Oct 23 16:56:36 2014 +0200 +++ b/src/browser/sat_browser/plugin_sec_otr.py Sat Jan 24 01:45:39 2015 +0100 @@ -29,7 +29,7 @@ log = getLogger(__name__) from constants import Const as C -import jid +from sat_frontends.tools import jid import otrjs_wrapper as otr import dialog import panels @@ -127,7 +127,7 @@ @param host (satWebFrontend) @param account (Account) - @param other_jid (JID): JID of the person your chat correspondent + @param other_jid (jid.JID): JID of the person your chat correspondent """ super(Context, self).__init__(account, other_jid) self.host = host @@ -157,14 +157,14 @@ log.debug("message received (was %s): %s" % ('encrypted' if encrypted else 'plain', msg)) if not encrypted: if self.state == otr.context.STATE_ENCRYPTED: - log.warning(u"Received unencrypted message in an encrypted context (from %(jid)s)" % {'jid': self.peer.full()}) + log.warning(u"Received unencrypted message in an encrypted context (from %(jid)s)" % {'jid': self.peer}) self.host.newMessageCb(self.peer, RECEIVE_PLAIN_IN_ENCRYPTED_CONTEXT, C.MESS_TYPE_INFO, self.host.whoami, {}) self.host.newMessageCb(self.peer, msg, "chat", self.host.whoami, {}) def sendMessageCb(self, msg, meta=None): assert isinstance(self.peer, jid.JID) log.debug("message to send%s: %s" % ((' (attached meta data: %s)' % meta) if meta else '', msg)) - self.host.bridge.call('sendMessage', (None, self.host.sendError), self.peer.full(), msg, '', 'chat', {'send_only': 'true'}) + self.host.bridge.call('sendMessage', (None, self.host.sendError), self.peer, msg, '', 'chat', {'send_only': 'true'}) def messageErrorCb(self, error): log.error('error occured: %s' % error) @@ -173,7 +173,7 @@ if status == otr.context.STATUS_AKE_INIT: return - other_jid_s = self.peer.full() + other_jid_s = self.peer feedback = _(u"Error: the state of the conversation with %s is unknown!") trust = self.getCurrentTrust() @@ -196,7 +196,7 @@ def setCurrentTrust(self, new_trust='', act='asked', type_='trust'): log.debug("setCurrentTrust: trust={trust}, act={act}, type={type}".format(type=type_, trust=new_trust, act=act)) - title = (AUTH_OTHER_TITLE if act == "asked" else AUTH_US_TITLE).format(jid=self.peer.full()) + title = (AUTH_OTHER_TITLE if act == "asked" else AUTH_US_TITLE).format(jid=self.peer) old_trust = self.getCurrentTrust() if type_ == 'abort': msg = AUTH_ABORTED_TXT @@ -236,7 +236,7 @@ self.setCurrentTrust('fingerprint' if confirm else '') text = (AUTH_INFO_TXT + "" + AUTH_FINGERPRINT_TXT + "" + AUTH_FINGERPRINT_VERIFY).format(you=self.host.whoami, your_fp=priv_key.fingerprint(), other=self.peer, other_fp=other_key.fingerprint(), eol=DIALOG_EOL) - title = AUTH_OTHER_TITLE.format(jid=self.peer.full()) + title = AUTH_OTHER_TITLE.format(jid=self.peer) dialog.ConfirmDialog(setTrust, text, title, AddStyleName="maxWidthLimit").show() def smpAuthCb(self, type_, data, act=None): @@ -261,7 +261,7 @@ # make us need the dirty self.smpAuthAbort. else: log.error("FIXME: unmanaged ambiguous 'act' value in Context.smpAuthCb!") - title = (AUTH_OTHER_TITLE if act == "asked" else AUTH_US_TITLE).format(jid=self.peer.full()) + title = (AUTH_OTHER_TITLE if act == "asked" else AUTH_US_TITLE).format(jid=self.peer) if type_ == 'question': if act == 'asked': def cb(question, answer=None): @@ -297,7 +297,7 @@ class Account(otr.context.Account): def __init__(self, host): - log.debug(u"new account: %s" % host.whoami.full()) + log.debug(u"new account: %s" % host.whoami) if not host.whoami.resource: log.warning("Account created without resource") super(Account, self).__init__(host.whoami) @@ -337,17 +337,17 @@ def getContextForUser(self, other_jid, start=True): """Get the context for the given JID - @param other_jid (JID): your correspondent + @param other_jid (jid.JID): your correspondent @param start (bool): start non-existing context if True @return: Context """ log.debug(u"getContextForUser [%s]" % other_jid) if not other_jid.resource: log.error("getContextForUser called with a bare jid") - running_sessions = [jid.bareJID() for jid in self.contexts.keys() if self.contexts[jid].state == otr.context.STATE_ENCRYPTED] + running_sessions = [jid_.bareJID() for jid_ in self.contexts.keys() if self.contexts[jid_].state == otr.context.STATE_ENCRYPTED] if start or (other_jid in running_sessions): users_ml = DIALOG_USERS_ML.format(subject=D_("OTR issue in Libervia: getContextForUser called with a bare jid in an encrypted context")) - text = RESOURCE_ISSUE.format(eol=DIALOG_EOL, jid=other_jid.full(), users_ml=users_ml) + text = RESOURCE_ISSUE.format(eol=DIALOG_EOL, jid=other_jid, users_ml=users_ml) dialog.InfoDialog(RESOURCE_ISSUE_TITLE, text, AddStyleName="maxWidthLimit").show() return None # never start an OTR session with a bare JID if start: @@ -380,7 +380,7 @@ def infoTextCallback(self, other_jid, cb): """Get the current info text for a conversation and run a callback. - @param other_jid (JID): JID of the correspondant + @param other_jid (jid.JID): JID of the correspondant @paam cb (callable): method to be called with the computed info text """ def gotResource(other_jid): @@ -414,21 +414,24 @@ for context in self.context_manager.contexts.values(): context.disconnect() - def fixResource(self, jid, cb): + def fixResource(self, jid_, cb): # FIXME: it's dirty, but libervia doesn't manage resources correctly now, refactoring is planed - if jid.resource: - self.last_resources[jid.bare] = jid.resource - cb(jid) - elif jid.bare in self.last_resources: - jid.setResource(self.last_resources[jid.bare]) - cb(jid) + if jid_.resource: + self.last_resources[jid_.bare] = jid_.resource + cb(jid_) + elif jid_.bare in self.last_resources: + # FIXME: to be removed: must use new resource system + # jid_.setResource(self.last_resources[jid_.bare]) + cb(jid_) else: - def gotResource(resource): - if resource: - jid.setResource(resource) - self.last_resources[jid.bare] = jid.resource - cb(jid) - self.host.bridge.call('getLastResource', gotResource, jid.full()) + pass # FIXME: to be removed: must use new resource system + # def gotResource(resource): + # if resource: + # jid_.setResource(resource) + # self.last_resources[jid_.bare] = jid_.resource + # cb(jid_) + # + # self.host.bridge.call('getLastResource', gotResource, jid_) def messageReceivedTrigger(self, from_jid, msg, msg_type, to_jid, extra): if msg_type == C.MESS_TYPE_INFO: @@ -441,19 +444,19 @@ def decrypt(context): context.receiveMessage(msg) - def cb(jid): - otrctx = self.context_manager.getContextForUser(jid, start=False) + def cb(jid_): + otrctx = self.context_manager.getContextForUser(jid_, start=False) if otrctx is None: def confirm(confirm): if confirm: - self.host.getOrCreateLiberviaWidget(panels.ChatPanel, {'item': jid}) - decrypt(self.context_manager.startContext(jid)) + self.host.getOrCreateLiberviaWidget(panels.ChatPanel, {'item': jid_}) + decrypt(self.context_manager.startContext(jid_)) else: # FIXME: plain text messages with whitespaces would be lost here when WHITESPACE_START_AKE is True pass key = self.context_manager.account.privkey msg = QUERY_RECEIVED + QUERY_SLOWDOWN + (QUERY_KEY if key else QUERY_NO_KEY) + QUERY_CONFIRM - dialog.ConfirmDialog(confirm, msg.format(jid=jid.full(), eol=DIALOG_EOL), QUERY_TITLE, AddStyleName="maxWidthLimit").show() + dialog.ConfirmDialog(confirm, msg.format(jid=jid_, eol=DIALOG_EOL), QUERY_TITLE, AddStyleName="maxWidthLimit").show() else: # do not ask if the context exist decrypt(otrctx) @@ -462,19 +465,19 @@ return False # interrupt the main process def sendMessageTrigger(self, to_jid, msg, msg_type, extra): - def cb(jid): - otrctx = self.context_manager.getContextForUser(jid, start=False) + def cb(jid_): + otrctx = self.context_manager.getContextForUser(jid_, start=False) if otrctx is not None and otrctx.state != otr.context.STATE_PLAINTEXT: if otrctx.state == otr.context.STATE_ENCRYPTED: log.debug(u"encrypting message") otrctx.sendMessage(msg) - self.host.newMessageCb(self.host.whoami, msg, msg_type, jid, extra) + self.host.newMessageCb(self.host.whoami, msg, msg_type, jid_, extra) else: feedback = SEND_PLAIN_IN_FINISHED_CONTEXT - dialog.InfoDialog(FINISHED_CONTEXT_TITLE.format(jid=to_jid.full()), feedback, AddStyleName="maxWidthLimit").show() + dialog.InfoDialog(FINISHED_CONTEXT_TITLE.format(jid=to_jid), feedback, AddStyleName="maxWidthLimit").show() else: log.debug(u"sending message unencrypted") - self.host.bridge.call('sendMessage', (None, self.host.sendError), to_jid.full(), msg, '', msg_type, extra) + self.host.bridge.call('sendMessage', (None, self.host.sendError), to_jid, msg, '', msg_type, extra) if msg_type == 'groupchat': return True @@ -489,14 +492,14 @@ def endSession(self, other_jid, profile, finish=False): """Finish or disconnect an OTR session - @param other_jid (JID): str + @param other_jid (jid.JID): str @param finish: if True, finish the session but do not disconnect it @return: True if the session has been finished or disconnected, False if there was nothing to do """ def cb(other_jid): def not_available(): if not finish: - self.host.newMessageCb(other_jid, END_PLAIN.format(jid=other_jid.full()), C.MESS_TYPE_INFO, self.host.whoami, {}) + self.host.newMessageCb(other_jid, END_PLAIN.format(jid=other_jid), C.MESS_TYPE_INFO, self.host.whoami, {}) priv_key = self.context_manager.account.privkey if priv_key is None: @@ -526,16 +529,16 @@ if otrctx: otrctx.sendQueryMessage() - def cb(jid): + def cb(jid_): key = self.context_manager.account.privkey if key is None: def confirm(confirm): if confirm: - query(jid) + query(jid_) msg = QUERY_SEND + QUERY_SLOWDOWN + QUERY_NO_KEY + QUERY_CONFIRM - dialog.ConfirmDialog(confirm, msg.format(jid=jid.full(), eol=DIALOG_EOL), QUERY_TITLE, AddStyleName="maxWidthLimit").show() + dialog.ConfirmDialog(confirm, msg.format(jid=jid_, eol=DIALOG_EOL), QUERY_TITLE, AddStyleName="maxWidthLimit").show() else: # on query reception we ask always, if we initiate we just ask the first time - query(jid) + query(jid_) try: other_jid = menu_data['jid'] diff -r bade589dbd5a -r a5019e62c3e9 src/browser/sat_browser/register.py --- a/src/browser/sat_browser/register.py Thu Oct 23 16:56:36 2014 +0200 +++ b/src/browser/sat_browser/register.py Sat Jan 24 01:45:39 2015 +0100 @@ -186,7 +186,7 @@ self.login_warning_msg.setVisible(True) else: self.submit_type.setValue('login') - self.submit() + self.submit(None) def onRegister(self, button): # XXX: for now libervia forces the creation to lower case @@ -203,7 +203,7 @@ else: self.register_warning_msg.setVisible(False) self.submit_type.setValue('register') - self.submit() + self.submit(None) def onSubmit(self, event): pass diff -r bade589dbd5a -r a5019e62c3e9 src/browser/sat_browser/xmlui.py --- a/src/browser/sat_browser/xmlui.py Thu Oct 23 16:56:36 2014 +0200 +++ b/src/browser/sat_browser/xmlui.py Sat Jan 24 01:45:39 2015 +0100 @@ -407,11 +407,11 @@ class XMLUIPanel(LiberviaXMLUIBase, xmlui.XMLUIPanel, VerticalPanel): widget_factory = WidgetFactory() - def __init__(self, host, parsed_xml, title=None, flags=None): + def __init__(self, host, parsed_xml, title=None, flags=None, callback=None, profile=C.PROF_KEY_NONE): self.widget_factory._xmlui_main = self VerticalPanel.__init__(self) self.setSize('100%', '100%') - xmlui.XMLUIPanel.__init__(self, host, parsed_xml, title, flags) + xmlui.XMLUIPanel.__init__(self, host, parsed_xml, title, flags, callback, profile) def setCloseCb(self, close_cb): self.close_cb = close_cb @@ -445,7 +445,7 @@ def show(self): options = ['NO_CLOSE'] if self.type == C.XMLUI_FORM else [] - _dialog = dialog.GenericDialog(self.title, self, options=options) + _dialog = dialog.GenericDialog(self.xmlui_title, self, options=options) self.setCloseCb(_dialog.close) _dialog.show() @@ -453,8 +453,8 @@ class XMLUIDialog(LiberviaXMLUIBase, xmlui.XMLUIDialog): dialog_factory = GenericFactory() - def __init__(self, host, parsed_dom, title = None, flags = None): - xmlui.XMLUIDialog.__init__(self, host, parsed_dom, title, flags) + def __init__(self, host, parsed_dom, title = None, flags = None, callback=None, profile=C.PROF_KEY_NONE): + xmlui.XMLUIDialog.__init__(self, host, parsed_dom, title, flags, callback, profile) xmlui.registerClass(xmlui.CLASS_PANEL, XMLUIPanel) xmlui.registerClass(xmlui.CLASS_DIALOG, XMLUIDialog) diff -r bade589dbd5a -r a5019e62c3e9 src/server/server.py --- a/src/server/server.py Thu Oct 23 16:56:36 2014 +0200 +++ b/src/server/server.py Sat Jan 24 01:45:39 2015 +0100 @@ -184,13 +184,6 @@ return jsonrpc.JSONRPC._cbRender(self, fault, request, parsed.get('id'), parsed.get('jsonrpc')) # pylint: disable=E1103 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) @@ -231,6 +224,11 @@ profile = ISATSession(self.session).profile return self.sat_host.bridge.getWaitingSub(profile) + def jsonrpc_getWaitingConf(self): + """Return list of waiting confirmations""" + profile = ISATSession(self.session).profile + return self.sat_host.bridge.getWaitingConf(profile) + def jsonrpc_setStatus(self, presence, status): """Change the presence and/or status @param presence: value from ("", "chat", "away", "dnd", "xa") @@ -359,10 +357,10 @@ profile = sat_session.profile sat_jid = sat_session.jid if not sat_jid: - log.error("No jid saved for this profile") - return {} + # we keep a session cache for jid to avoir jid spoofing + sat_jid = sat_session.jid = JID(self.sat_host.bridge.getParamA("JabberID", "Connection", profile_key=profile)) if JID(from_jid).userhost() != sat_jid.userhost() and JID(to_jid).userhost() != sat_jid.userhost(): - log.error("Trying to get history from a different jid, maybe a hack attempt ?") + log.error("Trying to get history from a different jid (given (browser): {}, real (backend): {}), maybe a hack attempt ?".format(from_jid, sat_jid)) return {} d = self.asyncBridgeCall("getHistory", from_jid, to_jid, size, between, search, profile) @@ -418,6 +416,11 @@ profile = ISATSession(self.session).profile return self.sat_host.bridge.getRoomsJoined(profile) + def jsonrpc_getRoomsSubjects(self): + """Return list of room subjects""" + profile = ISATSession(self.session).profile + return self.sat_host.bridge.getRoomsSubjects(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 @@ -466,7 +469,10 @@ @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) + try: + return self.sat_host.bridge.getEntityData(jid, keys, profile) + except Exception as e: + raise Failure(jsonrpclib.Fault(C.ERRNUM_BRIDGE_ERRBACK, unicode(e))) def jsonrpc_getCard(self, jid): """Get VCard for entiry @@ -761,6 +767,7 @@ request.write(C.SESSION_ACTIVE) request.finish() return + # we manage profile server side to avoid profile spoofing sat_session.profile = profile self.sat_host.prof_connected.add(profile)