core, frontends: refactoring to base Libervia on QuickFrontend (big mixed commit):

/!\ not finished, everything is still instable !
    - bridge: DBus bridge has been modified to allow blocking call to be called in the same way as asynchronous calls
    - bridge: calls with a callback and no errback are now possible, default errback log the error
    - constants: removed hack to manage presence without OrderedDict, as an OrderedDict like class has been implemented in Libervia
    - core: getLastResource has been removed and replaced by getMainResource (there is a global better management of resources)
    - various style improvments: use of constants when possible, fixed variable overlaps, import of module instead of direct class import
    - frontends: printInfo and printMessage methods in (Quick)Chat are more generic (use of extra instead of timestamp)
    - frontends: bridge creation and option parsing (command line arguments) are now specified by the frontend in QuickApp __init__
    - frontends: ProfileManager manage a more complete plug sequence (some stuff formerly manage in contact_list have moved to ProfileManager)
    - quick_frontend (quick_widgets): QuickWidgetsManager is now iterable (all widgets are then returned), or can return an iterator on a specific class (return all widgets of this class) with getWidgets
    - frontends: tools.jid can now be used in Pyjamas, with some care
    - frontends (XMLUI): profile is now managed
    - core (memory): big improvment on entities cache management (and specially resource management)
    - core (params/exceptions): added PermissionError
    - various fixes and improvments, check diff for more details
This commit is contained in:
Goffi 2015-01-24 01:00:29 +01:00
commit 8ce5c1862b
6 changed files with 1125 additions and 452 deletions

View file

@ -22,10 +22,6 @@ from sat_frontends import constants
class Const(constants.Const):
#Chats
CHAT_ONE2ONE = 'one2one'
CHAT_GROUP = 'group'
#Contact list
CONTACT_GROUPS = 'groups'
CONTACT_RESOURCES = 'resources'

View file

