1501 lines
57 KiB
Python
1501 lines
57 KiB
Python
#!/usr/bin/env python3
|
|
|
|
# helper class for making a SAT frontend
|
|
# Copyright (C) 2009-2026 Jérôme Poisson (goffi@goffi.org)
|
|
|
|
# This program is free software: you can redistribute it and/or modify
|
|
# it under the terms of the GNU Affero General Public License as published by
|
|
# the Free Software Foundation, either version 3 of the License, or
|
|
# (at your option) any later version.
|
|
|
|
# This program is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU Affero General Public License for more details.
|
|
|
|
# You should have received a copy of the GNU Affero General Public License
|
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
import sys
|
|
import time
|
|
from typing import Callable
|
|
|
|
from libervia.backend.core import exceptions
|
|
from libervia.backend.core.i18n import _
|
|
from libervia.backend.core.log import getLogger
|
|
from libervia.backend.tools import trigger
|
|
from libervia.backend.tools.common import data_format
|
|
from libervia.frontends.quick_frontend import quick_widgets
|
|
from libervia.frontends.quick_frontend import quick_menus
|
|
from libervia.frontends.quick_frontend import quick_blog
|
|
from libervia.frontends.quick_frontend import quick_chat, quick_games
|
|
from libervia.frontends.quick_frontend import quick_contact_list
|
|
from libervia.frontends.quick_frontend.constants import Const as C
|
|
from libervia.frontends.tools import jid
|
|
|
|
log = getLogger(__name__)
|
|
|
|
|
|
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", "nicknames"]
|
|
|
|
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"""
|
|
# first of all we create the contact lists
|
|
self.host.contact_lists.add_profile(self.profile)
|
|
|
|
# we get the essential params
|
|
self.bridge.param_get_a_async(
|
|
"JabberID",
|
|
"Connection",
|
|
profile_key=self.profile,
|
|
callback=self._plug_profile_jid,
|
|
errback=self._get_param_error,
|
|
)
|
|
|
|
def _plug_profile_jid(self, jid_s):
|
|
self.whoami = jid.JID(jid_s) # resource might change after the connection
|
|
log.info(f"Our current jid is: {self.whoami}")
|
|
self.bridge.is_connected(self.profile, callback=self._plug_profile_isconnected)
|
|
|
|
def _autodisconnect_eb(self, failure_):
|
|
# XXX: we ignore error on this parameter, as Libervia can't access it
|
|
log.warning(
|
|
_("Error while trying to get autodisconnect param, ignoring: {}").format(
|
|
failure_
|
|
)
|
|
)
|
|
self._plug_profile_autodisconnect("false")
|
|
|
|
def _plug_profile_isconnected(self, connected):
|
|
self.connected = connected
|
|
if connected:
|
|
self.host.profile_connected(self.profile)
|
|
self.bridge.param_get_a_async(
|
|
"autodisconnect",
|
|
"Connection",
|
|
profile_key=self.profile,
|
|
callback=self._plug_profile_autodisconnect,
|
|
errback=self._autodisconnect_eb,
|
|
)
|
|
|
|
def _plug_profile_autodisconnect(self, autodisconnect):
|
|
if C.bool(autodisconnect):
|
|
self._autodisconnect = True
|
|
self.bridge.param_get_a_async(
|
|
"autoconnect",
|
|
"Connection",
|
|
profile_key=self.profile,
|
|
callback=self._plug_profile_autoconnect,
|
|
errback=self._get_param_error,
|
|
)
|
|
|
|
def _plug_profile_autoconnect(self, value_str):
|
|
autoconnect = C.bool(value_str)
|
|
if autoconnect and not self.connected:
|
|
self.host.connect(
|
|
self.profile, callback=lambda __: 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.features_get(
|
|
profile_key=self.profile,
|
|
callback=self._plug_profile_get_features_cb,
|
|
errback=self._plug_profile_get_features_eb,
|
|
)
|
|
|
|
def _plug_profile_get_features_eb(self, failure):
|
|
log.error("Couldn't get features: {}".format(failure))
|
|
self._plug_profile_get_features_cb({})
|
|
|
|
def _plug_profile_get_features_cb(self, features):
|
|
self.host.features = features
|
|
self.host.bridge.entities_data_get(
|
|
[],
|
|
ProfileManager.cache_keys_to_get,
|
|
profile=self.profile,
|
|
callback=self._plug_profile_got_cached_values,
|
|
errback=self._plug_profile_failed_cached_values,
|
|
)
|
|
|
|
def _plug_profile_failed_cached_values(self, failure):
|
|
log.error("Couldn't get cached values: {}".format(failure))
|
|
self._plug_profile_got_cached_values({})
|
|
|
|
def _plug_profile_got_cached_values(self, cached_values):
|
|
contact_list = self.host.contact_lists[self.profile]
|
|
# add the contact list and its listener
|
|
for entity_s, data in cached_values.items():
|
|
for key, value in data.items():
|
|
self.host.entity_data_updated_handler(entity_s, key, value, self.profile)
|
|
|
|
if not self.connected:
|
|
self.host.set_presence_status(
|
|
C.PRESENCE_UNAVAILABLE, "", profile=self.profile
|
|
)
|
|
else:
|
|
|
|
contact_list.fill()
|
|
self.host.set_presence_status(profile=self.profile)
|
|
|
|
# The waiting subscription requests
|
|
self.bridge.sub_waiting_get(
|
|
self.profile, callback=self._plug_profile_got_waiting_sub
|
|
)
|
|
|
|
def _plug_profile_got_waiting_sub(self, waiting_sub):
|
|
for sub in waiting_sub:
|
|
self.host.subscribe_handler(waiting_sub[sub], sub, self.profile)
|
|
|
|
self.bridge.muc_get_rooms_joined(
|
|
self.profile, callback=self._plug_profile_got_rooms_joined
|
|
)
|
|
|
|
def _plug_profile_got_rooms_joined(self, rooms_args):
|
|
# Now we open the MUC window where we already are:
|
|
for room_args in rooms_args:
|
|
self.host.muc_room_joined_handler(*room_args, profile=self.profile)
|
|
# Presence must be requested after rooms are filled
|
|
self.host.bridge.presence_statuses_get(
|
|
self.profile, callback=self._plug_profile_got_presences
|
|
)
|
|
|
|
def _plug_profile_got_presences(self, presences):
|
|
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.presence_update_handler(
|
|
jabber_id, show, priority, statuses, self.profile
|
|
)
|
|
|
|
# At this point, profile should be fully plugged
|
|
# and we launch frontend specific method
|
|
self.host.profile_plugged(self.profile)
|
|
|
|
def _get_param_error(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 iter(self._profiles.keys())
|
|
|
|
def __getitem__(self, profile):
|
|
return self._profiles[profile]
|
|
|
|
def __len__(self):
|
|
return len(self._profiles)
|
|
|
|
def items(self):
|
|
return self._profiles.items()
|
|
|
|
def values(self):
|
|
return self._profiles.values()
|
|
|
|
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 choose_one_profile(self):
|
|
return list(self._profiles.keys())[0]
|
|
|
|
|
|
class QuickApp:
|
|
"""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
|
|
ENCRYPTION_HANDLERS = True #: set to False if encryption is handled separatly
|
|
#: if True, QuickApp will call resync itself, on all widgets at the same time
|
|
#: if False, frontend must call resync itself when suitable (e.g. widget is being
|
|
#: visible)
|
|
AUTO_RESYNC = True
|
|
|
|
def __init__(
|
|
self,
|
|
bridge_factory,
|
|
xmlui,
|
|
check_options=None,
|
|
connect_bridge=True,
|
|
async_bridge_factory=None,
|
|
):
|
|
"""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()
|
|
# profiles currently being plugged, used to (un)lock contact list updates
|
|
self._plugs_in_progress = set()
|
|
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
|
|
|
|
# see selected_widget setter and getter
|
|
self._selected_widget = None
|
|
|
|
# listeners are callable watching events
|
|
self._listeners = {} # key: listener type ("avatar", "selected", etc),
|
|
# value: list of callbacks
|
|
|
|
# cf. [register_action_handler]
|
|
self._action_handlers: dict[str, Callable[[dict, str, int, str], None]] = {}
|
|
|
|
# triggers
|
|
self.trigger = (
|
|
trigger.TriggerManager()
|
|
) # trigger are used to change the default behaviour
|
|
|
|
## bridge ##
|
|
self.bridge = bridge_factory()
|
|
if async_bridge_factory is None:
|
|
log.warning("No async bridge specified")
|
|
self.a_bridge = None
|
|
else:
|
|
self.a_bridge = async_bridge_factory()
|
|
ProfileManager.bridge = self.bridge
|
|
if connect_bridge:
|
|
self.connect_bridge()
|
|
|
|
# frontend notifications
|
|
self._notif_id = 0
|
|
self._notifications = {}
|
|
# watched progresses and associated callbacks
|
|
self._progress_ids = {}
|
|
# available features
|
|
# FIXME: features are profile specific, to be checked
|
|
self.features = None
|
|
#: map of short name to namespaces
|
|
self.ns_map = {}
|
|
#: available encryptions
|
|
self.encryption_plugins = []
|
|
# state of synchronisation with backend
|
|
self._sync = True
|
|
|
|
def connect_bridge(self):
|
|
self.bridge.bridge_connect(callback=self._bridge_cb, errback=self._bridge_eb)
|
|
|
|
def _namespaces_get_cb(self, ns_map):
|
|
self.ns_map = ns_map
|
|
|
|
def _namespaces_get_eb(self, failure_):
|
|
log.error(_("Can't get namespaces map: {msg}").format(msg=failure_))
|
|
|
|
def _encryption_plugins_get_cb(self, plugins_ser):
|
|
self.encryption_plugins = data_format.deserialise(plugins_ser, type_check=list)
|
|
|
|
def _encryption_plugins_get_eb(self, failure_):
|
|
log.warning(_("Can't retrieve encryption plugins: {msg}").format(msg=failure_))
|
|
|
|
def on_bridge_connected(self):
|
|
self.bridge.ready_get(self.on_backend_ready)
|
|
|
|
def _bridge_cb(self):
|
|
self.register_signal("connected")
|
|
self.register_signal("disconnected")
|
|
self.register_signal("action_new")
|
|
self.register_signal("contact_new")
|
|
self.register_signal("message_new")
|
|
if self.ENCRYPTION_HANDLERS:
|
|
self.register_signal("message_encryption_started")
|
|
self.register_signal("message_encryption_stopped")
|
|
self.register_signal("presence_update")
|
|
self.register_signal("subscribe")
|
|
self.register_signal("param_update")
|
|
self.register_signal("contact_deleted")
|
|
self.register_signal("entity_data_updated")
|
|
self.register_signal("progress_started")
|
|
self.register_signal("progress_finished")
|
|
self.register_signal("progress_error")
|
|
self.register_signal("muc_room_joined", iface="plugin")
|
|
self.register_signal("muc_room_left", iface="plugin")
|
|
self.register_signal("muc_room_user_changed_nick", iface="plugin")
|
|
self.register_signal("muc_room_new_subject", iface="plugin")
|
|
self.register_signal("chat_state_received", iface="plugin")
|
|
self.register_signal("message_state", iface="plugin")
|
|
self.register_signal("ps_event", iface="plugin")
|
|
# useful for debugging
|
|
self.register_signal("_debug", iface="core")
|
|
|
|
# FIXME: do it dynamically
|
|
quick_games.Tarot.register_signals(self)
|
|
quick_games.Quiz.register_signals(self)
|
|
quick_games.Radiocol.register_signals(self)
|
|
self.on_bridge_connected()
|
|
|
|
def _bridge_eb(self, failure):
|
|
if isinstance(failure, exceptions.BridgeExceptionNoService):
|
|
print(
|
|
_("Unable to connect to the Libervia backend. Are you sure it's running?")
|
|
)
|
|
sys.exit(C.EXIT_BACKEND_NOT_FOUND)
|
|
elif isinstance(failure, exceptions.BridgeInitError):
|
|
print((_("Can't init bridge")))
|
|
sys.exit(C.EXIT_BRIDGE_ERROR)
|
|
else:
|
|
print((_("Error while initialising bridge: {}".format(failure))))
|
|
|
|
def on_backend_ready(self):
|
|
log.info("backend is ready")
|
|
self.bridge.namespaces_get(
|
|
callback=self._namespaces_get_cb, errback=self._namespaces_get_eb
|
|
)
|
|
# we cache available encryption plugins, as we'll use them on each
|
|
# new chat widget
|
|
self.bridge.encryption_plugins_get(
|
|
callback=self._encryption_plugins_get_cb,
|
|
errback=self._encryption_plugins_get_eb,
|
|
)
|
|
|
|
@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.choose_one_profile()
|
|
|
|
@property
|
|
def visible_widgets(self):
|
|
"""Widgets currently visible
|
|
|
|
This must be implemented by frontend
|
|
@return (iter[object]): iterable on visible widgets
|
|
widgets can be QuickWidgets or not
|
|
"""
|
|
raise NotImplementedError
|
|
|
|
@property
|
|
def visible_quick_widgets(self):
|
|
"""QuickWidgets currently visible
|
|
|
|
This generator iterate only on QuickWidgets, discarding other kinds of
|
|
widget the frontend may have.
|
|
@return (iter[object]): iterable on visible widgets
|
|
"""
|
|
for w in self.visisble_widgets:
|
|
if isinstance(w, quick_widgets.QuickWidget):
|
|
return w
|
|
|
|
@property
|
|
def selected_widget(self):
|
|
"""widget currently selected
|
|
|
|
This must be set by frontend using setter.
|
|
"""
|
|
return self._selected_widget
|
|
|
|
@selected_widget.setter
|
|
def selected_widget(self, wid):
|
|
"""Set the currently selected widget
|
|
|
|
Must be set by frontend
|
|
"""
|
|
if self._selected_widget == wid:
|
|
return
|
|
self._selected_widget = wid
|
|
try:
|
|
on_selected = wid.on_selected
|
|
except AttributeError:
|
|
pass
|
|
else:
|
|
on_selected()
|
|
|
|
self.call_listeners("selected", wid)
|
|
|
|
# backend state management
|
|
|
|
@property
|
|
def sync(self):
|
|
"""Synchronization flag
|
|
|
|
True if this frontend is synchronised with backend
|
|
"""
|
|
return self._sync
|
|
|
|
@sync.setter
|
|
def sync(self, state):
|
|
"""Called when backend is desynchronised or resynchronising
|
|
|
|
@param state(bool): True: if the backend is resynchronising
|
|
False when we lose synchronisation, for instance if frontend is going to sleep
|
|
or if connection has been lost and a reconnection is needed
|
|
"""
|
|
if state:
|
|
log.debug("we are synchronised with server")
|
|
if self.AUTO_RESYNC:
|
|
# we are resynchronising all widgets
|
|
log.debug("doing a full widgets resynchronisation")
|
|
for w in self.widgets:
|
|
try:
|
|
resync = w.resync
|
|
except AttributeError:
|
|
pass
|
|
else:
|
|
resync()
|
|
self.contact_lists.fill()
|
|
|
|
self._sync = state
|
|
else:
|
|
log.debug("we have lost synchronisation with server")
|
|
self._sync = state
|
|
# we've lost synchronisation, all widgets must be notified
|
|
# note: this is always called independently of AUTO_RESYNC
|
|
for w in self.widgets:
|
|
try:
|
|
w.sync = False
|
|
except AttributeError:
|
|
pass
|
|
|
|
def register_signal(
|
|
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
|
|
"""
|
|
log.debug("registering signal {name}".format(name=function_name))
|
|
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 signal_received(*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, signal_received, 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:
|
|
- contactsFilled: called when contact have been fully filled for a profiles
|
|
kwargs: profile
|
|
- avatar: called when avatar data is updated
|
|
args: (entity, avatar_data, profile)
|
|
- nicknames: called when nicknames data is updated
|
|
args: (entity, nicknames, profile)
|
|
- presence: called when a presence is received
|
|
args: (entity, show, priority, statuses, profile)
|
|
- selected: called when a widget is selected
|
|
args: (selected_widget,)
|
|
- notification: called when a new notification is emited
|
|
args: (entity, notification_data, profile)
|
|
- notificationsClear: called when notifications are cleared
|
|
args: (entity, type_, profile)
|
|
- widgetNew: a new QuickWidget has been created
|
|
args: (widget,)
|
|
- widgetDeleted: all instances of a widget with specific hash have been
|
|
deleted
|
|
args: (widget_deleted,)
|
|
- 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.import_menu]
|
|
path: same as in [sat.core.sat_main.SAT.import_menu]
|
|
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)
|
|
- progress_finished: called when a progressing action has just finished
|
|
args: (progress_id, metadata, profile)
|
|
- progress_error: called when a progressing action failed
|
|
args: (progress_id, error_msg, profile):
|
|
@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_, {})[callback] = profiles_filter
|
|
|
|
def removeListener(self, type_, callback, ignore_missing=False):
|
|
"""Remove a callback from listeners
|
|
|
|
@param type_(str): same as for [addListener]
|
|
@param callback(callable): callback to remove
|
|
@param ignore_missing(bool): if True, don't log error if the listener doesn't
|
|
exist
|
|
"""
|
|
assert type_ in C.LISTENERS
|
|
try:
|
|
self._listeners[type_].pop(callback)
|
|
except KeyError:
|
|
if not ignore_missing:
|
|
log.error(
|
|
f"Trying to remove an inexisting listener (type = {type_}): "
|
|
f"{callback}"
|
|
)
|
|
|
|
def call_listeners(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 list(listeners.items()):
|
|
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 post_init(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 profile_plugged(self, profile):
|
|
"""Method called when the profile is fully plugged
|
|
|
|
This will 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(
|
|
"Calling cached signal [%s] with args %s and kwargs %s"
|
|
% (function_name, args, kwargs)
|
|
)
|
|
handler(*args, **kwargs)
|
|
|
|
self.call_listeners("profile_plugged", profile=profile)
|
|
if not self._plugs_in_progress:
|
|
self.contact_lists.lock_update(False)
|
|
|
|
def profile_connected(self, profile):
|
|
"""Called when a plugged profile is connected
|
|
|
|
it is called independently of profile_plugged (may be called before or after
|
|
profile_plugged)
|
|
"""
|
|
pass
|
|
|
|
def connect(self, profile, callback=None, errback=None):
|
|
if not callback:
|
|
callback = lambda __: None
|
|
if not errback:
|
|
|
|
def errback(failure):
|
|
log.error(_("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.action_launch(C.CHANGE_XMPP_PASSWD_ID, {}, profile=profile)
|
|
else:
|
|
self.show_dialog(message, fullname, "error")
|
|
|
|
self.bridge.connect(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.lock_update()
|
|
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 new_widget(self, widget):
|
|
raise NotImplementedError
|
|
|
|
# bridge signals hanlers
|
|
|
|
def connected_handler(self, jid_s, profile):
|
|
"""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.set_presence_status(profile=profile)
|
|
# FIXME: fill() is already called for all profiles when doing self.sync = True
|
|
# a per-profile fill() should be done once, see below note
|
|
self.contact_lists[profile].fill()
|
|
# if we were already displaying widgets, they must be resynchronized
|
|
# FIXME: self.sync is for all profiles
|
|
# while (dis)connection is per-profile.
|
|
# A mechanism similar to sync should be available
|
|
# on a per-profile basis
|
|
self.sync = True
|
|
self.profile_connected(profile)
|
|
|
|
def disconnected_handler(self, profile):
|
|
"""called when the connection is closed"""
|
|
log.debug(_("Disconnected"))
|
|
self.contact_lists[profile].disconnect()
|
|
# FIXME: see note on connected_handler
|
|
self.sync = False
|
|
self.set_presence_status(C.PRESENCE_UNAVAILABLE, "", profile=profile)
|
|
|
|
def action_new_handler(self, action_data_s, action_id, security_limit, profile):
|
|
action_data = data_format.deserialise(action_data_s)
|
|
action_type = action_data.get("type")
|
|
action_handler = self._action_handlers.get(action_type)
|
|
if action_handler is not None:
|
|
action_handler(action_data, action_id, security_limit, profile)
|
|
else:
|
|
self.action_manager(
|
|
action_data, user_action=False, action_id=action_id, profile=profile
|
|
)
|
|
|
|
def contact_new_handler(self, jid_s, attributes, groups, profile):
|
|
entity = jid.JID(jid_s)
|
|
groups = list(groups)
|
|
self.contact_lists[profile].set_contact(
|
|
entity, groups, attributes, in_roster=True
|
|
)
|
|
|
|
def message_new_handler(
|
|
self, uid, timestamp, from_jid_s, to_jid_s, msg, subject, type_, extra_s, profile
|
|
):
|
|
from_jid = jid.JID(from_jid_s)
|
|
to_jid = jid.JID(to_jid_s)
|
|
extra = data_format.deserialise(extra_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
|
|
mess_to_jid = to_jid if from_me else from_jid
|
|
target = mess_to_jid.bare
|
|
contact_list = self.contact_lists[profile]
|
|
|
|
try:
|
|
is_room = contact_list.is_room(target)
|
|
except exceptions.NotFound:
|
|
is_room = False
|
|
|
|
if target.resource and not is_room:
|
|
# we avoid resource locking, but we must keep resource for private MUC
|
|
# messages
|
|
target = target
|
|
# we want to be sure to have at least one QuickChat instance
|
|
self.widgets.get_or_create_widget(
|
|
quick_chat.QuickChat,
|
|
target,
|
|
type_=C.CHAT_GROUP if is_room else 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.set_contact(from_jid)
|
|
|
|
# we dispatch the message in the widgets
|
|
for widget in self.widgets.get_widgets(
|
|
quick_chat.QuickChat, target=target, profiles=(profile,)
|
|
):
|
|
widget.message_new(
|
|
uid, timestamp, from_jid, mess_to_jid, msg, subject, type_, extra, profile
|
|
)
|
|
|
|
def message_encryption_started_handler(self, destinee_jid_s, plugin_data, profile):
|
|
destinee_jid = jid.JID(destinee_jid_s)
|
|
plugin_data = data_format.deserialise(plugin_data)
|
|
for widget in self.widgets.get_widgets(
|
|
quick_chat.QuickChat, target=destinee_jid.bare, profiles=(profile,)
|
|
):
|
|
widget.message_encryption_started(plugin_data)
|
|
|
|
def message_encryption_stopped_handler(self, destinee_jid_s, plugin_data, profile):
|
|
destinee_jid = jid.JID(destinee_jid_s)
|
|
for widget in self.widgets.get_widgets(
|
|
quick_chat.QuickChat, target=destinee_jid.bare, profiles=(profile,)
|
|
):
|
|
widget.message_encryption_stopped(plugin_data)
|
|
|
|
def message_state_handler(self, uid, status, profile):
|
|
for widget in self.widgets.get_widgets(quick_chat.QuickChat, profiles=(profile,)):
|
|
widget.on_message_state(uid, status, profile)
|
|
|
|
def message_send(
|
|
self,
|
|
to_jid,
|
|
message,
|
|
subject=None,
|
|
mess_type="auto",
|
|
extra=None,
|
|
callback=None,
|
|
errback=None,
|
|
profile_key=C.PROF_KEY_NONE,
|
|
):
|
|
if not subject and not extra and (not message or message == {"": ""}):
|
|
log.debug("Not sending empty message")
|
|
return
|
|
|
|
if subject is None:
|
|
subject = {}
|
|
if extra is None:
|
|
extra = {}
|
|
if callback is None:
|
|
callback = (
|
|
lambda __=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.show_dialog(
|
|
message=failure.message, title=failure.fullname, type="error"
|
|
)
|
|
|
|
if not self.trigger.point(
|
|
"messageSendTrigger",
|
|
to_jid,
|
|
message,
|
|
subject,
|
|
mess_type,
|
|
extra,
|
|
callback,
|
|
errback,
|
|
profile_key=profile_key,
|
|
):
|
|
return
|
|
|
|
self.bridge.message_send(
|
|
str(to_jid),
|
|
message,
|
|
subject,
|
|
mess_type,
|
|
data_format.serialise(extra),
|
|
profile_key,
|
|
callback=callback,
|
|
errback=errback,
|
|
)
|
|
|
|
def set_presence_status(self, show="", status=None, profile=C.PROF_KEY_NONE):
|
|
raise NotImplementedError
|
|
|
|
def presence_update_handler(self, entity_s, show, priority, statuses, profile):
|
|
# XXX: this log is commented because it's really too verbose even for DEBUG logs
|
|
# but it is kept here as it may still be useful for troubleshooting
|
|
# log.debug(
|
|
# _(
|
|
# u"presence update for %(entity)s (show=%(show)s, priority=%(priority)s, "
|
|
# u"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.set_presence_status(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.set_presence_status(show, status, profile=profile)
|
|
return
|
|
|
|
self.call_listeners("presence", entity, show, priority, statuses, profile=profile)
|
|
|
|
def muc_room_joined_handler(
|
|
self, room_jid_s, occupants, user_nick, subject, statuses, profile
|
|
):
|
|
"""Called when a MUC room is joined"""
|
|
log.debug(
|
|
"Room [{room_jid}] joined by {profile}, users presents:{users}".format(
|
|
room_jid=room_jid_s, profile=profile, users=list(occupants.keys())
|
|
)
|
|
)
|
|
room_jid = jid.JID(room_jid_s)
|
|
self.contact_lists[profile].set_special(room_jid, C.CONTACT_SPECIAL_GROUP)
|
|
self.widgets.get_or_create_widget(
|
|
quick_chat.QuickChat,
|
|
room_jid,
|
|
type_=C.CHAT_GROUP,
|
|
nick=user_nick,
|
|
occupants=occupants,
|
|
subject=subject,
|
|
statuses=statuses,
|
|
profile=profile,
|
|
)
|
|
|
|
def muc_room_left_handler(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.get_widget(quick_chat.QuickChat, room_jid, profile)
|
|
if chat_widget:
|
|
self.widgets.delete_widget(
|
|
chat_widget, all_instances=True, explicit_close=True
|
|
)
|
|
self.contact_lists[profile].remove_contact(room_jid)
|
|
|
|
def muc_room_user_changed_nick_handler(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.get_or_create_widget(
|
|
quick_chat.QuickChat, room_jid, type_=C.CHAT_GROUP, profile=profile
|
|
)
|
|
chat_widget.change_user_nick(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 muc_room_new_subject_handler(self, room_jid_s, subject, profile):
|
|
"""Called when subject of MUC room change"""
|
|
room_jid = jid.JID(room_jid_s)
|
|
chat_widget = self.widgets.get_or_create_widget(
|
|
quick_chat.QuickChat, room_jid, type_=C.CHAT_GROUP, profile=profile
|
|
)
|
|
chat_widget.set_subject(subject)
|
|
log.debug(
|
|
"new subject for room [%(room_jid)s]: %(subject)s"
|
|
% {"room_jid": room_jid, "subject": subject}
|
|
)
|
|
|
|
def chat_state_received_handler(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.get_widgets(
|
|
quick_chat.QuickChat, target=from_jid.bare, profiles=(profile,)
|
|
):
|
|
widget.on_chat_state(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
|
|
"""
|
|
assert type_ in C.NOTIFY_ALL
|
|
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._notif_id += 1
|
|
self.call_listeners("notification", entity, notif_data, profile=profile)
|
|
|
|
def get_notifs(
|
|
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 = iter(main_notif_dict.values())
|
|
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 = iter(notifs_from_select.values())
|
|
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 clear_notifs(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.call_listeners("notificationsClear", entity, type_, profile=profile)
|
|
|
|
def ps_event_handler(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 (serialised_dict): event data
|
|
"""
|
|
data = data_format.deserialise(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
|
|
|
|
# FIXME: check if [] make sense (instead of None)
|
|
_groups = data.get("group")
|
|
|
|
for wid in self.widgets.get_widgets(quick_blog.QuickBlog):
|
|
wid.add_entry_if_accepted(service_s, node, data, _groups, profile)
|
|
|
|
try:
|
|
comments_node, comments_service = (
|
|
data["comments_node"],
|
|
data["comments_service"],
|
|
)
|
|
except KeyError:
|
|
pass
|
|
else:
|
|
self.bridge.mb_get(
|
|
comments_service,
|
|
comments_node,
|
|
C.NO_LIMIT,
|
|
[],
|
|
{"subscribe": C.BOOL_TRUE},
|
|
profile=profile,
|
|
)
|
|
elif event_type == C.PS_RETRACT:
|
|
for wid in self.widgets.get_widgets(quick_blog.QuickBlog):
|
|
wid.delete_entry_if_present(service_s, node, data["id"], profile)
|
|
pass
|
|
else:
|
|
log.warning("Unmanaged PubSub event type {}".format(event_type))
|
|
|
|
def register_progress_cbs(self, progress_id, callback, errback):
|
|
"""Register progression callbacks
|
|
|
|
@param progress_id(unicode): id of the progression to check
|
|
@param callback(callable, None): method to call when progressing action
|
|
successfuly finished.
|
|
None to ignore
|
|
@param errback(callable, None): method to call when progressions action failed
|
|
None to ignore
|
|
"""
|
|
callbacks = self._progress_ids.setdefault(progress_id, [])
|
|
callbacks.append((callback, errback))
|
|
|
|
def progress_started_handler(self, pid, metadata, profile):
|
|
log.info("Progress {} started".format(pid))
|
|
|
|
def progress_finished_handler(self, pid, metadata, profile):
|
|
log.info("Progress {} finished".format(pid))
|
|
try:
|
|
callbacks = self._progress_ids.pop(pid)
|
|
except KeyError:
|
|
pass
|
|
else:
|
|
for callback, __ in callbacks:
|
|
if callback is not None:
|
|
callback(metadata, profile=profile)
|
|
self.call_listeners("progress_finished", pid, metadata, profile=profile)
|
|
|
|
def progress_error_handler(self, pid, err_msg, profile):
|
|
log.warning("Progress {pid} error: {err_msg}".format(pid=pid, err_msg=err_msg))
|
|
try:
|
|
callbacks = self._progress_ids.pop(pid)
|
|
except KeyError:
|
|
pass
|
|
else:
|
|
for __, errback in callbacks:
|
|
if errback is not None:
|
|
errback(err_msg, profile=profile)
|
|
self.call_listeners("progress_error", pid, err_msg, profile=profile)
|
|
|
|
def _subscribe_cb(self, answer, data):
|
|
entity, profile = data
|
|
type_ = "subscribed" if answer else "unsubscribed"
|
|
self.bridge.subscription(type_, str(entity.bare), profile_key=profile)
|
|
|
|
def subscribe_handler(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.show_dialog(
|
|
_("The contact {contact} has accepted your subscription").format(
|
|
contact=entity.bare
|
|
),
|
|
_("Subscription confirmation"),
|
|
)
|
|
elif type == "unsubscribed":
|
|
# this is a subscription refusal, we just have to inform user
|
|
self.show_dialog(
|
|
_("The contact {contact} has refused your subscription").format(
|
|
contact=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.show_dialog(
|
|
_(
|
|
"The contact {contact} wants to subscribe to your presence"
|
|
".\nDo you accept ?"
|
|
).format(contact=entity.bare),
|
|
_("Subscription confirmation"),
|
|
"yes/no",
|
|
answer_cb=self._subscribe_cb,
|
|
answer_data=(entity, profile),
|
|
)
|
|
|
|
def _debug_handler(self, action, parameters, profile):
|
|
if action == "widgets_dump":
|
|
from pprint import pformat
|
|
|
|
log.info("Widgets dump:\n{data}".format(data=pformat(self.widgets._widgets)))
|
|
else:
|
|
log.warning("Unknown debug action: {action}".format(action=action))
|
|
|
|
def show_dialog(self, message, title, type="info", answer_cb=None, answer_data=None):
|
|
"""Show a dialog to user
|
|
|
|
Frontends must override this method
|
|
@param message(unicode): body of the dialog
|
|
@param title(unicode): title of the dialog
|
|
@param type(unicode): one of:
|
|
- "info": information dialog (callbacks not used)
|
|
- "warning": important information to notice (callbacks not used)
|
|
- "error": something went wrong (callbacks not used)
|
|
- "yes/no": a dialog with 2 choices (yes and no)
|
|
@param answer_cb(callable): method to call on answer.
|
|
Arguments depend on dialog type:
|
|
- "yes/no": argument is a boolean (True for yes)
|
|
@param answer_data(object): data to link on callback
|
|
"""
|
|
# FIXME: misnamed method + types are not well chosen. Need to be rethought
|
|
raise NotImplementedError
|
|
|
|
def show_alert(self, message):
|
|
# FIXME: doesn't seems used anymore, to remove?
|
|
pass # FIXME
|
|
|
|
def dialog_failure(self, failure):
|
|
log.warning("Failure: {}".format(failure))
|
|
|
|
def progress_id_handler(self, progress_id, profile):
|
|
"""Callback used when an action result in a progress id"""
|
|
log.info("Progress ID received: {}".format(progress_id))
|
|
|
|
def is_hidden(self):
|
|
"""Tells if the frontend window is hidden.
|
|
|
|
@return bool
|
|
"""
|
|
raise NotImplementedError
|
|
|
|
def param_update_handler(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) == ("General", C.SHOW_OFFLINE_CONTACTS):
|
|
self.contact_lists[profile].show_offline_contacts(C.bool(value))
|
|
elif (namespace, name) == ("General", C.SHOW_EMPTY_GROUPS):
|
|
self.contact_lists[profile].show_empty_groups(C.bool(value))
|
|
|
|
def contact_deleted_handler(self, jid_s, profile):
|
|
target = jid.JID(jid_s)
|
|
self.contact_lists[profile].remove_contact(target)
|
|
|
|
def entity_data_updated_handler(self, entity_s, key, value_raw, profile):
|
|
entity = jid.JID(entity_s)
|
|
value = data_format.deserialise(value_raw, type_check=None)
|
|
if key == "nicknames":
|
|
assert isinstance(value, list) or value is None
|
|
if entity in self.contact_lists[profile]:
|
|
self.contact_lists[profile].set_cache(entity, "nicknames", value)
|
|
self.call_listeners("nicknames", entity, value, profile=profile)
|
|
elif key == "avatar" and self.AVATARS_HANDLER:
|
|
assert isinstance(value, dict) or value is None
|
|
self.contact_lists[profile].set_cache(entity, "avatar", value)
|
|
self.call_listeners("avatar", entity, value, profile=profile)
|
|
|
|
def register_action_handler(
|
|
self, action_type: str, handler: Callable[[dict, str, int, str], None]
|
|
) -> None:
|
|
"""Register a handler for action type.
|
|
|
|
If an action of this type is received, the handler will be used, otherwise,
|
|
generic ``action_manager`` is used
|
|
@param action_type: type of action that the handler manage
|
|
@param handler: method to call when an actipn of ``action_type`` is received.
|
|
Will have following args:
|
|
``action_data``
|
|
Data of the action.
|
|
``action_id``
|
|
ID of the action.
|
|
``security_limit``
|
|
Security limit, used to check if this action can be used in current security
|
|
context.
|
|
``profile``
|
|
Profile name.
|
|
"""
|
|
if handler in self._action_handlers:
|
|
raise exceptions.ConflictError(
|
|
f"There is already a registered handler for {action_type} actions: "
|
|
f"{handler}"
|
|
)
|
|
self._action_handlers[action_type] = handler
|
|
|
|
def action_manager(
|
|
self,
|
|
action_data: dict,
|
|
callback: Callable | None = None,
|
|
ui_show_cb: Callable | None = None,
|
|
user_action: bool = True,
|
|
action_id: str | None = None,
|
|
progress_cb: Callable | None = None,
|
|
progress_eb: Callable | None = None,
|
|
profile: str = C.PROF_KEY_NONE,
|
|
) -> None:
|
|
"""Handle backend action
|
|
|
|
@param action_data: action dict as sent by action_launch or returned by an
|
|
UI action
|
|
@param callback: if not None, callback to use on XMLUI answer
|
|
@param ui_show_cb: if not None, method to call to show the XMLUI
|
|
@param user_action: if True, the action is a result of a user interaction
|
|
else the action come from backend direclty (i.e. action_new).
|
|
This is useful to know if the frontend can display a popup immediately (if
|
|
True) or if it should add it to a queue that the user can activate later.
|
|
@param action_id: ID of the action.
|
|
@param progress_cb: method to call when progression is finished.
|
|
Only make sense if a progress is expected in this action
|
|
@param progress_eb: method to call when something went wrong
|
|
during progression.
|
|
Only make sense if a progress is expected in this action
|
|
"""
|
|
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:
|
|
if progress_cb or progress_eb:
|
|
self.register_progress_cbs(progress_id, progress_cb, progress_eb)
|
|
self.progress_id_handler(progress_id, profile)
|
|
|
|
def _action_cb(self, data, callback, callback_id, profile):
|
|
if callback is None:
|
|
self.action_manager(data, profile=profile)
|
|
else:
|
|
callback(data=data, cb_id=callback_id, profile=profile)
|
|
|
|
def action_launch(
|
|
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.action_manager 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()
|
|
action_cb = lambda data: self._action_cb(
|
|
data_format.deserialise(data), callback, callback_id, profile
|
|
)
|
|
self.bridge.action_launch(
|
|
callback_id,
|
|
data_format.serialise(data),
|
|
profile,
|
|
callback=action_cb,
|
|
errback=self.dialog_failure,
|
|
)
|
|
|
|
def launch_menu(
|
|
self,
|
|
menu_type,
|
|
path,
|
|
data=None,
|
|
callback=None,
|
|
security_limit=C.SECURITY_LIMIT_MAX,
|
|
profile=C.PROF_KEY_NONE,
|
|
):
|
|
"""Launch a menu manually
|
|
|
|
@param menu_type(unicode): type of the menu to launch
|
|
@param path(iterable[unicode]): path to the menu
|
|
@param data: data needed only for certain actions
|
|
@param callback(callable, None): will be called with the resut
|
|
if None, self.action_manager will be called
|
|
else the callable will be called with the following kw parameters:
|
|
- data: action_data
|
|
- cb_id: (menu_type, path) tuple
|
|
- profile: %(doc_profile)s
|
|
@param profile: %(doc_profile)s
|
|
|
|
"""
|
|
if data is None:
|
|
data = dict()
|
|
action_cb = lambda data: self._action_cb(
|
|
data, callback, (menu_type, path), profile
|
|
)
|
|
self.bridge.menu_launch(
|
|
menu_type,
|
|
path,
|
|
data,
|
|
security_limit,
|
|
profile,
|
|
callback=action_cb,
|
|
errback=self.dialog_failure,
|
|
)
|
|
|
|
def disconnect(self, profile):
|
|
log.info("disconnecting")
|
|
self.call_listeners("disconnect", profile=profile)
|
|
self.bridge.disconnect(profile)
|
|
|
|
def on_exit(self):
|
|
"""Must be called when the frontend is terminating"""
|
|
to_unplug = []
|
|
for profile, profile_manager in self.profiles.items():
|
|
if profile_manager.connected and profile_manager.autodisconnect:
|
|
# The user wants autodisconnection
|
|
self.disconnect(profile)
|
|
to_unplug.append(profile)
|
|
for profile in to_unplug:
|
|
self.unplug_profile(profile)
|