916 lines
41 KiB
Python
916 lines
41 KiB
Python
#!/usr/bin/env python2
|
|
# -*- coding: utf-8 -*-
|
|
|
|
# helper class for making a SAT frontend
|
|
# Copyright (C) 2009-2016 Jérôme Poisson (goffi@goffi.org)
|
|
|
|
# This program is free software: you can redistribute it and/or modify
|
|
# it under the terms of the GNU Affero General Public License as published by
|
|
# the Free Software Foundation, either version 3 of the License, or
|
|
# (at your option) any later version.
|
|
|
|
# This program is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU Affero General Public License for more details.
|
|
|
|
# You should have received a copy of the GNU Affero General Public License
|
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
from sat.core.log import getLogger
|
|
log = getLogger(__name__)
|
|
|
|
from sat.core.i18n import _
|
|
from sat.core import exceptions
|
|
from sat.tools import trigger
|
|
from sat.tools.common import data_format
|
|
|
|
from sat_frontends.tools import jid
|
|
from sat_frontends.quick_frontend import quick_widgets
|
|
from sat_frontends.quick_frontend import quick_menus
|
|
from sat_frontends.quick_frontend import quick_blog
|
|
from sat_frontends.quick_frontend import quick_chat, quick_games
|
|
from sat_frontends.quick_frontend import quick_contact_list
|
|
from sat_frontends.quick_frontend.constants import Const as C
|
|
|
|
import sys
|
|
from collections import OrderedDict
|
|
import time
|
|
|
|
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
|
|
|
|
|
|
class ProfileManager(object):
|
|
"""Class managing all data relative to one profile, and plugging in mechanism"""
|
|
# TODO: handle waiting XMLUI requests: getWaitingConf doesn't exist anymore
|
|
# and a way to keep some XMLUI request between sessions is expected in backend
|
|
host = None
|
|
bridge = None
|
|
cache_keys_to_get = ['avatar']
|
|
|
|
def __init__(self, profile):
|
|
self.profile = profile
|
|
self.connected = False
|
|
self.whoami = None
|
|
self.notifications = {} # key: bare jid or '' for general, value: notif data
|
|
|
|
@property
|
|
def autodisconnect(self):
|
|
try:
|
|
autodisconnect = self._autodisconnect
|
|
except AttributeError:
|
|
autodisconnect = False
|
|
return autodisconnect
|
|
|
|
def plug(self):
|
|
"""Plug the profile to the host"""
|
|
# we get the essential params
|
|
self.bridge.asyncGetParamA("JabberID", "Connection", profile_key=self.profile,
|
|
callback=self._plug_profile_jid, errback=self._getParamError)
|
|
|
|
def _plug_profile_jid(self, jid_s):
|
|
self.whoami = jid.JID(jid_s) # resource might change after the connection
|
|
self.bridge.isConnected(self.profile, callback=self._plug_profile_isconnected)
|
|
|
|
def _plug_profile_isconnected(self, connected):
|
|
self.connected = connected
|
|
self.bridge.asyncGetParamA("autodisconnect", "Connection", profile_key=self.profile,
|
|
callback=self._plug_profile_autodisconnect, errback=self._getParamError)
|
|
|
|
def _plug_profile_autodisconnect(self, autodisconnect):
|
|
if C.bool(autodisconnect):
|
|
self._autodisconnect = True
|
|
self.bridge.asyncGetParamA("autoconnect", "Connection", profile_key=self.profile,
|
|
callback=self._plug_profile_autoconnect, errback=self._getParamError)
|
|
|
|
def _plug_profile_autoconnect(self, value_str):
|
|
autoconnect = C.bool(value_str)
|
|
if autoconnect and not self.connected:
|
|
self.host.asyncConnect(self.profile, callback=lambda dummy: self._plug_profile_afterconnect())
|
|
else:
|
|
self._plug_profile_afterconnect()
|
|
|
|
def _plug_profile_afterconnect(self):
|
|
# Profile can be connected or not
|
|
# we get cached data
|
|
self.connected = True
|
|
self.host.bridge.getFeatures(profile_key=self.profile, callback=self._plug_profile_getFeaturesCb, errback=self._plug_profile_getFeaturesEb)
|
|
|
|
def _plug_profile_getFeaturesEb(self, failure):
|
|
log.error(u"Couldn't get features: {}".format(failure))
|
|
self._plug_profile_getFeaturesCb({})
|
|
|
|
def _plug_profile_getFeaturesCb(self, features):
|
|
self.host.features = features
|
|
self.host.bridge.getEntitiesData([], ProfileManager.cache_keys_to_get, profile=self.profile, callback=self._plug_profile_gotCachedValues, errback=self._plug_profile_failedCachedValues)
|
|
|
|
def _plug_profile_failedCachedValues(self, failure):
|
|
log.error(u"Couldn't get cached values: {}".format(failure))
|
|
self._plug_profile_gotCachedValues({})
|
|
|
|
def _plug_profile_gotCachedValues(self, cached_values):
|
|
# add the contact list and its listener
|
|
contact_list = self.host.contact_lists.addProfile(self.profile)
|
|
|
|
for entity_s, data in cached_values.iteritems():
|
|
for key, value in data.iteritems():
|
|
self.host.entityDataUpdatedHandler(entity_s, key, value, self.profile)
|
|
|
|
if not self.connected:
|
|
self.host.setPresenceStatus(C.PRESENCE_UNAVAILABLE, '', profile=self.profile)
|
|
else:
|
|
|
|
contact_list.fill()
|
|
self.host.setPresenceStatus(profile=self.profile)
|
|
|
|
#The waiting subscription requests
|
|
self.bridge.getWaitingSub(self.profile, callback=self._plug_profile_gotWaitingSub)
|
|
|
|
def _plug_profile_gotWaitingSub(self, waiting_sub):
|
|
for sub in waiting_sub:
|
|
self.host.subscribeHandler(waiting_sub[sub], sub, self.profile)
|
|
|
|
self.bridge.mucGetRoomsJoined(self.profile, callback=self._plug_profile_gotRoomsJoined)
|
|
|
|
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.mucRoomJoinedHandler(*room_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 = (u'%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(u"No cache data for {}".format(contact)))
|
|
|
|
# At this point, profile should be fully plugged
|
|
# and we launch frontend specific method
|
|
self.host.profilePlugged(self.profile)
|
|
|
|
def _getParamError(self, failure):
|
|
log.error(_("Can't get profile parameter: {msg}").format(msg=failure))
|
|
|
|
|
|
class ProfilesManager(object):
|
|
"""Class managing collection of profiles"""
|
|
|
|
def __init__(self):
|
|
self._profiles = {}
|
|
|
|
def __contains__(self, profile):
|
|
return profile in self._profiles
|
|
|
|
def __iter__(self):
|
|
return self._profiles.iterkeys()
|
|
|
|
def __getitem__(self, profile):
|
|
return self._profiles[profile]
|
|
|
|
def __len__(self):
|
|
return len(self._profiles)
|
|
|
|
def iteritems(self):
|
|
return self._profiles.iteritems()
|
|
|
|
def plug(self, profile):
|
|
if profile in self._profiles:
|
|
raise exceptions.ConflictError('A profile of the name [{}] is already plugged'.format(profile))
|
|
self._profiles[profile] = ProfileManager(profile)
|
|
self._profiles[profile].plug()
|
|
|
|
def unplug(self, profile):
|
|
if profile not in self._profiles:
|
|
raise ValueError('The profile [{}] is not plugged'.format(profile))
|
|
|
|
# remove the contact list and its listener
|
|
host = self._profiles[profile].host
|
|
host.contact_lists[profile].unplug()
|
|
|
|
del self._profiles[profile]
|
|
|
|
def chooseOneProfile(self):
|
|
return self._profiles.keys()[0]
|
|
|
|
|
|
class QuickApp(object):
|
|
"""This class contain the main methods needed for the frontend"""
|
|
MB_HANDLER = True # Set to False if the frontend doesn't manage microblog
|
|
AVATARS_HANDLER = True # set to False if avatars are not used
|
|
|
|
def __init__(self, bridge_factory, xmlui, check_options=None, connect_bridge=True):
|
|
"""Create a frontend application
|
|
|
|
@param bridge_factory: method to use to create the Bridge
|
|
@param xmlui: xmlui module
|
|
@param check_options: method to call to check options (usually command line arguments)
|
|
"""
|
|
self.xmlui = xmlui
|
|
self.menus = quick_menus.QuickMenusManager(self)
|
|
ProfileManager.host = self
|
|
self.profiles = ProfilesManager()
|
|
self._plugs_in_progress = set() # profiles currently being plugged, used to (un)lock contact list updates
|
|
self.ready_profiles = set() # profiles which are connected and ready
|
|
self.signals_cache = {} # used to keep signal received between start of plug_profile and when the profile is actualy ready
|
|
self.contact_lists = quick_contact_list.QuickContactListHandler(self)
|
|
self.widgets = quick_widgets.QuickWidgetsManager(self)
|
|
if check_options is not None:
|
|
self.options = check_options()
|
|
else:
|
|
self.options = None
|
|
|
|
# widgets
|
|
self.selected_widget = None # widget currently selected (must be filled by frontend)
|
|
|
|
# listeners
|
|
self._listeners = {} # key: listener type ("avatar", "selected", etc), value: list of callbacks
|
|
|
|
# triggers
|
|
self.trigger = trigger.TriggerManager() # trigger are used to change the default behaviour
|
|
|
|
## bridge ##
|
|
self.bridge = bridge_factory()
|
|
ProfileManager.bridge = self.bridge
|
|
if connect_bridge:
|
|
self.connectBridge()
|
|
|
|
self._notif_id = 0
|
|
self._notifications = OrderedDict()
|
|
self.features = None
|
|
|
|
def connectBridge(self):
|
|
self.bridge.bridgeConnect(callback=self._bridgeCb, errback=self._bridgeEb)
|
|
|
|
def onBridgeConnected(self):
|
|
pass
|
|
|
|
def _bridgeCb(self):
|
|
self.registerSignal("connected")
|
|
self.registerSignal("disconnected")
|
|
self.registerSignal("actionNew")
|
|
self.registerSignal("newContact")
|
|
self.registerSignal("messageNew")
|
|
self.registerSignal("presenceUpdate")
|
|
self.registerSignal("subscribe")
|
|
self.registerSignal("paramUpdate")
|
|
self.registerSignal("contactDeleted")
|
|
self.registerSignal("entityDataUpdated")
|
|
self.registerSignal("progressStarted")
|
|
self.registerSignal("progressFinished")
|
|
self.registerSignal("progressError")
|
|
self.registerSignal("mucRoomJoined", iface="plugin")
|
|
self.registerSignal("mucRoomLeft", iface="plugin")
|
|
self.registerSignal("mucRoomUserChangedNick", iface="plugin")
|
|
self.registerSignal("mucRoomNewSubject", iface="plugin")
|
|
self.registerSignal("chatStateReceived", iface="plugin")
|
|
self.registerSignal("messageState", iface="plugin")
|
|
self.registerSignal("psEvent", iface="plugin")
|
|
|
|
# FIXME: do it dynamically
|
|
quick_games.Tarot.registerSignals(self)
|
|
quick_games.Quiz.registerSignals(self)
|
|
quick_games.Radiocol.registerSignals(self)
|
|
self.onBridgeConnected()
|
|
|
|
def _bridgeEb(self, failure):
|
|
if isinstance(failure, exceptions.BridgeExceptionNoService):
|
|
print(_(u"Can't connect to SàT backend, are you sure it's launched ?"))
|
|
sys.exit(1)
|
|
elif isinstance(failure, exceptions.BridgeInitError):
|
|
print(_(u"Can't init bridge"))
|
|
sys.exit(1)
|
|
else:
|
|
print(_(u"Error while initialising bridge: {}".format(failure)))
|
|
|
|
@property
|
|
def current_profile(self):
|
|
"""Profile that a user would expect to use"""
|
|
try:
|
|
return self.selected_widget.profile
|
|
except (TypeError, AttributeError):
|
|
return self.profiles.chooseOneProfile()
|
|
|
|
@property
|
|
def visible_widgets(self):
|
|
"""widgets currently visible (must be implemented by frontend)
|
|
|
|
@return (iter[QuickWidget]): iterable on visible widgets
|
|
"""
|
|
raise NotImplementedError
|
|
|
|
def registerSignal(self, function_name, handler=None, iface="core", with_profile=True):
|
|
"""Register a handler for a signal
|
|
|
|
@param function_name (str): name of the signal to handle
|
|
@param handler (instancemethod): method to call when the signal arrive, None for calling an automatically named handler (function_name + 'Handler')
|
|
@param iface (str): interface of the bridge to use ('core' or 'plugin')
|
|
@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, "{}{}".format(function_name, 'Handler'))
|
|
if not with_profile:
|
|
self.bridge.register_signal(function_name, handler, iface)
|
|
return
|
|
|
|
def signalReceived(*args, **kwargs):
|
|
profile = kwargs.get('profile')
|
|
if profile is None:
|
|
if not args:
|
|
raise exceptions.ProfileNotSetError
|
|
profile = args[-1]
|
|
if profile is not None:
|
|
if not self.check_profile(profile):
|
|
if profile in self.profiles:
|
|
# profile is not ready but is in self.profiles, that's mean that it's being connecting and we need to cache the signal
|
|
self.signals_cache.setdefault(profile, []).append((function_name, handler, args, kwargs))
|
|
return # we ignore signal for profiles we don't manage
|
|
handler(*args, **kwargs)
|
|
self.bridge.register_signal(function_name, signalReceived, iface)
|
|
|
|
def addListener(self, type_, callback, profiles_filter=None):
|
|
"""Add a listener for an event
|
|
|
|
/!\ don't forget to remove listener when not used anymore (e.g. if you delete a widget)
|
|
@param type_: type of event, can be:
|
|
- avatar: called when avatar data is updated
|
|
args: (entity, avatar file, profile)
|
|
- nick: called when nick data is updated
|
|
args: (entity, new_nick, profile)
|
|
- presence: called when a presence is received
|
|
args: (entity, show, priority, statuses, profile)
|
|
- notification: called when a new notification is emited
|
|
args: (entity, notification_data, profile)
|
|
- notification_clear: called when notifications are cleared
|
|
args: (entity, type_, profile)
|
|
- menu: called when a menu item is added or removed
|
|
args: (type_, path, path_i18n, item) were values are:
|
|
type_: same as in [sat.core.sat_main.SAT.importMenu]
|
|
path: same as in [sat.core.sat_main.SAT.importMenu]
|
|
path_i18n: translated path (or None if the item is removed)
|
|
item: instance of quick_menus.MenuItemBase or None if the item is removed
|
|
- gotMenus: called only once when menu are available (no arg)
|
|
@param callback: method to call on event
|
|
@param profiles_filter (set[unicode]): if set and not empty, the
|
|
listener will be callable only by one of the given profiles.
|
|
"""
|
|
assert type_ in C.LISTENERS
|
|
self._listeners.setdefault(type_, OrderedDict())[callback] = profiles_filter
|
|
|
|
def removeListener(self, type_, callback):
|
|
"""Remove a callback from listeners
|
|
|
|
@param type_: same as for [addListener]
|
|
@param callback: callback to remove
|
|
"""
|
|
assert type_ in C.LISTENERS
|
|
self._listeners[type_].pop(callback)
|
|
|
|
def callListeners(self, type_, *args, **kwargs):
|
|
"""Call the methods which listen type_ event. If a profiles filter has
|
|
been register with a listener and profile argument is not None, the
|
|
listener will be called only if profile is in the profiles filter list.
|
|
|
|
@param type_: same as for [addListener]
|
|
@param *args: arguments sent to callback
|
|
@param **kwargs: keywords argument, mainly used to pass "profile" when needed
|
|
"""
|
|
assert type_ in C.LISTENERS
|
|
try:
|
|
listeners = self._listeners[type_]
|
|
except KeyError:
|
|
pass
|
|
else:
|
|
profile = kwargs.get("profile")
|
|
for listener, profiles_filter in listeners.iteritems():
|
|
if profile is None or not profiles_filter or profile in profiles_filter:
|
|
listener(*args, **kwargs)
|
|
|
|
def check_profile(self, profile):
|
|
"""Tell if the profile is currently followed by the application, and ready"""
|
|
return profile in self.ready_profiles
|
|
|
|
def postInit(self, profile_manager):
|
|
"""Must be called after initialization is done, do all automatic task (auto plug profile)
|
|
|
|
@param profile_manager: instance of a subclass of Quick_frontend.QuickProfileManager
|
|
"""
|
|
if self.options and self.options.profile:
|
|
profile_manager.autoconnect([self.options.profile])
|
|
|
|
def profilePlugged(self, profile):
|
|
"""Method called when the profile is fully plugged, to launch frontend specific workflow
|
|
|
|
/!\ if you override the method and don't call the parent, be sure to add the profile to ready_profiles !
|
|
if you don't, all signals will stay in cache
|
|
|
|
@param profile(unicode): %(doc_profile)s
|
|
"""
|
|
self._plugs_in_progress.remove(profile)
|
|
self.ready_profiles.add(profile)
|
|
|
|
# profile is ready, we can call send signals that where is cache
|
|
cached_signals = self.signals_cache.pop(profile, [])
|
|
for function_name, handler, args, kwargs in cached_signals:
|
|
log.debug(u"Calling cached signal [%s] with args %s and kwargs %s" % (function_name, args, kwargs))
|
|
handler(*args, **kwargs)
|
|
|
|
self.callListeners('profilePlugged', profile=profile)
|
|
if not self._plugs_in_progress:
|
|
self.contact_lists.lockUpdate(False)
|
|
|
|
def asyncConnect(self, profile, callback=None, errback=None):
|
|
if not callback:
|
|
callback = lambda dummy: None
|
|
if not errback:
|
|
def errback(failure):
|
|
log.error(_(u"Can't connect profile [%s]") % failure)
|
|
try:
|
|
module = failure.module
|
|
except AttributeError:
|
|
module = ''
|
|
try:
|
|
message = failure.message
|
|
except AttributeError:
|
|
message = 'error'
|
|
try:
|
|
fullname = failure.fullname
|
|
except AttributeError:
|
|
fullname = 'error'
|
|
if module.startswith('twisted.words.protocols.jabber') and failure.condition == "not-authorized":
|
|
self.launchAction(C.CHANGE_XMPP_PASSWD_ID, {}, profile=profile)
|
|
else:
|
|
self.showDialog(message, fullname, 'error')
|
|
self.bridge.asyncConnect(profile, callback=callback, errback=errback)
|
|
|
|
def plug_profiles(self, profiles):
|
|
"""Tell application which profiles must be used
|
|
|
|
@param profiles: list of valid profile names
|
|
"""
|
|
self.contact_lists.lockUpdate()
|
|
self._plugs_in_progress.update(profiles)
|
|
self.plugging_profiles()
|
|
for profile in profiles:
|
|
self.profiles.plug(profile)
|
|
|
|
def plugging_profiles(self):
|
|
"""Method to subclass to manage frontend specific things to do
|
|
|
|
will be called when profiles are choosen and are to be plugged soon
|
|
"""
|
|
pass
|
|
|
|
def unplug_profile(self, profile):
|
|
"""Tell the application to not follow anymore the profile"""
|
|
if not profile in self.profiles:
|
|
raise ValueError("The profile [{}] is not plugged".format(profile))
|
|
self.profiles.unplug(profile)
|
|
|
|
def clear_profile(self):
|
|
self.profiles.clear()
|
|
|
|
def newWidget(self, widget):
|
|
raise NotImplementedError
|
|
|
|
def connectedHandler(self, profile, jid_s):
|
|
"""Called when the connection is made.
|
|
|
|
@param jid_s (unicode): the JID that we were assigned by the server,
|
|
as the resource might differ from the JID we asked for.
|
|
"""
|
|
log.debug(_("Connected"))
|
|
self.profiles[profile].whoami = jid.JID(jid_s)
|
|
self.setPresenceStatus(profile=profile)
|
|
self.contact_lists[profile].fill()
|
|
|
|
def disconnectedHandler(self, profile):
|
|
"""called when the connection is closed"""
|
|
log.debug(_("Disconnected"))
|
|
self.contact_lists[profile].disconnect()
|
|
self.setPresenceStatus(C.PRESENCE_UNAVAILABLE, '', profile=profile)
|
|
|
|
def actionNewHandler(self, action_data, id_, security_limit, profile):
|
|
self.actionManager(action_data, user_action=False, profile=profile)
|
|
|
|
def newContactHandler(self, jid_s, attributes, groups, profile):
|
|
entity = jid.JID(jid_s)
|
|
groups = list(groups)
|
|
self.contact_lists[profile].setContact(entity, groups, attributes, in_roster=True)
|
|
|
|
def messageNewHandler(self, uid, timestamp, from_jid_s, to_jid_s, msg, subject, type_, extra, profile):
|
|
from_jid = jid.JID(from_jid_s)
|
|
to_jid = jid.JID(to_jid_s)
|
|
if not self.trigger.point("messageNewTrigger", uid, timestamp, from_jid, to_jid, msg, subject, type_, extra, profile=profile):
|
|
return
|
|
|
|
from_me = from_jid.bare == self.profiles[profile].whoami.bare
|
|
target = to_jid if from_me else from_jid
|
|
contact_list = self.contact_lists[profile]
|
|
# we want to be sure to have at least one QuickChat instance
|
|
self.widgets.getOrCreateWidget(quick_chat.QuickChat, target, type_=C.CHAT_ONE2ONE, on_new_widget=None, profile=profile)
|
|
|
|
if not from_jid in contact_list and from_jid.bare != self.profiles[profile].whoami.bare:
|
|
#XXX: needed to show entities which haven't sent any
|
|
# presence information and which are not in roster
|
|
contact_list.setContact(from_jid)
|
|
|
|
# we dispatch the message in the widgets
|
|
for widget in self.widgets.getWidgets(quick_chat.QuickChat, target=target, profiles=(profile,)):
|
|
widget.messageNew(uid, timestamp, from_jid, target, msg, subject, type_, extra, profile)
|
|
|
|
def messageStateHandler(self, uid, status, profile):
|
|
for widget in self.widgets.getWidgets(quick_chat.QuickChat, profiles=(profile,)):
|
|
widget.onMessageState(uid, status, profile)
|
|
|
|
def messageSend(self, to_jid, message, subject=None, mess_type="auto", extra=None, callback=None, errback=None, profile_key=C.PROF_KEY_NONE):
|
|
if subject is None:
|
|
subject = {}
|
|
if extra is None:
|
|
extra = {}
|
|
if callback is 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")
|
|
|
|
if not self.trigger.point("messageSendTrigger", to_jid, message, subject, mess_type, extra, callback, errback, profile_key=profile_key):
|
|
return
|
|
|
|
self.bridge.messageSend(unicode(to_jid), message, subject, mess_type, extra, profile_key, callback=callback, errback=errback)
|
|
|
|
def setPresenceStatus(self, show='', status=None, profile=C.PROF_KEY_NONE):
|
|
raise NotImplementedError
|
|
|
|
def presenceUpdateHandler(self, entity_s, show, priority, statuses, profile):
|
|
|
|
log.debug(_(u"presence update for %(entity)s (show=%(show)s, priority=%(priority)s, statuses=%(statuses)s) [profile:%(profile)s]")
|
|
% {'entity': entity_s, C.PRESENCE_SHOW: show, C.PRESENCE_PRIORITY: priority, C.PRESENCE_STATUSES: statuses, 'profile': profile})
|
|
entity = jid.JID(entity_s)
|
|
|
|
if entity == self.profiles[profile].whoami:
|
|
if show == C.PRESENCE_UNAVAILABLE:
|
|
self.setPresenceStatus(C.PRESENCE_UNAVAILABLE, '', profile=profile)
|
|
else:
|
|
# FIXME: try to retrieve user language status before fallback to default
|
|
status = statuses.get(C.PRESENCE_STATUSES_DEFAULT, None)
|
|
self.setPresenceStatus(show, status, profile=profile)
|
|
return
|
|
|
|
self.callListeners('presence', entity, show, priority, statuses, profile=profile)
|
|
|
|
def mucRoomJoinedHandler(self, room_jid_s, occupants, user_nick, subject, profile):
|
|
"""Called when a MUC room is joined"""
|
|
log.debug(u"Room [{room_jid}] joined by {profile}, users presents:{users}".format(room_jid=room_jid_s, profile=profile, users=occupants.keys()))
|
|
room_jid = jid.JID(room_jid_s)
|
|
self.widgets.getOrCreateWidget(quick_chat.QuickChat, room_jid, type_=C.CHAT_GROUP, nick=user_nick, occupants=occupants, subject=subject, profile=profile)
|
|
self.contact_lists[profile].setSpecial(room_jid, C.CONTACT_SPECIAL_GROUP)
|
|
# chat_widget.update()
|
|
|
|
def mucRoomLeftHandler(self, room_jid_s, profile):
|
|
"""Called when a MUC room is left"""
|
|
log.debug(u"Room [%(room_jid)s] left by %(profile)s" % {'room_jid': room_jid_s, 'profile': profile})
|
|
room_jid = jid.JID(room_jid_s)
|
|
chat_widget = self.widgets.getWidget(quick_chat.QuickChat, room_jid, profile)
|
|
if chat_widget:
|
|
self.widgets.deleteWidget(chat_widget)
|
|
self.contact_lists[profile].removeContact(room_jid)
|
|
|
|
def mucRoomUserChangedNickHandler(self, room_jid_s, old_nick, new_nick, profile):
|
|
"""Called when an user joined a MUC room"""
|
|
room_jid = jid.JID(room_jid_s)
|
|
chat_widget = self.widgets.getOrCreateWidget(quick_chat.QuickChat, room_jid, type_=C.CHAT_GROUP, profile=profile)
|
|
chat_widget.changeUserNick(old_nick, new_nick)
|
|
log.debug(u"user [%(old_nick)s] is now known as [%(new_nick)s] in room [%(room_jid)s]" % {'old_nick': old_nick, 'new_nick': new_nick, 'room_jid': room_jid})
|
|
|
|
def mucRoomNewSubjectHandler(self, room_jid_s, subject, profile):
|
|
"""Called when subject of MUC room change"""
|
|
room_jid = jid.JID(room_jid_s)
|
|
chat_widget = self.widgets.getOrCreateWidget(quick_chat.QuickChat, room_jid, type_=C.CHAT_GROUP, profile=profile)
|
|
chat_widget.setSubject(subject)
|
|
log.debug(u"new subject for room [%(room_jid)s]: %(subject)s" % {'room_jid': room_jid, "subject": subject})
|
|
|
|
def chatStateReceivedHandler(self, from_jid_s, state, profile):
|
|
"""Called when a new chat state (XEP-0085) is received.
|
|
|
|
@param from_jid_s (unicode): JID of a contact or C.ENTITY_ALL
|
|
@param state (unicode): new state
|
|
@param profile (unicode): current profile
|
|
"""
|
|
from_jid = jid.JID(from_jid_s)
|
|
for widget in self.widgets.getWidgets(quick_chat.QuickChat, profiles=(profile,)):
|
|
widget.onChatState(from_jid, state, profile)
|
|
|
|
def notify(self, type_, entity=None, message=None, subject=None, callback=None, cb_args=None, widget=None, profile=C.PROF_KEY_NONE):
|
|
"""Trigger an event notification
|
|
|
|
@param type_(unicode): notifation kind,
|
|
one of C.NOTIFY_* constant or any custom type specific to frontend
|
|
@param entity(jid.JID, None): entity involved in the notification
|
|
if entity is in contact list, a indicator may be added in front of it
|
|
@param message(unicode, None): message of the notification
|
|
@param subject(unicode, None): subject of the notification
|
|
@param callback(callable, None): method to call when notification is selected
|
|
@param cb_args(list, None): list of args for callback
|
|
@param widget(object, None): widget where the notification happened
|
|
"""
|
|
notif_dict = self.profiles[profile].notifications
|
|
key = '' if entity is None else entity.bare
|
|
type_notifs = notif_dict.setdefault(key, {}).setdefault(type_, [])
|
|
notif_data = {
|
|
'id': self._notif_id,
|
|
'time': time.time(),
|
|
'entity': entity,
|
|
'callback': callback,
|
|
'cb_args': cb_args,
|
|
'message': message,
|
|
'subject': subject,
|
|
}
|
|
if widget is not None:
|
|
notif_data[widget] = widget
|
|
type_notifs.append(notif_data)
|
|
self._notifications[self._notif_id] = notif_data
|
|
self.callListeners('notification', entity, notif_data, profile=profile)
|
|
|
|
def getNotifs(self, entity=None, type_=None, exact_jid=None, profile=C.PROF_KEY_NONE):
|
|
"""return notifications for given entity
|
|
|
|
@param entity(jid.JID, None, C.ENTITY_ALL): jid of the entity to check
|
|
bare jid to get all notifications, full jid to filter on resource
|
|
None to get general notifications
|
|
C.ENTITY_ALL to get all notifications
|
|
@param type_(unicode, None): notification type to filter
|
|
None to get all notifications
|
|
@param exact_jid(bool, None): if True, only return notifications from
|
|
exact entity jid (i.e. not including other resources)
|
|
None for automatic selection (True for full jid, False else)
|
|
False to get resources notifications
|
|
False doesn't do anything if entity is not a bare jid
|
|
@return (iter[dict]): notifications
|
|
"""
|
|
main_notif_dict = self.profiles[profile].notifications
|
|
|
|
if entity is C.ENTITY_ALL:
|
|
selected_notifs = main_notif_dict.itervalues()
|
|
exact_jid = False
|
|
else:
|
|
if entity is None:
|
|
key = ''
|
|
exact_jid = False
|
|
else:
|
|
key = entity.bare
|
|
if exact_jid is None:
|
|
exact_jid = bool(entity.resource)
|
|
selected_notifs = (main_notif_dict.setdefault(key, {}),)
|
|
|
|
for notifs_from_select in selected_notifs:
|
|
|
|
if type_ is None:
|
|
type_notifs = notifs_from_select.itervalues()
|
|
else:
|
|
type_notifs = (notifs_from_select.get(type_, []),)
|
|
|
|
for notifs in type_notifs:
|
|
for notif in notifs:
|
|
if exact_jid and notif['entity'] != entity:
|
|
continue
|
|
yield notif
|
|
|
|
def clearNotifs(self, entity, type_=None, profile=C.PROF_KEY_NONE):
|
|
"""return notifications for given entity
|
|
|
|
@param entity(jid.JID, None): bare jid of the entity to check
|
|
None to clear general notifications (but keep entities ones)
|
|
@param type_(unicode, None): notification type to filter
|
|
None to clear all notifications
|
|
@return (list[dict]): list of notifications
|
|
"""
|
|
notif_dict = self.profiles[profile].notifications
|
|
key = '' if entity is None else entity.bare
|
|
try:
|
|
if type_ is None:
|
|
del notif_dict[key]
|
|
else:
|
|
del notif_dict[key][type_]
|
|
except KeyError:
|
|
return
|
|
self.callListeners('notificationsClear', entity, type_, profile=profile)
|
|
|
|
def psEventHandler(self, category, service_s, node, event_type, data, profile):
|
|
"""Called when a PubSub event is received.
|
|
|
|
@param category(unicode): event category (e.g. "PEP", "MICROBLOG")
|
|
@param service_s (unicode): pubsub service
|
|
@param node (unicode): pubsub node
|
|
@param event_type (unicode): event type (one of C.PUBLISH, C.RETRACT, C.DELETE)
|
|
@param data (dict): event data
|
|
"""
|
|
service_s = jid.JID(service_s)
|
|
|
|
if category == C.PS_MICROBLOG and self.MB_HANDLER:
|
|
if event_type == C.PS_PUBLISH:
|
|
if not 'content' in data:
|
|
log.warning("No content found in microblog data")
|
|
return
|
|
_groups = set(data_format.dict2iter('group', data)) or None # FIXME: check if [] make sense (instead of None)
|
|
|
|
for wid in self.widgets.getWidgets(quick_blog.QuickBlog):
|
|
wid.addEntryIfAccepted(service_s, node, data, _groups, profile)
|
|
|
|
try:
|
|
comments_node, comments_service = data['comments_node'], data['comments_service']
|
|
except KeyError:
|
|
pass
|
|
else:
|
|
self.bridge.mbGet(comments_service, comments_node, C.NO_LIMIT, [], {"subscribe":C.BOOL_TRUE}, profile=profile)
|
|
elif event_type == C.PS_RETRACT:
|
|
for wid in self.widgets.getWidgets(quick_blog.QuickBlog):
|
|
wid.deleteEntryIfPresent(service_s, node, data['id'], profile)
|
|
pass
|
|
else:
|
|
log.warning("Unmanaged PubSub event type {}".format(event_type))
|
|
|
|
def progressStartedHandler(self, pid, metadata, profile):
|
|
log.info(u"Progress {} started".format(pid))
|
|
|
|
def progressFinishedHandler(self, pid, metadata, profile):
|
|
log.info(u"Progress {} finished".format(pid))
|
|
|
|
def progressErrorHandler(self, pid, err_msg, profile):
|
|
log.warning(u"Progress {pid} error: {err_msg}".format(pid=pid, err_msg=err_msg))
|
|
|
|
def _subscribe_cb(self, answer, data):
|
|
entity, profile = data
|
|
type_ = "subscribed" if answer else "unsubscribed"
|
|
self.bridge.subscription(type_, unicode(entity.bare), profile_key=profile)
|
|
|
|
def subscribeHandler(self, type, raw_jid, profile):
|
|
"""Called when a subsciption management signal is received"""
|
|
entity = jid.JID(raw_jid)
|
|
if type == "subscribed":
|
|
# this is a subscription confirmation, we just have to inform user
|
|
# TODO: call self.getEntityMBlog to add the new contact blogs
|
|
self.showDialog(_("The contact %s has accepted your subscription") % entity.bare, _('Subscription confirmation'))
|
|
elif type == "unsubscribed":
|
|
# this is a subscription refusal, we just have to inform user
|
|
self.showDialog(_("The contact %s has refused your subscription") % entity.bare, _('Subscription refusal'), 'error')
|
|
elif type == "subscribe":
|
|
# this is a subscriptionn request, we have to ask for user confirmation
|
|
# TODO: use sat.stdui.ui_contact_list to display the groups selector
|
|
self.showDialog(_("The contact %s wants to subscribe to your presence.\nDo you accept ?") % entity.bare, _('Subscription confirmation'), 'yes/no', answer_cb=self._subscribe_cb, answer_data=(entity, profile))
|
|
|
|
def showDialog(self, message, title, type="info", answer_cb=None, answer_data=None):
|
|
raise NotImplementedError
|
|
|
|
def showAlert(self, message):
|
|
pass #FIXME
|
|
|
|
def dialogFailure(self, failure):
|
|
log.warning(u"Failure: {}".format(failure))
|
|
|
|
def progressIdHandler(self, progress_id, profile):
|
|
"""Callback used when an action result in a progress id"""
|
|
log.info(u"Progress ID received: {}".format(progress_id))
|
|
|
|
def isHidden(self):
|
|
"""Tells if the frontend window is hidden.
|
|
|
|
@return bool
|
|
"""
|
|
raise NotImplementedError
|
|
|
|
def paramUpdateHandler(self, name, value, namespace, profile):
|
|
log.debug(_(u"param update: [%(namespace)s] %(name)s = %(value)s") % {'namespace': namespace, 'name': name, 'value': value})
|
|
if (namespace, name) == ("Connection", "JabberID"):
|
|
log.debug(_(u"Changing JID to %s") % value)
|
|
self.profiles[profile].whoami = jid.JID(value)
|
|
elif (namespace, name) == ('General', C.SHOW_OFFLINE_CONTACTS):
|
|
self.contact_lists[profile].showOfflineContacts(C.bool(value))
|
|
elif (namespace, name) == ('General', C.SHOW_EMPTY_GROUPS):
|
|
self.contact_lists[profile].showEmptyGroups(C.bool(value))
|
|
|
|
def contactDeletedHandler(self, jid_s, profile):
|
|
target = jid.JID(jid_s)
|
|
self.contact_lists[profile].removeContact(target)
|
|
|
|
def entityDataUpdatedHandler(self, entity_s, key, value, profile):
|
|
entity = jid.JID(entity_s)
|
|
if key == "nick": # this is the roster nick, not the MUC nick
|
|
if entity in self.contact_lists[profile]:
|
|
self.contact_lists[profile].setCache(entity, 'nick', value)
|
|
self.callListeners('nick', entity, value, profile=profile)
|
|
elif key == "avatar":
|
|
if value: # and entity in self.contact_lists[profile]:
|
|
# FIXME: contact_list check is removed to work around 'avatar' issues
|
|
# (avatar can't be requested if we don't save them here)
|
|
# avatar need to be refactored properly in backend and here
|
|
def gotFilename(filename):
|
|
self.contact_lists[profile].setCache(entity, 'avatar', filename)
|
|
self.callListeners('avatar', entity, filename, profile=profile)
|
|
self.bridge.getAvatarFile(value, callback=gotFilename)
|
|
|
|
def actionManager(self, action_data, callback=None, ui_show_cb=None, user_action=True, profile=C.PROF_KEY_NONE):
|
|
"""Handle backend action
|
|
|
|
@param action_data(dict): action dict as sent by launchAction or returned by an UI action
|
|
@param callback(None, callback): if not None, callback to use on XMLUI answer
|
|
@param ui_show_cb(None, callback): if not None, method to call to show the XMLUI
|
|
@param user_action(bool): if True, the action is a result of a user interaction
|
|
else the action come from backend direclty (i.e. actionNew)
|
|
"""
|
|
try:
|
|
xmlui = action_data.pop('xmlui')
|
|
except KeyError:
|
|
pass
|
|
else:
|
|
ui = self.xmlui.create(self, xml_data=xmlui, flags=("FROM_BACKEND",) if not user_action else None, callback=callback, profile=profile)
|
|
if ui_show_cb is None:
|
|
ui.show()
|
|
else:
|
|
ui_show_cb(ui)
|
|
|
|
try:
|
|
progress_id = action_data.pop('progress')
|
|
except KeyError:
|
|
pass
|
|
else:
|
|
self.progressIdHandler(progress_id, profile)
|
|
|
|
# we ignore metadata
|
|
action_data = {k:v for k,v in action_data.iteritems() if not k.startswith("meta_")}
|
|
|
|
if action_data:
|
|
raise exceptions.DataError(u"Not all keys in action_data are managed ({keys})".format(keys=', '.join(action_data.keys())))
|
|
|
|
def launchAction(self, callback_id, data=None, callback=None, profile=C.PROF_KEY_NONE):
|
|
""" Launch a dynamic action
|
|
|
|
@param callback_id: id of the action to launch
|
|
@param data: data needed only for certain actions
|
|
@param callback(callable, None): will be called with the resut
|
|
if None, self.actionManager will be called
|
|
else the callable will be called with the following kw parameters:
|
|
- data: action_data
|
|
- cb_id: callback id
|
|
- profile: %(doc_profile)s
|
|
@param profile: %(doc_profile)s
|
|
|
|
"""
|
|
if data is None:
|
|
data = dict()
|
|
|
|
|
|
def action_cb(data):
|
|
if callback is None:
|
|
self.actionManager(data, profile=profile)
|
|
else:
|
|
callback(data=data, cb_id=callback_id, profile=profile)
|
|
|
|
self.bridge.launchAction(callback_id, data, profile, callback=action_cb, errback=self.dialogFailure)
|
|
|
|
def getAvatar(self, entity, profile):
|
|
"""return avatar path for an entity"""
|
|
contact_list = self.contact_lists[profile]
|
|
avatar = contact_list.getCache(entity, "avatar")
|
|
if avatar is None:
|
|
self.bridge.getCard(unicode(entity), profile)
|
|
contact_list.setCache(entity, "avatar", "")
|
|
if not avatar:
|
|
avatar = self.getDefaultAvatar(entity)
|
|
return avatar
|
|
|
|
def getDefaultAvatar(self, entity=None):
|
|
"""return default avatar to use with given entity
|
|
|
|
must be implemented by frontend
|
|
@param entity(jid.JID): entity for which a default avatar is needed
|
|
"""
|
|
raise NotImplementedError
|
|
|
|
def disconnect(self, profile):
|
|
log.info("disconnecting")
|
|
self.callListeners('disconnect', profile=profile)
|
|
self.bridge.disconnect(profile)
|
|
|
|
def onExit(self):
|
|
"""Must be called when the frontend is terminating"""
|
|
to_unplug = []
|
|
for profile, profile_manager in self.profiles.iteritems():
|
|
if profile_manager.connected(profile) and profile_manager.autodisconnect:
|
|
#The user wants autodisconnection
|
|
self.disconnect(profile)
|
|
to_unplug.append(profile)
|
|
for profile in to_unplug:
|
|
self.unplug_profile(profile)
|