@ -22,11 +22,9 @@ import sys
from sat.core.log import getLogger
log = getLogger(__name__)
from sat.core import exceptions
from sat_frontends.bridge.DBus import DBusBridgeFrontend
from sat_frontends.tools import jid
from sat_frontends.quick_frontend.quick_widgets import QuickWidgetsManager
from sat_frontends.quick_frontend import quick_chat
from optparse import OptionParser
from sat_frontends.quick_frontend.constants import Const as C
@ -78,20 +76,54 @@ class ProfileManager(object):
contact_list.fill()
#The waiting subscription requests
waitingSub = self.bridge.getWaitingSub(self.profile)
for sub in waitingSub:
self.host.subscribeHandler(waitingSub[sub], sub, self.profile)
self.bridge.getWaitingSub(self.profile, callback=self._plug_profile_gotWaitingSub)
#Now we open the MUC window where we already are:
for room_args in self.bridge.getRoomsJoined(self.profile):
self.host.roomJoinedHandler(*room_args, profile=self.profile)
def _plug_profile_gotWaitingSub(self, waiting_sub):
for sub in waiting_sub:
self.host.subscribeHandler(waiting_sub[sub], sub, self.profile)
for subject_args in self.bridge.getRoomsSubjects(self.profile):
self.host.roomNewSubjectHandler(*subject_args, profile=self.profile)
self.bridge.getRoomsJoined(self.profile, callback=self._plug_profile_gotRoomsJoined)
#Finaly, we get the waiting confirmation requests
for confirm_id, confirm_type, data in self.bridge.getWaitingConf(self.profile):
self.host.askConfirmationHandler(confirm_id, confirm_type, data, self.profile)
def _plug_profile_gotRoomsJoined(self, rooms_args):
#Now we open the MUC window where we already are:
for room_args in rooms_args:
self.host.roomJoinedHandler(*room_args, profile=self.profile)
self.bridge.getRoomsSubjects(self.profile, callback=self._plug_profile_gotRoomsSubjects)
def _plug_profile_gotRoomsSubjects(self, subjects_args):
for subject_args in subjects_args:
self.host.roomNewSubjectHandler(*subject_args, profile=self.profile)
#Presence must be requested after rooms are filled
self.host.bridge.getPresenceStatuses(self.profile, callback=self._plug_profile_gotPresences)
def _plug_profile_gotPresences(self, presences):
def gotEntityData(data, contact):
for key in ('avatar', 'nick'):
if key in data:
self.host.entityDataUpdatedHandler(contact, key, data[key], self.profile)
for contact in presences:
for res in presences[contact]:
jabber_id = ('%s/%s' % (jid.JID(contact).bare, res)) if res else contact
show = presences[contact][res][0]
priority = presences[contact][res][1]
statuses = presences[contact][res][2]
self.host.presenceUpdateHandler(jabber_id, show, priority, statuses, self.profile)
self.host.bridge.getEntityData(contact, ['avatar', 'nick'], self.profile, callback=lambda data, contact=contact: gotEntityData(data, contact), errback=lambda failure, contact=contact: log.debug("No cache data for {}".format(contact)))
#Finaly, we get the waiting confirmation requests
self.bridge.getWaitingConf(self.profile, callback=self._plug_profile_gotWaitingConf)
def _plug_profile_gotWaitingConf(self, waiting_confs):
for confirm_id, confirm_type, data in waiting_confs:
self.host.askConfirmationHandler(confirm_id, confirm_type, data, self.profile)
# At this point, profile should be fully plugged
# and we launch frontend specific method
self.host.profilePlugged(self.profile)
def _getParamError(self, ignore):
log.error(_("Can't get profile parameter"))
@ -132,20 +164,27 @@ class ProfilesManager(object):
class QuickApp(object):
"""This class contain the main methods needed for the frontend"""
def __init__(self):
def __init__(self, create_bridge, check_options=None):
"""Create a frontend application
@param create_bridge: method to use to create the Bridge
@param check_options: method to call to check options (usually command line arguments)
"""
ProfileManager.host = self
self.profiles = ProfilesManager()
self.contact_lists = {}
self.widgets = QuickWidgetsManager(self)
self.check_options()
if check_options is not None:
self.options = check_options()
else:
self.options = None
# widgets
self.visible_widgets = set() # widgets currently visible (must be filled by frontend)
self.selected_widget = None # widget currently selected (must be filled by frontend)
## bridge ##
try:
self.bridge = DBusBridgeFrontend()
self.bridge = create_bridge()
except exceptions.BridgeExceptionNoService:
print(_(u"Can't connect to SàT backend, are you sure it's launched ?"))
sys.exit(1)
@ -202,6 +241,11 @@ class QuickApp(object):
except (TypeError, AttributeError):
return self.profiles.chooseOneProfile()
@property
def visible_widgets(self):
"""widgets currently visible (must be implemented by frontend)"""
raise NotImplementedError
def registerSignal(self, functionName, handler=None, iface="core", with_profile=True):
"""Register a handler for a signal
@ -211,7 +255,7 @@ class QuickApp(object):
@param with_profile (boolean): True if the signal concerns a specific profile, in that case the profile name has to be passed by the caller
"""
if handler is None:
handler = getattr(self, "%s%s" % (functionName, 'Handler'))
handler = getattr(self, "{}{}".format(functionName, 'Handler'))
if not with_profile:
self.bridge.register(functionName, handler, iface)
return
@ -236,24 +280,15 @@ class QuickApp(object):
@param profile_manager: instance of a subclass of Quick_frontend.QuickProfileManager
"""
if self.options.profile:
if self.options and self.options.profile:
profile_manager.autoconnect([self.options.profile])
def check_options(self):
"""Check command line options"""
usage = _("""
%prog [options]
def profilePlugged(self, profile):
"""Method called when the profile is fully plugged, to launch frontend specific workflow
%prog --help for options list
""")
parser = OptionParser(usage=usage) # TODO: use argparse
parser.add_option("-p", "--profile", help=_("Select the profile to use"))
(self.options, args) = parser.parse_args()
if self.options.profile:
self.options.profile = self.options.profile.decode('utf-8')
return args
@param profile(unicode): %(doc_profile)s
"""
pass
def asyncConnect(self, profile, callback=None, errback=None):
if not callback:
@ -281,7 +316,7 @@ class QuickApp(object):
will be called when profiles are choosen and are to be plugged soon
"""
raise NotImplementedError
pass
def unplug_profile(self, profile):
"""Tell the application to not follow anymore the profile"""
@ -292,6 +327,14 @@ class QuickApp(object):
def clear_profile(self):
self.profiles.clear()
def addContactList(self, profile):
"""Method to subclass to add a contact list widget
will be called on each profile session build
@return: a ContactList widget
"""
return NotImplementedError
def newWidget(self, widget):
raise NotImplementedError
@ -331,7 +374,7 @@ class QuickApp(object):
def sendMessage(self, to_jid, message, subject='', mess_type="auto", extra={}, callback=None, errback=None, profile_key=C.PROF_KEY_NONE):
if callback is None:
callback = lambda: None
callback = lambda dummy=None: None # FIXME: optional argument is here because pyjamas doesn't support callback without arg with json proxy
if errback is None:
errback = lambda failure: self.showDialog(failure.fullname, failure.message, "error")
self.bridge.sendMessage(to_jid, message, subject, mess_type, extra, profile_key, callback=callback, errback=errback)
@ -500,10 +543,9 @@ class QuickApp(object):
@profile: current profile
"""
from_jid = jid.JID(from_jid_s) if from_jid_s != C.ENTITY_ALL else C.ENTITY_ALL
for widget in self.visible_widgets:
if isinstance(widget, quick_chat.QuickChat):
if from_jid == C.ENTITY_ALL or from_jid.bare == widget.target.bare:
widget.updateChatState(from_jid, state)
for widget in self.widgets.getWidgets(quick_chat.QuickChat):
if from_jid == C.ENTITY_ALL or from_jid.bare == widget.target.bare:
widget.updateChatState(from_jid, state)
def _subscribe_cb(self, answer, data):
entity, profile = data
@ -554,8 +596,9 @@ class QuickApp(object):
self.contact_lists[profile].setCache(entity, 'nick', value)
elif key == "avatar":
if entity in self.contact_lists[profile]:
filename = self.bridge.getAvatarFile(value)
self.contact_lists[profile].setCache(entity, 'avatar', filename)
def gotFilename(filename):
self.contact_lists[profile].setCache(entity, 'avatar', filename)
self.bridge.getAvatarFile(value, callback=gotFilename)
def askConfirmationHandler(self, confirm_id, confirm_type, data, profile):
raise NotImplementedError

View file

@ -20,11 +20,18 @@
from sat.core.i18n import _
from sat.core.log import getLogger
log = getLogger(__name__)
from sat_frontends.tools.jid import JID
from sat_frontends.tools import jid
from sat_frontends.quick_frontend import quick_widgets
from sat_frontends.quick_frontend.constants import Const as C
try:
# FIXME: to be removed when an acceptable solution is here
unicode('') # XXX: unicode doesn't exist in pyjamas
except (TypeError, AttributeError): # Error raised is not the same depending on pyjsbuild options
unicode = lambda x: str(x)
class QuickChat(quick_widgets.QuickWidget):
def __init__(self, host, target, type_=C.CHAT_ONE2ONE, profiles=None):
@ -153,11 +160,11 @@ class QuickChat(quick_widgets.QuickWidget):
def onHistory(history):
for line in history:
timestamp, from_jid, to_jid, message, _type, extra = line
if ((self.type == C.CHAT_GROUP and _type != C.MESS_TYPE_GROUPCHAT) or
(self.type == C.CHAT_ONE2ONE and _type == C.MESS_TYPE_GROUPCHAT)):
timestamp, from_jid, to_jid, message, type_, extra = line # FIXME: extra is unused !
if ((self.type == C.CHAT_GROUP and type_ != C.MESS_TYPE_GROUPCHAT) or
(self.type == C.CHAT_ONE2ONE and type_ == C.MESS_TYPE_GROUPCHAT)):
continue
self.printMessage(JID(from_jid), message, profile, timestamp)
self.printMessage(jid.JID(from_jid), message, {'timestamp':timestamp}, profile)
self.afterHistoryPrint()
def onHistoryError(err):
@ -165,7 +172,7 @@ class QuickChat(quick_widgets.QuickWidget):
target = self.target.bare
return self.host.bridge.getHistory(self.host.profiles[profile].whoami.bare, target, size, search=search, profile=profile, callback=onHistory, errback=onHistoryError)
self.host.bridge.getHistory(unicode(self.host.profiles[profile].whoami.bare), unicode(target), size, True, search, profile, callback=onHistory, errback=onHistoryError)
def _get_nick(self, entity):
"""Return nick of this entity when possible"""
@ -193,29 +200,27 @@ class QuickChat(quick_widgets.QuickWidget):
chat_widget = self.getOrCreatePrivateWidget(target)
chat_widget.newMessage(from_jid, target, msg, type_, extra, profile)
else:
timestamp = extra.get('archive')
if type_ == C.MESS_TYPE_INFO:
self.printInfo(msg, timestamp=float(timestamp) if timestamp else None)
self.printInfo(msg, extra=extra)
else:
self.printMessage(from_jid, msg, profile, float(timestamp) if timestamp else None)
self.printMessage(from_jid, msg, extra, profile)
def printMessage(self, from_jid, msg, profile, timestamp=None):
def printMessage(self, from_jid, msg, extra=None, profile=C.PROF_KEY_NONE):
"""Print message in chat window. Must be implemented by child class"""
jid = JID(from_jid)
nick = self._get_nick(jid)
mymess = (jid.resource == self.nick) if self.type == C.CHAT_GROUP else (jid.bare == self.host.profiles[profile].whoami.bare) #mymess = True if message comes from local user
nick = self._get_nick(from_jid)
mymess = (from_jid.resource == self.nick) if self.type == C.CHAT_GROUP else (from_jid.bare == self.host.profiles[profile].whoami.bare) #mymess = True if message comes from local user
if msg.startswith('/me '):
self.printInfo('* %s %s' % (nick, msg[4:]), type_='me', timestamp=timestamp)
self.printInfo('* %s %s' % (nick, msg[4:]), type_='me', extra=extra)
return
return jid, nick, mymess
return nick, mymess
def printInfo(self, msg, type_='normal', timestamp=None):
def printInfo(self, msg, type_='normal', extra=None):
"""Print general info
@param msg: message to print
@type_: one of:
normal: general info like "toto has joined the room"
me: "/me" information like "/me clenches his fist" ==> "toto clenches his fist"
@param timestamp (float): number of seconds since epoch
@param extra (dict): message data
"""
raise NotImplementedError

View file

@ -22,7 +22,18 @@ from sat.core.log import getLogger
log = getLogger(__name__)
from sat_frontends.quick_frontend.quick_widgets import QuickWidget
from sat_frontends.quick_frontend.constants import Const as C
from sat_frontends.tools import jid
try:
# FIXME: to be removed when an acceptable solution is here
unicode('') # XXX: unicode doesn't exist in pyjamas
except (TypeError, AttributeError): # Error raised is not the same depending on pyjsbuild options
# XXX: pyjamas' max doesn't support key argument, so we implement it ourself
pyjamas_max = max
def max(iterable, key):
iter_cpy = list(iterable)
iter_cpy.sort(key=key)
return pyjamas_max(iter_cpy)
class QuickContactList(QuickWidget):
@ -78,18 +89,6 @@ class QuickContactList(QuickWidget):
for contact in contacts:
self.host.newContactHandler(*contact, profile=self.profile)
presences = self.host.bridge.getPresenceStatuses(self.profile)
for contact in presences:
for res in presences[contact]:
jabber_id = ('%s/%s' % (jid.JID(contact).bare, res)) if res else contact
show = presences[contact][res][0]
priority = presences[contact][res][1]
statuses = presences[contact][res][2]
self.host.presenceUpdateHandler(jabber_id, show, priority, statuses, self.profile)
data = self.host.bridge.getEntityData(contact, ['avatar', 'nick'], self.profile)
for key in ('avatar', 'nick'):
if key in data:
self.host.entityDataUpdatedHandler(contact, key, data[key], self.profile)
self.host.bridge.getContacts(self.profile, callback=gotContacts)
def update(self):
@ -105,7 +104,12 @@ class QuickContactList(QuickWidget):
- if resource is given, it is used
@param name(unicode): name the data to get, or None to get everything
"""
cache = self._cache[entity.bare]
try:
cache = self._cache[entity.bare]
except KeyError:
self.setContact(entity)
cache = self._cache[entity.bare]
if name is None:
return cache
try:
@ -280,10 +284,6 @@ class QuickContactList(QuickWidget):
set_.difference_update(to_remove)
self.update()
def add(self, jid, param_groups=None):
"""add a contact to the list"""
raise NotImplementedError
def updatePresence(self, entity, show, priority, statuses):
"""Update entity's presence status
@ -311,7 +311,6 @@ class QuickContactList(QuickWidget):
priority_resource = max(resources_data, key=lambda res: resources_data[res][C.PRESENCE_PRIORITY])
cache[C.CONTACT_MAIN_RESOURCE] = priority_resource
self.update()
def unselectAll(self):
"""Unselect all contacts"""

View file

@ -17,7 +17,9 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from sat.core.i18n import _
from os.path import exists, splitext
from optparse import OptionParser
def getNewPath(path):
""" Check if path exists, and find a non existant path if needed """
@ -31,3 +33,19 @@ def getNewPath(path):
return new_path
idx+=1
def check_options():
"""Check command line options"""
usage = _("""
%prog [options]
%prog --help for options list
""")
parser = OptionParser(usage=usage) # TODO: use argparse
parser.add_option("-p", "--profile", help=_("Select the profile to use"))
(options, args) = parser.parse_args()
if options.profile:
options.profile = options.profile.decode('utf-8')
return options

View file

@ -25,6 +25,13 @@ from sat.core import exceptions
classes_map = {}
try:
# FIXME: to be removed when an acceptable solution is here
unicode('') # XXX: unicode doesn't exist in pyjamas
except (TypeError, AttributeError): # Error raised is not the same depending on pyjsbuild options
unicode = str
def register(base_cls, child_cls=None):
"""Register a child class to use by default when a base class is needed
@ -43,6 +50,40 @@ class QuickWidgetsManager(object):
self.host = host
self._widgets = {}
def __iter__(self):
"""Iterate throught all widgets"""
for widget_map in self._widgets.itervalues():
for widget in widget_map.itervalues():
yield widget
def getRealClass(self, class_):
"""Return class registered for given class_
@param class_: subclass of QuickWidget
@return: class actually used to create widget
"""
try:
cls = classes_map[class_]
except KeyError:
cls = class_
if cls is None:
raise exceptions.InternalError("There is not class registered for {}".format(class_))
return cls
def getWidgets(self, class_):
"""Get all subclassed widgets
@param class_: subclass of QuickWidget, same parameter as used in [getOrCreateWidget]
@return: iterator on widgets
"""
class_ = self.getRealClass(class_)
try:
widgets_map = self._widgets[class_]
except KeyError:
return iter([])
else:
return widgets_map.itervalues()
def getOrCreateWidget(self, class_, target, *args, **kwargs):
"""Get an existing widget or create a new one when necessary
@ -60,13 +101,7 @@ class QuickWidgetsManager(object):
if 'force_hash' is present, the hash given in value will be used instead of the one returned by class_.getWidgetHash
@return: a class_ instance, either new or already existing
"""
# class management
try:
cls = classes_map[class_]
except KeyError:
cls = class_
if cls is None:
raise exceptions.InternalError("There is not class registered for {}".format(class_))
cls = self.getRealClass(class_)
# arguments management
_args = [self.host, target] + list(args) or [] # FIXME: check if it's really necessary to use optional args
@ -86,12 +121,12 @@ class QuickWidgetsManager(object):
hash_ = cls.getWidgetHash(target, _kwargs['profiles'])
# widget creation or retrieval
widgets_list = self._widgets.setdefault(cls, {}) # we sorts widgets by classes
widgets_map = self._widgets.setdefault(cls, {}) # we sorts widgets by classes
if not cls.SINGLE:
widget = None # if the class is not SINGLE, we always create a new widget
else:
try:
widget = widgets_list[hash_]
widget = widgets_map[hash_]
widget.addTarget(target)
except KeyError:
widget = None
@ -106,7 +141,7 @@ class QuickWidgetsManager(object):
log.debug(u"Creating new widget for target {} {}".format(target, cls))
widget = cls(*_args, **_kwargs)
widgets_list[hash_] = widget
widgets_map[hash_] = widget
if on_new_widget == 'NEW_WIDGET':
self.host.newWidget(widget)