689 lines
31 KiB
Python
689 lines
31 KiB
Python
#!/usr/bin/python
|
|
# -*- coding: utf-8 -*-
|
|
|
|
# helper class for making a SAT frontend
|
|
# Copyright (C) 2009, 2010, 2011, 2012, 2013, 2014 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.misc import TriggerManager
|
|
|
|
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_chat, quick_games
|
|
from sat_frontends.quick_frontend.constants import Const as C
|
|
|
|
import sys
|
|
from collections import OrderedDict
|
|
|
|
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"""
|
|
host = None
|
|
bridge = None
|
|
cache_keys_to_get = ['avatar']
|
|
|
|
def __init__(self, profile):
|
|
self.profile = profile
|
|
self.whoami = None
|
|
self.data = {}
|
|
|
|
def __getitem__(self, key):
|
|
return self.data[key]
|
|
|
|
def __setitem__(self, key, value):
|
|
self.data[key] = value
|
|
|
|
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):
|
|
self.whoami = jid.JID(_jid)
|
|
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.bridge.isConnected(self.profile):
|
|
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.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("Couldn't get cached values: {}".format(failure))
|
|
self._plug_profile_gotCachedValues({})
|
|
|
|
def _plug_profile_gotCachedValues(self, cached_values):
|
|
# TODO: watched plugin
|
|
|
|
# add the contact list and its listener
|
|
contact_list = self.host.addContactList(self.profile)
|
|
self.host.contact_lists[self.profile] = contact_list
|
|
|
|
for entity, data in cached_values.iteritems():
|
|
for key, value in data.iteritems():
|
|
contact_list.setCache(jid.JID(entity), key, value)
|
|
|
|
if not self.bridge.isConnected(self.profile):
|
|
self.host.setStatusOnline(False, profile=self.profile)
|
|
else:
|
|
self.host.setStatusOnline(True, profile=self.profile)
|
|
|
|
contact_list.fill()
|
|
|
|
#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.getRoomsJoined(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.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"))
|
|
|
|
|
|
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 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].onDelete()
|
|
del host.contact_lists[profile]
|
|
|
|
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"""
|
|
|
|
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)
|
|
"""
|
|
self.menus = quick_menus.QuickMenusManager(self)
|
|
ProfileManager.host = self
|
|
self.profiles = ProfilesManager()
|
|
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 = {}
|
|
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 = TriggerManager() # trigger are used to change the default behaviour
|
|
|
|
## bridge ##
|
|
try:
|
|
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)
|
|
except exceptions.BridgeInitError:
|
|
print(_(u"Can't init bridge"))
|
|
sys.exit(1)
|
|
ProfileManager.bridge = self.bridge
|
|
self.registerSignal("connected")
|
|
self.registerSignal("disconnected")
|
|
self.registerSignal("newContact")
|
|
self.registerSignal("newMessage")
|
|
self.registerSignal("newAlert")
|
|
self.registerSignal("presenceUpdate")
|
|
self.registerSignal("subscribe")
|
|
self.registerSignal("paramUpdate")
|
|
self.registerSignal("contactDeleted")
|
|
self.registerSignal("entityDataUpdated")
|
|
self.registerSignal("askConfirmation")
|
|
self.registerSignal("actionResult")
|
|
self.registerSignal("actionResultExt", self.actionResultHandler)
|
|
self.registerSignal("roomJoined", iface="plugin")
|
|
self.registerSignal("roomLeft", iface="plugin")
|
|
self.registerSignal("roomUserJoined", iface="plugin")
|
|
self.registerSignal("roomUserLeft", iface="plugin")
|
|
self.registerSignal("roomUserChangedNick", iface="plugin")
|
|
self.registerSignal("roomNewSubject", iface="plugin")
|
|
self.registerSignal("chatStateReceived", iface="plugin")
|
|
self.registerSignal("personalEvent", iface="plugin")
|
|
|
|
# FIXME: do it dynamically
|
|
quick_games.Tarot.registerSignals(self)
|
|
quick_games.Quiz.registerSignals(self)
|
|
quick_games.Radiocol.registerSignals(self)
|
|
|
|
self.current_action_ids = set() # FIXME: to be removed
|
|
self.current_action_ids_cb = {} # FIXME: to be removed
|
|
self.media_dir = self.bridge.getConfig('', 'media_dir')
|
|
|
|
@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)"""
|
|
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(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(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)
|
|
- 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.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))
|
|
|
|
self.callListeners('profilePlugged', profile=profile)
|
|
|
|
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)
|
|
if failure.module.startswith('twisted.words.protocols.jabber') and failure.condition == "not-authorized":
|
|
self.launchAction(C.CHANGE_XMPP_PASSWD_ID, {}, profile=profile)
|
|
else:
|
|
self.showDialog(failure.message, failure.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.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 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
|
|
|
|
def connectedHandler(self, profile):
|
|
"""called when the connection is made"""
|
|
log.debug(_("Connected"))
|
|
self.setStatusOnline(True, profile=profile)
|
|
|
|
def disconnectedHandler(self, profile):
|
|
"""called when the connection is closed"""
|
|
log.debug(_("Disconnected"))
|
|
self.contact_lists[profile].clearContacts()
|
|
self.setStatusOnline(False, profile=profile)
|
|
|
|
def newContactHandler(self, JabberId, attributes, groups, profile):
|
|
entity = jid.JID(JabberId)
|
|
_groups = list(groups)
|
|
self.contact_lists[profile].setContact(entity, _groups, attributes, in_roster=True)
|
|
|
|
def newMessageHandler(self, from_jid_s, msg, type_, to_jid_s, extra, profile):
|
|
from_jid = jid.JID(from_jid_s)
|
|
to_jid = jid.JID(to_jid_s)
|
|
|
|
if not self.trigger.point("newMessageTrigger", from_jid, msg, type_, to_jid, extra, profile=profile):
|
|
return
|
|
|
|
from_me = from_jid.bare == self.profiles[profile].whoami.bare
|
|
target = to_jid if from_me else from_jid
|
|
|
|
chat_type = C.CHAT_GROUP if type_ == C.MESS_TYPE_GROUPCHAT else C.CHAT_ONE2ONE
|
|
contact_list = self.contact_lists[profile]
|
|
|
|
chat_widget = self.widgets.getOrCreateWidget(quick_chat.QuickChat, target, type_=chat_type, on_new_widget=None, profile=profile)
|
|
|
|
self.current_action_ids = set() # FIXME: to be removed
|
|
self.current_action_ids_cb = {} # FIXME: to be removed
|
|
|
|
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 display the message in the widget
|
|
chat_widget.newMessage(from_jid, target, msg, type_, extra, profile)
|
|
|
|
# ContactList alert
|
|
visible = False
|
|
for widget in self.visible_widgets:
|
|
if isinstance(widget, quick_chat.QuickChat) and widget.manageMessage(from_jid, type_):
|
|
visible = True
|
|
break
|
|
if not visible:
|
|
contact_list.setAlert(from_jid.bare if type_ == C.MESS_TYPE_GROUPCHAT else from_jid)
|
|
|
|
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 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("sendMessageTrigger", to_jid, message, subject, mess_type, extra, callback, errback, profile_key=profile_key):
|
|
return
|
|
|
|
self.bridge.sendMessage(unicode(to_jid), message, subject, mess_type, extra, profile_key, callback=callback, errback=errback)
|
|
|
|
def newAlertHandler(self, msg, title, alert_type, profile):
|
|
assert alert_type in ['INFO', 'ERROR']
|
|
self.showDialog(unicode(msg), unicode(title), alert_type.lower())
|
|
|
|
def setStatusOnline(self, online=True, show="", statuses={}, profile=C.PROF_KEY_NONE):
|
|
raise NotImplementedError
|
|
|
|
def presenceUpdateHandler(self, entity_s, show, priority, statuses, profile):
|
|
|
|
log.debug(_("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 == "unavailable":
|
|
self.setStatusOnline(False, profile=profile)
|
|
else:
|
|
self.setStatusOnline(True, show, statuses, profile=profile)
|
|
return
|
|
|
|
# #FIXME: must be moved in a plugin
|
|
# if entity.bare in self.profiles[profile].data.get('watched',[]) and not entity.bare in self.profiles[profile]['onlineContact']:
|
|
# self.showAlert(_("Watched jid [%s] is connected !") % entity.bare)
|
|
|
|
self.callListeners('presence', entity, show, priority, statuses, profile=profile)
|
|
|
|
def roomJoinedHandler(self, room_jid_s, room_nicks, user_nick, profile):
|
|
"""Called when a MUC room is joined"""
|
|
log.debug("Room [%(room_jid)s] joined by %(profile)s, users presents:%(users)s" % {'room_jid': room_jid_s, 'profile': profile, 'users': room_nicks})
|
|
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.setUserNick(user_nick)
|
|
chat_widget.id = room_jid # FIXME: to be removed
|
|
room_nicks = [unicode(nick) for nick in room_nicks] # FIXME: should be done in DBus bridge / is that still needed?!
|
|
nicks = list(set([user_nick] + room_nicks))
|
|
nicks.sort()
|
|
chat_widget.setPresents(nicks)
|
|
self.contact_lists[profile].setSpecial(room_jid, C.CONTACT_SPECIAL_GROUP)
|
|
|
|
def roomLeftHandler(self, room_jid_s, profile):
|
|
"""Called when a MUC room is left"""
|
|
log.debug("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].remove(room_jid)
|
|
|
|
def roomUserJoinedHandler(self, room_jid_s, user_nick, user_data, 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.replaceUser(user_nick)
|
|
log.debug("user [%(user_nick)s] joined room [%(room_jid)s]" % {'user_nick': user_nick, 'room_jid': room_jid})
|
|
|
|
def roomUserLeftHandler(self, room_jid_s, user_nick, user_data, 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.removeUser(user_nick)
|
|
log.debug("user [%(user_nick)s] left room [%(room_jid)s]" % {'user_nick': user_nick, 'room_jid': room_jid})
|
|
|
|
def roomUserChangedNickHandler(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("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 roomNewSubjectHandler(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("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 is received.
|
|
|
|
@param from_jid_s: JID of the contact who sent his state, or '@ALL@'
|
|
@param state: new state (string)
|
|
@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.widgets.getWidgets(quick_chat.QuickChat):
|
|
if from_jid == C.ENTITY_ALL or from_jid.bare == widget.target.bare:
|
|
widget.updateChatState(from_jid, state)
|
|
|
|
def personalEventHandler(self, sender, event_type, data):
|
|
"""Called when a PEP event is received.
|
|
|
|
@param sender (jid.JID): event sender
|
|
@param event_type (unicode): event type, e.g. 'MICROBLOG' or 'MICROBLOG_DELETE'
|
|
@param data (dict): event data
|
|
"""
|
|
# FIXME move some code from Libervia to here and put the magic strings to constants
|
|
pass
|
|
|
|
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
|
|
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
|
|
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 paramUpdateHandler(self, name, value, namespace, profile):
|
|
log.debug(_("param update: [%(namespace)s] %(name)s = %(value)s") % {'namespace': namespace, 'name': name, 'value': value})
|
|
if (namespace, name) == ("Connection", "JabberID"):
|
|
log.debug(_("Changing JID to %s") % value)
|
|
self.profiles[profile].whoami = jid.JID(value)
|
|
elif (namespace, name) == ("Misc", "Watched"):
|
|
self.profiles[profile]['watched'] = value.split()
|
|
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].remove(target)
|
|
|
|
def entityDataUpdatedHandler(self, entity_s, key, value, profile):
|
|
entity = jid.JID(entity_s)
|
|
if key == "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 entity in self.contact_lists[profile]:
|
|
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 askConfirmationHandler(self, confirm_id, confirm_type, data, profile):
|
|
raise NotImplementedError
|
|
|
|
def actionResultHandler(self, type, id, data, profile):
|
|
raise NotImplementedError
|
|
|
|
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: if not None and 'validated' key is present, it will be called with the following parameters:
|
|
- callback_id
|
|
- data
|
|
- profile_key
|
|
@param profile_key: %(doc_profile)s
|
|
|
|
"""
|
|
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 in self.profiles:
|
|
if self.bridge.isConnected(profile):
|
|
if C.bool(self.bridge.getParamA("autodisconnect", "Connection", profile_key=profile)):
|
|
#The user wants autodisconnection
|
|
self.disconnect(profile)
|
|
to_unplug.append(profile)
|
|
for profile in to_unplug:
|
|
self.unplug_profile(profile)
|