Mercurial > libervia-backend
comparison libervia/frontends/quick_frontend/quick_app.py @ 4074:26b7ed2817da
refactoring: rename `sat_frontends` to `libervia.frontends`
author | Goffi <goffi@goffi.org> |
---|---|
date | Fri, 02 Jun 2023 14:12:38 +0200 |
parents | sat_frontends/quick_frontend/quick_app.py@4b842c1fb686 |
children | 6b581d4c249f |
comparison
equal
deleted
inserted
replaced
4073:7c5654c54fed | 4074:26b7ed2817da |
---|---|
1 #!/usr/bin/env python3 | |
2 | |
3 # helper class for making a SAT frontend | |
4 # Copyright (C) 2009-2021 Jérôme Poisson (goffi@goffi.org) | |
5 | |
6 # This program is free software: you can redistribute it and/or modify | |
7 # it under the terms of the GNU Affero General Public License as published by | |
8 # the Free Software Foundation, either version 3 of the License, or | |
9 # (at your option) any later version. | |
10 | |
11 # This program is distributed in the hope that it will be useful, | |
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of | |
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
14 # GNU Affero General Public License for more details. | |
15 | |
16 # You should have received a copy of the GNU Affero General Public License | |
17 # along with this program. If not, see <http://www.gnu.org/licenses/>. | |
18 | |
19 from libervia.backend.core.log import getLogger | |
20 from libervia.backend.core.i18n import _ | |
21 from libervia.backend.core import exceptions | |
22 from libervia.backend.tools import trigger | |
23 from libervia.backend.tools.common import data_format | |
24 | |
25 from libervia.frontends.tools import jid | |
26 from libervia.frontends.quick_frontend import quick_widgets | |
27 from libervia.frontends.quick_frontend import quick_menus | |
28 from libervia.frontends.quick_frontend import quick_blog | |
29 from libervia.frontends.quick_frontend import quick_chat, quick_games | |
30 from libervia.frontends.quick_frontend import quick_contact_list | |
31 from libervia.frontends.quick_frontend.constants import Const as C | |
32 | |
33 import sys | |
34 import time | |
35 | |
36 | |
37 log = getLogger(__name__) | |
38 | |
39 | |
40 class ProfileManager(object): | |
41 """Class managing all data relative to one profile, and plugging in mechanism""" | |
42 | |
43 # TODO: handle waiting XMLUI requests: getWaitingConf doesn't exist anymore | |
44 # and a way to keep some XMLUI request between sessions is expected in backend | |
45 host = None | |
46 bridge = None | |
47 cache_keys_to_get = ['avatar', 'nicknames'] | |
48 | |
49 def __init__(self, profile): | |
50 self.profile = profile | |
51 self.connected = False | |
52 self.whoami = None | |
53 self.notifications = {} # key: bare jid or '' for general, value: notif data | |
54 | |
55 @property | |
56 def autodisconnect(self): | |
57 try: | |
58 autodisconnect = self._autodisconnect | |
59 except AttributeError: | |
60 autodisconnect = False | |
61 return autodisconnect | |
62 | |
63 def plug(self): | |
64 """Plug the profile to the host""" | |
65 # first of all we create the contact lists | |
66 self.host.contact_lists.add_profile(self.profile) | |
67 | |
68 # we get the essential params | |
69 self.bridge.param_get_a_async( | |
70 "JabberID", | |
71 "Connection", | |
72 profile_key=self.profile, | |
73 callback=self._plug_profile_jid, | |
74 errback=self._get_param_error, | |
75 ) | |
76 | |
77 def _plug_profile_jid(self, jid_s): | |
78 self.whoami = jid.JID(jid_s) # resource might change after the connection | |
79 log.info(f"Our current jid is: {self.whoami}") | |
80 self.bridge.is_connected(self.profile, callback=self._plug_profile_isconnected) | |
81 | |
82 def _autodisconnect_eb(self, failure_): | |
83 # XXX: we ignore error on this parameter, as Libervia can't access it | |
84 log.warning( | |
85 _("Error while trying to get autodisconnect param, ignoring: {}").format( | |
86 failure_ | |
87 ) | |
88 ) | |
89 self._plug_profile_autodisconnect("false") | |
90 | |
91 def _plug_profile_isconnected(self, connected): | |
92 self.connected = connected | |
93 if connected: | |
94 self.host.profile_connected(self.profile) | |
95 self.bridge.param_get_a_async( | |
96 "autodisconnect", | |
97 "Connection", | |
98 profile_key=self.profile, | |
99 callback=self._plug_profile_autodisconnect, | |
100 errback=self._autodisconnect_eb, | |
101 ) | |
102 | |
103 def _plug_profile_autodisconnect(self, autodisconnect): | |
104 if C.bool(autodisconnect): | |
105 self._autodisconnect = True | |
106 self.bridge.param_get_a_async( | |
107 "autoconnect", | |
108 "Connection", | |
109 profile_key=self.profile, | |
110 callback=self._plug_profile_autoconnect, | |
111 errback=self._get_param_error, | |
112 ) | |
113 | |
114 def _plug_profile_autoconnect(self, value_str): | |
115 autoconnect = C.bool(value_str) | |
116 if autoconnect and not self.connected: | |
117 self.host.connect( | |
118 self.profile, callback=lambda __: self._plug_profile_afterconnect() | |
119 ) | |
120 else: | |
121 self._plug_profile_afterconnect() | |
122 | |
123 def _plug_profile_afterconnect(self): | |
124 # Profile can be connected or not | |
125 # we get cached data | |
126 self.connected = True | |
127 self.host.bridge.features_get( | |
128 profile_key=self.profile, | |
129 callback=self._plug_profile_get_features_cb, | |
130 errback=self._plug_profile_get_features_eb, | |
131 ) | |
132 | |
133 def _plug_profile_get_features_eb(self, failure): | |
134 log.error("Couldn't get features: {}".format(failure)) | |
135 self._plug_profile_get_features_cb({}) | |
136 | |
137 def _plug_profile_get_features_cb(self, features): | |
138 self.host.features = features | |
139 self.host.bridge.entities_data_get([], ProfileManager.cache_keys_to_get, | |
140 profile=self.profile, | |
141 callback=self._plug_profile_got_cached_values, | |
142 errback=self._plug_profile_failed_cached_values) | |
143 | |
144 def _plug_profile_failed_cached_values(self, failure): | |
145 log.error("Couldn't get cached values: {}".format(failure)) | |
146 self._plug_profile_got_cached_values({}) | |
147 | |
148 def _plug_profile_got_cached_values(self, cached_values): | |
149 contact_list = self.host.contact_lists[self.profile] | |
150 # add the contact list and its listener | |
151 for entity_s, data in cached_values.items(): | |
152 for key, value in data.items(): | |
153 self.host.entity_data_updated_handler(entity_s, key, value, self.profile) | |
154 | |
155 if not self.connected: | |
156 self.host.set_presence_status(C.PRESENCE_UNAVAILABLE, "", profile=self.profile) | |
157 else: | |
158 | |
159 contact_list.fill() | |
160 self.host.set_presence_status(profile=self.profile) | |
161 | |
162 # The waiting subscription requests | |
163 self.bridge.sub_waiting_get( | |
164 self.profile, callback=self._plug_profile_got_waiting_sub | |
165 ) | |
166 | |
167 def _plug_profile_got_waiting_sub(self, waiting_sub): | |
168 for sub in waiting_sub: | |
169 self.host.subscribe_handler(waiting_sub[sub], sub, self.profile) | |
170 | |
171 self.bridge.muc_get_rooms_joined( | |
172 self.profile, callback=self._plug_profile_got_rooms_joined | |
173 ) | |
174 | |
175 def _plug_profile_got_rooms_joined(self, rooms_args): | |
176 # Now we open the MUC window where we already are: | |
177 for room_args in rooms_args: | |
178 self.host.muc_room_joined_handler(*room_args, profile=self.profile) | |
179 # Presence must be requested after rooms are filled | |
180 self.host.bridge.presence_statuses_get( | |
181 self.profile, callback=self._plug_profile_got_presences | |
182 ) | |
183 | |
184 def _plug_profile_got_presences(self, presences): | |
185 for contact in presences: | |
186 for res in presences[contact]: | |
187 jabber_id = ("%s/%s" % (jid.JID(contact).bare, res)) if res else contact | |
188 show = presences[contact][res][0] | |
189 priority = presences[contact][res][1] | |
190 statuses = presences[contact][res][2] | |
191 self.host.presence_update_handler( | |
192 jabber_id, show, priority, statuses, self.profile | |
193 ) | |
194 | |
195 # At this point, profile should be fully plugged | |
196 # and we launch frontend specific method | |
197 self.host.profile_plugged(self.profile) | |
198 | |
199 def _get_param_error(self, failure): | |
200 log.error(_("Can't get profile parameter: {msg}").format(msg=failure)) | |
201 | |
202 | |
203 class ProfilesManager(object): | |
204 """Class managing collection of profiles""" | |
205 | |
206 def __init__(self): | |
207 self._profiles = {} | |
208 | |
209 def __contains__(self, profile): | |
210 return profile in self._profiles | |
211 | |
212 def __iter__(self): | |
213 return iter(self._profiles.keys()) | |
214 | |
215 def __getitem__(self, profile): | |
216 return self._profiles[profile] | |
217 | |
218 def __len__(self): | |
219 return len(self._profiles) | |
220 | |
221 def items(self): | |
222 return self._profiles.items() | |
223 | |
224 def values(self): | |
225 return self._profiles.values() | |
226 | |
227 def plug(self, profile): | |
228 if profile in self._profiles: | |
229 raise exceptions.ConflictError( | |
230 "A profile of the name [{}] is already plugged".format(profile) | |
231 ) | |
232 self._profiles[profile] = ProfileManager(profile) | |
233 self._profiles[profile].plug() | |
234 | |
235 def unplug(self, profile): | |
236 if profile not in self._profiles: | |
237 raise ValueError("The profile [{}] is not plugged".format(profile)) | |
238 | |
239 # remove the contact list and its listener | |
240 host = self._profiles[profile].host | |
241 host.contact_lists[profile].unplug() | |
242 | |
243 del self._profiles[profile] | |
244 | |
245 def choose_one_profile(self): | |
246 return list(self._profiles.keys())[0] | |
247 | |
248 | |
249 class QuickApp(object): | |
250 """This class contain the main methods needed for the frontend""" | |
251 | |
252 MB_HANDLER = True #: Set to False if the frontend doesn't manage microblog | |
253 AVATARS_HANDLER = True #: set to False if avatars are not used | |
254 ENCRYPTION_HANDLERS = True #: set to False if encryption is handled separatly | |
255 #: if True, QuickApp will call resync itself, on all widgets at the same time | |
256 #: if False, frontend must call resync itself when suitable (e.g. widget is being | |
257 #: visible) | |
258 AUTO_RESYNC = True | |
259 | |
260 def __init__(self, bridge_factory, xmlui, check_options=None, connect_bridge=True): | |
261 """Create a frontend application | |
262 | |
263 @param bridge_factory: method to use to create the bridge | |
264 @param xmlui: xmlui module | |
265 @param check_options: method to call to check options (usually command line | |
266 arguments) | |
267 """ | |
268 self.xmlui = xmlui | |
269 self.menus = quick_menus.QuickMenusManager(self) | |
270 ProfileManager.host = self | |
271 self.profiles = ProfilesManager() | |
272 # profiles currently being plugged, used to (un)lock contact list updates | |
273 self._plugs_in_progress = set() | |
274 self.ready_profiles = set() # profiles which are connected and ready | |
275 self.signals_cache = {} # used to keep signal received between start of | |
276 # plug_profile and when the profile is actualy ready | |
277 self.contact_lists = quick_contact_list.QuickContactListHandler(self) | |
278 self.widgets = quick_widgets.QuickWidgetsManager(self) | |
279 if check_options is not None: | |
280 self.options = check_options() | |
281 else: | |
282 self.options = None | |
283 | |
284 # see selected_widget setter and getter | |
285 self._selected_widget = None | |
286 | |
287 # listeners are callable watching events | |
288 self._listeners = {} # key: listener type ("avatar", "selected", etc), | |
289 # value: list of callbacks | |
290 | |
291 # triggers | |
292 self.trigger = ( | |
293 trigger.TriggerManager() | |
294 ) # trigger are used to change the default behaviour | |
295 | |
296 ## bridge ## | |
297 self.bridge = bridge_factory() | |
298 ProfileManager.bridge = self.bridge | |
299 if connect_bridge: | |
300 self.connect_bridge() | |
301 | |
302 # frontend notifications | |
303 self._notif_id = 0 | |
304 self._notifications = {} | |
305 # watched progresses and associated callbacks | |
306 self._progress_ids = {} | |
307 # available features | |
308 # FIXME: features are profile specific, to be checked | |
309 self.features = None | |
310 #: map of short name to namespaces | |
311 self.ns_map = {} | |
312 #: available encryptions | |
313 self.encryption_plugins = [] | |
314 # state of synchronisation with backend | |
315 self._sync = True | |
316 | |
317 def connect_bridge(self): | |
318 self.bridge.bridge_connect(callback=self._bridge_cb, errback=self._bridge_eb) | |
319 | |
320 def _namespaces_get_cb(self, ns_map): | |
321 self.ns_map = ns_map | |
322 | |
323 def _namespaces_get_eb(self, failure_): | |
324 log.error(_("Can't get namespaces map: {msg}").format(msg=failure_)) | |
325 | |
326 def _encryption_plugins_get_cb(self, plugins_ser): | |
327 self.encryption_plugins = data_format.deserialise(plugins_ser, type_check=list) | |
328 | |
329 def _encryption_plugins_get_eb(self, failure_): | |
330 log.warning(_("Can't retrieve encryption plugins: {msg}").format(msg=failure_)) | |
331 | |
332 def on_bridge_connected(self): | |
333 self.bridge.ready_get(self.on_backend_ready) | |
334 | |
335 def _bridge_cb(self): | |
336 self.register_signal("connected") | |
337 self.register_signal("disconnected") | |
338 self.register_signal("action_new") | |
339 self.register_signal("contact_new") | |
340 self.register_signal("message_new") | |
341 if self.ENCRYPTION_HANDLERS: | |
342 self.register_signal("message_encryption_started") | |
343 self.register_signal("message_encryption_stopped") | |
344 self.register_signal("presence_update") | |
345 self.register_signal("subscribe") | |
346 self.register_signal("param_update") | |
347 self.register_signal("contact_deleted") | |
348 self.register_signal("entity_data_updated") | |
349 self.register_signal("progress_started") | |
350 self.register_signal("progress_finished") | |
351 self.register_signal("progress_error") | |
352 self.register_signal("muc_room_joined", iface="plugin") | |
353 self.register_signal("muc_room_left", iface="plugin") | |
354 self.register_signal("muc_room_user_changed_nick", iface="plugin") | |
355 self.register_signal("muc_room_new_subject", iface="plugin") | |
356 self.register_signal("chat_state_received", iface="plugin") | |
357 self.register_signal("message_state", iface="plugin") | |
358 self.register_signal("ps_event", iface="plugin") | |
359 # useful for debugging | |
360 self.register_signal("_debug", iface="core") | |
361 | |
362 # FIXME: do it dynamically | |
363 quick_games.Tarot.register_signals(self) | |
364 quick_games.Quiz.register_signals(self) | |
365 quick_games.Radiocol.register_signals(self) | |
366 self.on_bridge_connected() | |
367 | |
368 def _bridge_eb(self, failure): | |
369 if isinstance(failure, exceptions.BridgeExceptionNoService): | |
370 print((_("Can't connect to SàT backend, are you sure it's launched ?"))) | |
371 sys.exit(C.EXIT_BACKEND_NOT_FOUND) | |
372 elif isinstance(failure, exceptions.BridgeInitError): | |
373 print((_("Can't init bridge"))) | |
374 sys.exit(C.EXIT_BRIDGE_ERROR) | |
375 else: | |
376 print((_("Error while initialising bridge: {}".format(failure)))) | |
377 | |
378 def on_backend_ready(self): | |
379 log.info("backend is ready") | |
380 self.bridge.namespaces_get( | |
381 callback=self._namespaces_get_cb, errback=self._namespaces_get_eb) | |
382 # we cache available encryption plugins, as we'll use them on each | |
383 # new chat widget | |
384 self.bridge.encryption_plugins_get( | |
385 callback=self._encryption_plugins_get_cb, | |
386 errback=self._encryption_plugins_get_eb) | |
387 | |
388 | |
389 @property | |
390 def current_profile(self): | |
391 """Profile that a user would expect to use""" | |
392 try: | |
393 return self.selected_widget.profile | |
394 except (TypeError, AttributeError): | |
395 return self.profiles.choose_one_profile() | |
396 | |
397 @property | |
398 def visible_widgets(self): | |
399 """Widgets currently visible | |
400 | |
401 This must be implemented by frontend | |
402 @return (iter[object]): iterable on visible widgets | |
403 widgets can be QuickWidgets or not | |
404 """ | |
405 raise NotImplementedError | |
406 | |
407 @property | |
408 def visible_quick_widgets(self): | |
409 """QuickWidgets currently visible | |
410 | |
411 This generator iterate only on QuickWidgets, discarding other kinds of | |
412 widget the frontend may have. | |
413 @return (iter[object]): iterable on visible widgets | |
414 """ | |
415 for w in self.visisble_widgets: | |
416 if isinstance(w, quick_widgets.QuickWidget): | |
417 return w | |
418 | |
419 @property | |
420 def selected_widget(self): | |
421 """widget currently selected | |
422 | |
423 This must be set by frontend using setter. | |
424 """ | |
425 return self._selected_widget | |
426 | |
427 @selected_widget.setter | |
428 def selected_widget(self, wid): | |
429 """Set the currently selected widget | |
430 | |
431 Must be set by frontend | |
432 """ | |
433 if self._selected_widget == wid: | |
434 return | |
435 self._selected_widget = wid | |
436 try: | |
437 on_selected = wid.on_selected | |
438 except AttributeError: | |
439 pass | |
440 else: | |
441 on_selected() | |
442 | |
443 self.call_listeners("selected", wid) | |
444 | |
445 # backend state management | |
446 | |
447 @property | |
448 def sync(self): | |
449 """Synchronization flag | |
450 | |
451 True if this frontend is synchronised with backend | |
452 """ | |
453 return self._sync | |
454 | |
455 @sync.setter | |
456 def sync(self, state): | |
457 """Called when backend is desynchronised or resynchronising | |
458 | |
459 @param state(bool): True: if the backend is resynchronising | |
460 False when we lose synchronisation, for instance if frontend is going to sleep | |
461 or if connection has been lost and a reconnection is needed | |
462 """ | |
463 if state: | |
464 log.debug("we are synchronised with server") | |
465 if self.AUTO_RESYNC: | |
466 # we are resynchronising all widgets | |
467 log.debug("doing a full widgets resynchronisation") | |
468 for w in self.widgets: | |
469 try: | |
470 resync = w.resync | |
471 except AttributeError: | |
472 pass | |
473 else: | |
474 resync() | |
475 self.contact_lists.fill() | |
476 | |
477 self._sync = state | |
478 else: | |
479 log.debug("we have lost synchronisation with server") | |
480 self._sync = state | |
481 # we've lost synchronisation, all widgets must be notified | |
482 # note: this is always called independently of AUTO_RESYNC | |
483 for w in self.widgets: | |
484 try: | |
485 w.sync = False | |
486 except AttributeError: | |
487 pass | |
488 | |
489 def register_signal( | |
490 self, function_name, handler=None, iface="core", with_profile=True | |
491 ): | |
492 """Register a handler for a signal | |
493 | |
494 @param function_name (str): name of the signal to handle | |
495 @param handler (instancemethod): method to call when the signal arrive, | |
496 None for calling an automatically named handler (function_name + 'Handler') | |
497 @param iface (str): interface of the bridge to use ('core' or 'plugin') | |
498 @param with_profile (boolean): True if the signal concerns a specific profile, | |
499 in that case the profile name has to be passed by the caller | |
500 """ | |
501 log.debug("registering signal {name}".format(name=function_name)) | |
502 if handler is None: | |
503 handler = getattr(self, "{}{}".format(function_name, "_handler")) | |
504 if not with_profile: | |
505 self.bridge.register_signal(function_name, handler, iface) | |
506 return | |
507 | |
508 def signal_received(*args, **kwargs): | |
509 profile = kwargs.get("profile") | |
510 if profile is None: | |
511 if not args: | |
512 raise exceptions.ProfileNotSetError | |
513 profile = args[-1] | |
514 if profile is not None: | |
515 if not self.check_profile(profile): | |
516 if profile in self.profiles: | |
517 # profile is not ready but is in self.profiles, that's mean that | |
518 # it's being connecting and we need to cache the signal | |
519 self.signals_cache.setdefault(profile, []).append( | |
520 (function_name, handler, args, kwargs) | |
521 ) | |
522 return # we ignore signal for profiles we don't manage | |
523 handler(*args, **kwargs) | |
524 | |
525 self.bridge.register_signal(function_name, signal_received, iface) | |
526 | |
527 def addListener(self, type_, callback, profiles_filter=None): | |
528 """Add a listener for an event | |
529 | |
530 /!\ don't forget to remove listener when not used anymore (e.g. if you delete a | |
531 widget) | |
532 @param type_: type of event, can be: | |
533 - contactsFilled: called when contact have been fully filled for a profiles | |
534 kwargs: profile | |
535 - avatar: called when avatar data is updated | |
536 args: (entity, avatar_data, profile) | |
537 - nicknames: called when nicknames data is updated | |
538 args: (entity, nicknames, profile) | |
539 - presence: called when a presence is received | |
540 args: (entity, show, priority, statuses, profile) | |
541 - selected: called when a widget is selected | |
542 args: (selected_widget,) | |
543 - notification: called when a new notification is emited | |
544 args: (entity, notification_data, profile) | |
545 - notificationsClear: called when notifications are cleared | |
546 args: (entity, type_, profile) | |
547 - widgetNew: a new QuickWidget has been created | |
548 args: (widget,) | |
549 - widgetDeleted: all instances of a widget with specific hash have been | |
550 deleted | |
551 args: (widget_deleted,) | |
552 - menu: called when a menu item is added or removed | |
553 args: (type_, path, path_i18n, item) were values are: | |
554 type_: same as in [sat.core.sat_main.SAT.import_menu] | |
555 path: same as in [sat.core.sat_main.SAT.import_menu] | |
556 path_i18n: translated path (or None if the item is removed) | |
557 item: instance of quick_menus.MenuItemBase or None if the item is | |
558 removed | |
559 - gotMenus: called only once when menu are available (no arg) | |
560 - progress_finished: called when a progressing action has just finished | |
561 args: (progress_id, metadata, profile) | |
562 - progress_error: called when a progressing action failed | |
563 args: (progress_id, error_msg, profile): | |
564 @param callback: method to call on event | |
565 @param profiles_filter (set[unicode]): if set and not empty, the | |
566 listener will be callable only by one of the given profiles. | |
567 """ | |
568 assert type_ in C.LISTENERS | |
569 self._listeners.setdefault(type_, {})[callback] = profiles_filter | |
570 | |
571 def removeListener(self, type_, callback, ignore_missing=False): | |
572 """Remove a callback from listeners | |
573 | |
574 @param type_(str): same as for [addListener] | |
575 @param callback(callable): callback to remove | |
576 @param ignore_missing(bool): if True, don't log error if the listener doesn't | |
577 exist | |
578 """ | |
579 assert type_ in C.LISTENERS | |
580 try: | |
581 self._listeners[type_].pop(callback) | |
582 except KeyError: | |
583 if not ignore_missing: | |
584 log.error( | |
585 f"Trying to remove an inexisting listener (type = {type_}): " | |
586 f"{callback}") | |
587 | |
588 def call_listeners(self, type_, *args, **kwargs): | |
589 """Call the methods which listen type_ event. If a profiles filter has | |
590 been register with a listener and profile argument is not None, the | |
591 listener will be called only if profile is in the profiles filter list. | |
592 | |
593 @param type_: same as for [addListener] | |
594 @param *args: arguments sent to callback | |
595 @param **kwargs: keywords argument, mainly used to pass "profile" when needed | |
596 """ | |
597 assert type_ in C.LISTENERS | |
598 try: | |
599 listeners = self._listeners[type_] | |
600 except KeyError: | |
601 pass | |
602 else: | |
603 profile = kwargs.get("profile") | |
604 for listener, profiles_filter in list(listeners.items()): | |
605 if profile is None or not profiles_filter or profile in profiles_filter: | |
606 listener(*args, **kwargs) | |
607 | |
608 def check_profile(self, profile): | |
609 """Tell if the profile is currently followed by the application, and ready""" | |
610 return profile in self.ready_profiles | |
611 | |
612 def post_init(self, profile_manager): | |
613 """Must be called after initialization is done, do all automatic task | |
614 | |
615 (auto plug profile) | |
616 @param profile_manager: instance of a subclass of | |
617 Quick_frontend.QuickProfileManager | |
618 """ | |
619 if self.options and self.options.profile: | |
620 profile_manager.autoconnect([self.options.profile]) | |
621 | |
622 def profile_plugged(self, profile): | |
623 """Method called when the profile is fully plugged | |
624 | |
625 This will launch frontend specific workflow | |
626 | |
627 /!\ if you override the method and don't call the parent, be sure to add the | |
628 profile to ready_profiles ! if you don't, all signals will stay in cache | |
629 | |
630 @param profile(unicode): %(doc_profile)s | |
631 """ | |
632 self._plugs_in_progress.remove(profile) | |
633 self.ready_profiles.add(profile) | |
634 | |
635 # profile is ready, we can call send signals that where is cache | |
636 cached_signals = self.signals_cache.pop(profile, []) | |
637 for function_name, handler, args, kwargs in cached_signals: | |
638 log.debug( | |
639 "Calling cached signal [%s] with args %s and kwargs %s" | |
640 % (function_name, args, kwargs) | |
641 ) | |
642 handler(*args, **kwargs) | |
643 | |
644 self.call_listeners("profile_plugged", profile=profile) | |
645 if not self._plugs_in_progress: | |
646 self.contact_lists.lock_update(False) | |
647 | |
648 def profile_connected(self, profile): | |
649 """Called when a plugged profile is connected | |
650 | |
651 it is called independently of profile_plugged (may be called before or after | |
652 profile_plugged) | |
653 """ | |
654 pass | |
655 | |
656 def connect(self, profile, callback=None, errback=None): | |
657 if not callback: | |
658 callback = lambda __: None | |
659 if not errback: | |
660 | |
661 def errback(failure): | |
662 log.error(_("Can't connect profile [%s]") % failure) | |
663 try: | |
664 module = failure.module | |
665 except AttributeError: | |
666 module = "" | |
667 try: | |
668 message = failure.message | |
669 except AttributeError: | |
670 message = "error" | |
671 try: | |
672 fullname = failure.fullname | |
673 except AttributeError: | |
674 fullname = "error" | |
675 if ( | |
676 module.startswith("twisted.words.protocols.jabber") | |
677 and failure.condition == "not-authorized" | |
678 ): | |
679 self.action_launch(C.CHANGE_XMPP_PASSWD_ID, {}, profile=profile) | |
680 else: | |
681 self.show_dialog(message, fullname, "error") | |
682 | |
683 self.bridge.connect(profile, callback=callback, errback=errback) | |
684 | |
685 def plug_profiles(self, profiles): | |
686 """Tell application which profiles must be used | |
687 | |
688 @param profiles: list of valid profile names | |
689 """ | |
690 self.contact_lists.lock_update() | |
691 self._plugs_in_progress.update(profiles) | |
692 self.plugging_profiles() | |
693 for profile in profiles: | |
694 self.profiles.plug(profile) | |
695 | |
696 def plugging_profiles(self): | |
697 """Method to subclass to manage frontend specific things to do | |
698 | |
699 will be called when profiles are choosen and are to be plugged soon | |
700 """ | |
701 pass | |
702 | |
703 def unplug_profile(self, profile): | |
704 """Tell the application to not follow anymore the profile""" | |
705 if not profile in self.profiles: | |
706 raise ValueError("The profile [{}] is not plugged".format(profile)) | |
707 self.profiles.unplug(profile) | |
708 | |
709 def clear_profile(self): | |
710 self.profiles.clear() | |
711 | |
712 def new_widget(self, widget): | |
713 raise NotImplementedError | |
714 | |
715 # bridge signals hanlers | |
716 | |
717 def connected_handler(self, jid_s, profile): | |
718 """Called when the connection is made. | |
719 | |
720 @param jid_s (unicode): the JID that we were assigned by the server, | |
721 as the resource might differ from the JID we asked for. | |
722 """ | |
723 log.debug(_("Connected")) | |
724 self.profiles[profile].whoami = jid.JID(jid_s) | |
725 self.set_presence_status(profile=profile) | |
726 # FIXME: fill() is already called for all profiles when doing self.sync = True | |
727 # a per-profile fill() should be done once, see below note | |
728 self.contact_lists[profile].fill() | |
729 # if we were already displaying widgets, they must be resynchronized | |
730 # FIXME: self.sync is for all profiles | |
731 # while (dis)connection is per-profile. | |
732 # A mechanism similar to sync should be available | |
733 # on a per-profile basis | |
734 self.sync = True | |
735 self.profile_connected(profile) | |
736 | |
737 def disconnected_handler(self, profile): | |
738 """called when the connection is closed""" | |
739 log.debug(_("Disconnected")) | |
740 self.contact_lists[profile].disconnect() | |
741 # FIXME: see note on connected_handler | |
742 self.sync = False | |
743 self.set_presence_status(C.PRESENCE_UNAVAILABLE, "", profile=profile) | |
744 | |
745 def action_new_handler(self, action_data_s, id_, security_limit, profile): | |
746 self.action_manager( | |
747 data_format.deserialise(action_data_s), user_action=False, profile=profile | |
748 ) | |
749 | |
750 def contact_new_handler(self, jid_s, attributes, groups, profile): | |
751 entity = jid.JID(jid_s) | |
752 groups = list(groups) | |
753 self.contact_lists[profile].set_contact(entity, groups, attributes, in_roster=True) | |
754 | |
755 def message_new_handler( | |
756 self, uid, timestamp, from_jid_s, to_jid_s, msg, subject, type_, extra_s, | |
757 profile): | |
758 from_jid = jid.JID(from_jid_s) | |
759 to_jid = jid.JID(to_jid_s) | |
760 extra = data_format.deserialise(extra_s) | |
761 if not self.trigger.point( | |
762 "messageNewTrigger", uid, timestamp, from_jid, to_jid, msg, subject, type_, | |
763 extra, profile=profile,): | |
764 return | |
765 | |
766 from_me = from_jid.bare == self.profiles[profile].whoami.bare | |
767 mess_to_jid = to_jid if from_me else from_jid | |
768 target = mess_to_jid.bare | |
769 contact_list = self.contact_lists[profile] | |
770 | |
771 try: | |
772 is_room = contact_list.is_room(target) | |
773 except exceptions.NotFound: | |
774 is_room = False | |
775 | |
776 if target.resource and not is_room: | |
777 # we avoid resource locking, but we must keep resource for private MUC | |
778 # messages | |
779 target = target | |
780 # we want to be sure to have at least one QuickChat instance | |
781 self.widgets.get_or_create_widget( | |
782 quick_chat.QuickChat, | |
783 target, | |
784 type_ = C.CHAT_GROUP if is_room else C.CHAT_ONE2ONE, | |
785 on_new_widget = None, | |
786 profile = profile, | |
787 ) | |
788 | |
789 if ( | |
790 not from_jid in contact_list | |
791 and from_jid.bare != self.profiles[profile].whoami.bare | |
792 ): | |
793 # XXX: needed to show entities which haven't sent any | |
794 # presence information and which are not in roster | |
795 contact_list.set_contact(from_jid) | |
796 | |
797 # we dispatch the message in the widgets | |
798 for widget in self.widgets.get_widgets( | |
799 quick_chat.QuickChat, target=target, profiles=(profile,) | |
800 ): | |
801 widget.message_new( | |
802 uid, timestamp, from_jid, mess_to_jid, msg, subject, type_, extra, profile | |
803 ) | |
804 | |
805 def message_encryption_started_handler(self, destinee_jid_s, plugin_data, profile): | |
806 destinee_jid = jid.JID(destinee_jid_s) | |
807 plugin_data = data_format.deserialise(plugin_data) | |
808 for widget in self.widgets.get_widgets(quick_chat.QuickChat, | |
809 target=destinee_jid.bare, | |
810 profiles=(profile,)): | |
811 widget.message_encryption_started(plugin_data) | |
812 | |
813 def message_encryption_stopped_handler(self, destinee_jid_s, plugin_data, profile): | |
814 destinee_jid = jid.JID(destinee_jid_s) | |
815 for widget in self.widgets.get_widgets(quick_chat.QuickChat, | |
816 target=destinee_jid.bare, | |
817 profiles=(profile,)): | |
818 widget.message_encryption_stopped(plugin_data) | |
819 | |
820 def message_state_handler(self, uid, status, profile): | |
821 for widget in self.widgets.get_widgets(quick_chat.QuickChat, profiles=(profile,)): | |
822 widget.on_message_state(uid, status, profile) | |
823 | |
824 def message_send(self, to_jid, message, subject=None, mess_type="auto", extra=None, callback=None, errback=None, profile_key=C.PROF_KEY_NONE): | |
825 if not subject and not extra and (not message or message == {'': ''}): | |
826 log.debug("Not sending empty message") | |
827 return | |
828 | |
829 if subject is None: | |
830 subject = {} | |
831 if extra is None: | |
832 extra = {} | |
833 if callback is None: | |
834 callback = ( | |
835 lambda __=None: None | |
836 ) # FIXME: optional argument is here because pyjamas doesn't support callback | |
837 # without arg with json proxy | |
838 if errback is None: | |
839 errback = lambda failure: self.show_dialog( | |
840 message=failure.message, title=failure.fullname, type="error" | |
841 ) | |
842 | |
843 if not self.trigger.point("messageSendTrigger", to_jid, message, subject, mess_type, extra, callback, errback, profile_key=profile_key): | |
844 return | |
845 | |
846 self.bridge.message_send( | |
847 str(to_jid), | |
848 message, | |
849 subject, | |
850 mess_type, | |
851 data_format.serialise(extra), | |
852 profile_key, | |
853 callback=callback, | |
854 errback=errback, | |
855 ) | |
856 | |
857 def set_presence_status(self, show="", status=None, profile=C.PROF_KEY_NONE): | |
858 raise NotImplementedError | |
859 | |
860 def presence_update_handler(self, entity_s, show, priority, statuses, profile): | |
861 # XXX: this log is commented because it's really too verbose even for DEBUG logs | |
862 # but it is kept here as it may still be useful for troubleshooting | |
863 # log.debug( | |
864 # _( | |
865 # u"presence update for %(entity)s (show=%(show)s, priority=%(priority)s, " | |
866 # u"statuses=%(statuses)s) [profile:%(profile)s]" | |
867 # ) | |
868 # % { | |
869 # "entity": entity_s, | |
870 # C.PRESENCE_SHOW: show, | |
871 # C.PRESENCE_PRIORITY: priority, | |
872 # C.PRESENCE_STATUSES: statuses, | |
873 # "profile": profile, | |
874 # } | |
875 # ) | |
876 entity = jid.JID(entity_s) | |
877 | |
878 if entity == self.profiles[profile].whoami: | |
879 if show == C.PRESENCE_UNAVAILABLE: | |
880 self.set_presence_status(C.PRESENCE_UNAVAILABLE, "", profile=profile) | |
881 else: | |
882 # FIXME: try to retrieve user language status before fallback to default | |
883 status = statuses.get(C.PRESENCE_STATUSES_DEFAULT, None) | |
884 self.set_presence_status(show, status, profile=profile) | |
885 return | |
886 | |
887 self.call_listeners("presence", entity, show, priority, statuses, profile=profile) | |
888 | |
889 def muc_room_joined_handler( | |
890 self, room_jid_s, occupants, user_nick, subject, statuses, profile): | |
891 """Called when a MUC room is joined""" | |
892 log.debug( | |
893 "Room [{room_jid}] joined by {profile}, users presents:{users}".format( | |
894 room_jid=room_jid_s, profile=profile, users=list(occupants.keys()) | |
895 ) | |
896 ) | |
897 room_jid = jid.JID(room_jid_s) | |
898 self.contact_lists[profile].set_special(room_jid, C.CONTACT_SPECIAL_GROUP) | |
899 self.widgets.get_or_create_widget( | |
900 quick_chat.QuickChat, | |
901 room_jid, | |
902 type_=C.CHAT_GROUP, | |
903 nick=user_nick, | |
904 occupants=occupants, | |
905 subject=subject, | |
906 statuses=statuses, | |
907 profile=profile, | |
908 ) | |
909 | |
910 def muc_room_left_handler(self, room_jid_s, profile): | |
911 """Called when a MUC room is left""" | |
912 log.debug( | |
913 "Room [%(room_jid)s] left by %(profile)s" | |
914 % {"room_jid": room_jid_s, "profile": profile} | |
915 ) | |
916 room_jid = jid.JID(room_jid_s) | |
917 chat_widget = self.widgets.get_widget(quick_chat.QuickChat, room_jid, profile) | |
918 if chat_widget: | |
919 self.widgets.delete_widget( | |
920 chat_widget, all_instances=True, explicit_close=True) | |
921 self.contact_lists[profile].remove_contact(room_jid) | |
922 | |
923 def muc_room_user_changed_nick_handler(self, room_jid_s, old_nick, new_nick, profile): | |
924 """Called when an user joined a MUC room""" | |
925 room_jid = jid.JID(room_jid_s) | |
926 chat_widget = self.widgets.get_or_create_widget( | |
927 quick_chat.QuickChat, room_jid, type_=C.CHAT_GROUP, profile=profile | |
928 ) | |
929 chat_widget.change_user_nick(old_nick, new_nick) | |
930 log.debug( | |
931 "user [%(old_nick)s] is now known as [%(new_nick)s] in room [%(room_jid)s]" | |
932 % {"old_nick": old_nick, "new_nick": new_nick, "room_jid": room_jid} | |
933 ) | |
934 | |
935 def muc_room_new_subject_handler(self, room_jid_s, subject, profile): | |
936 """Called when subject of MUC room change""" | |
937 room_jid = jid.JID(room_jid_s) | |
938 chat_widget = self.widgets.get_or_create_widget( | |
939 quick_chat.QuickChat, room_jid, type_=C.CHAT_GROUP, profile=profile | |
940 ) | |
941 chat_widget.set_subject(subject) | |
942 log.debug( | |
943 "new subject for room [%(room_jid)s]: %(subject)s" | |
944 % {"room_jid": room_jid, "subject": subject} | |
945 ) | |
946 | |
947 def chat_state_received_handler(self, from_jid_s, state, profile): | |
948 """Called when a new chat state (XEP-0085) is received. | |
949 | |
950 @param from_jid_s (unicode): JID of a contact or C.ENTITY_ALL | |
951 @param state (unicode): new state | |
952 @param profile (unicode): current profile | |
953 """ | |
954 from_jid = jid.JID(from_jid_s) | |
955 for widget in self.widgets.get_widgets(quick_chat.QuickChat, target=from_jid.bare, | |
956 profiles=(profile,)): | |
957 widget.on_chat_state(from_jid, state, profile) | |
958 | |
959 def notify(self, type_, entity=None, message=None, subject=None, callback=None, | |
960 cb_args=None, widget=None, profile=C.PROF_KEY_NONE): | |
961 """Trigger an event notification | |
962 | |
963 @param type_(unicode): notifation kind, | |
964 one of C.NOTIFY_* constant or any custom type specific to frontend | |
965 @param entity(jid.JID, None): entity involved in the notification | |
966 if entity is in contact list, a indicator may be added in front of it | |
967 @param message(unicode, None): message of the notification | |
968 @param subject(unicode, None): subject of the notification | |
969 @param callback(callable, None): method to call when notification is selected | |
970 @param cb_args(list, None): list of args for callback | |
971 @param widget(object, None): widget where the notification happened | |
972 """ | |
973 assert type_ in C.NOTIFY_ALL | |
974 notif_dict = self.profiles[profile].notifications | |
975 key = "" if entity is None else entity.bare | |
976 type_notifs = notif_dict.setdefault(key, {}).setdefault(type_, []) | |
977 notif_data = { | |
978 "id": self._notif_id, | |
979 "time": time.time(), | |
980 "entity": entity, | |
981 "callback": callback, | |
982 "cb_args": cb_args, | |
983 "message": message, | |
984 "subject": subject, | |
985 } | |
986 if widget is not None: | |
987 notif_data[widget] = widget | |
988 type_notifs.append(notif_data) | |
989 self._notifications[self._notif_id] = notif_data | |
990 self._notif_id += 1 | |
991 self.call_listeners("notification", entity, notif_data, profile=profile) | |
992 | |
993 def get_notifs(self, entity=None, type_=None, exact_jid=None, profile=C.PROF_KEY_NONE): | |
994 """return notifications for given entity | |
995 | |
996 @param entity(jid.JID, None, C.ENTITY_ALL): jid of the entity to check | |
997 bare jid to get all notifications, full jid to filter on resource | |
998 None to get general notifications | |
999 C.ENTITY_ALL to get all notifications | |
1000 @param type_(unicode, None): notification type to filter | |
1001 None to get all notifications | |
1002 @param exact_jid(bool, None): if True, only return notifications from | |
1003 exact entity jid (i.e. not including other resources) | |
1004 None for automatic selection (True for full jid, False else) | |
1005 False to get resources notifications | |
1006 False doesn't do anything if entity is not a bare jid | |
1007 @return (iter[dict]): notifications | |
1008 """ | |
1009 main_notif_dict = self.profiles[profile].notifications | |
1010 | |
1011 if entity is C.ENTITY_ALL: | |
1012 selected_notifs = iter(main_notif_dict.values()) | |
1013 exact_jid = False | |
1014 else: | |
1015 if entity is None: | |
1016 key = "" | |
1017 exact_jid = False | |
1018 else: | |
1019 key = entity.bare | |
1020 if exact_jid is None: | |
1021 exact_jid = bool(entity.resource) | |
1022 selected_notifs = (main_notif_dict.setdefault(key, {}),) | |
1023 | |
1024 for notifs_from_select in selected_notifs: | |
1025 | |
1026 if type_ is None: | |
1027 type_notifs = iter(notifs_from_select.values()) | |
1028 else: | |
1029 type_notifs = (notifs_from_select.get(type_, []),) | |
1030 | |
1031 for notifs in type_notifs: | |
1032 for notif in notifs: | |
1033 if exact_jid and notif["entity"] != entity: | |
1034 continue | |
1035 yield notif | |
1036 | |
1037 def clear_notifs(self, entity, type_=None, profile=C.PROF_KEY_NONE): | |
1038 """return notifications for given entity | |
1039 | |
1040 @param entity(jid.JID, None): bare jid of the entity to check | |
1041 None to clear general notifications (but keep entities ones) | |
1042 @param type_(unicode, None): notification type to filter | |
1043 None to clear all notifications | |
1044 @return (list[dict]): list of notifications | |
1045 """ | |
1046 notif_dict = self.profiles[profile].notifications | |
1047 key = "" if entity is None else entity.bare | |
1048 try: | |
1049 if type_ is None: | |
1050 del notif_dict[key] | |
1051 else: | |
1052 del notif_dict[key][type_] | |
1053 except KeyError: | |
1054 return | |
1055 self.call_listeners("notificationsClear", entity, type_, profile=profile) | |
1056 | |
1057 def ps_event_handler(self, category, service_s, node, event_type, data, profile): | |
1058 """Called when a PubSub event is received. | |
1059 | |
1060 @param category(unicode): event category (e.g. "PEP", "MICROBLOG") | |
1061 @param service_s (unicode): pubsub service | |
1062 @param node (unicode): pubsub node | |
1063 @param event_type (unicode): event type (one of C.PUBLISH, C.RETRACT, C.DELETE) | |
1064 @param data (serialised_dict): event data | |
1065 """ | |
1066 data = data_format.deserialise(data) | |
1067 service_s = jid.JID(service_s) | |
1068 | |
1069 if category == C.PS_MICROBLOG and self.MB_HANDLER: | |
1070 if event_type == C.PS_PUBLISH: | |
1071 if not "content" in data: | |
1072 log.warning("No content found in microblog data") | |
1073 return | |
1074 | |
1075 # FIXME: check if [] make sense (instead of None) | |
1076 _groups = data.get("group") | |
1077 | |
1078 for wid in self.widgets.get_widgets(quick_blog.QuickBlog): | |
1079 wid.add_entry_if_accepted(service_s, node, data, _groups, profile) | |
1080 | |
1081 try: | |
1082 comments_node, comments_service = ( | |
1083 data["comments_node"], | |
1084 data["comments_service"], | |
1085 ) | |
1086 except KeyError: | |
1087 pass | |
1088 else: | |
1089 self.bridge.mb_get( | |
1090 comments_service, | |
1091 comments_node, | |
1092 C.NO_LIMIT, | |
1093 [], | |
1094 {"subscribe": C.BOOL_TRUE}, | |
1095 profile=profile, | |
1096 ) | |
1097 elif event_type == C.PS_RETRACT: | |
1098 for wid in self.widgets.get_widgets(quick_blog.QuickBlog): | |
1099 wid.delete_entry_if_present(service_s, node, data["id"], profile) | |
1100 pass | |
1101 else: | |
1102 log.warning("Unmanaged PubSub event type {}".format(event_type)) | |
1103 | |
1104 def register_progress_cbs(self, progress_id, callback, errback): | |
1105 """Register progression callbacks | |
1106 | |
1107 @param progress_id(unicode): id of the progression to check | |
1108 @param callback(callable, None): method to call when progressing action | |
1109 successfuly finished. | |
1110 None to ignore | |
1111 @param errback(callable, None): method to call when progressions action failed | |
1112 None to ignore | |
1113 """ | |
1114 callbacks = self._progress_ids.setdefault(progress_id, []) | |
1115 callbacks.append((callback, errback)) | |
1116 | |
1117 def progress_started_handler(self, pid, metadata, profile): | |
1118 log.info("Progress {} started".format(pid)) | |
1119 | |
1120 def progress_finished_handler(self, pid, metadata, profile): | |
1121 log.info("Progress {} finished".format(pid)) | |
1122 try: | |
1123 callbacks = self._progress_ids.pop(pid) | |
1124 except KeyError: | |
1125 pass | |
1126 else: | |
1127 for callback, __ in callbacks: | |
1128 if callback is not None: | |
1129 callback(metadata, profile=profile) | |
1130 self.call_listeners("progress_finished", pid, metadata, profile=profile) | |
1131 | |
1132 def progress_error_handler(self, pid, err_msg, profile): | |
1133 log.warning("Progress {pid} error: {err_msg}".format(pid=pid, err_msg=err_msg)) | |
1134 try: | |
1135 callbacks = self._progress_ids.pop(pid) | |
1136 except KeyError: | |
1137 pass | |
1138 else: | |
1139 for __, errback in callbacks: | |
1140 if errback is not None: | |
1141 errback(err_msg, profile=profile) | |
1142 self.call_listeners("progress_error", pid, err_msg, profile=profile) | |
1143 | |
1144 def _subscribe_cb(self, answer, data): | |
1145 entity, profile = data | |
1146 type_ = "subscribed" if answer else "unsubscribed" | |
1147 self.bridge.subscription(type_, str(entity.bare), profile_key=profile) | |
1148 | |
1149 def subscribe_handler(self, type, raw_jid, profile): | |
1150 """Called when a subsciption management signal is received""" | |
1151 entity = jid.JID(raw_jid) | |
1152 if type == "subscribed": | |
1153 # this is a subscription confirmation, we just have to inform user | |
1154 # TODO: call self.getEntityMBlog to add the new contact blogs | |
1155 self.show_dialog( | |
1156 _("The contact {contact} has accepted your subscription").format( | |
1157 contact=entity.bare | |
1158 ), | |
1159 _("Subscription confirmation"), | |
1160 ) | |
1161 elif type == "unsubscribed": | |
1162 # this is a subscription refusal, we just have to inform user | |
1163 self.show_dialog( | |
1164 _("The contact {contact} has refused your subscription").format( | |
1165 contact=entity.bare | |
1166 ), | |
1167 _("Subscription refusal"), | |
1168 "error", | |
1169 ) | |
1170 elif type == "subscribe": | |
1171 # this is a subscriptionn request, we have to ask for user confirmation | |
1172 # TODO: use sat.stdui.ui_contact_list to display the groups selector | |
1173 self.show_dialog( | |
1174 _( | |
1175 "The contact {contact} wants to subscribe to your presence" | |
1176 ".\nDo you accept ?" | |
1177 ).format(contact=entity.bare), | |
1178 _("Subscription confirmation"), | |
1179 "yes/no", | |
1180 answer_cb=self._subscribe_cb, | |
1181 answer_data=(entity, profile), | |
1182 ) | |
1183 | |
1184 def _debug_handler(self, action, parameters, profile): | |
1185 if action == "widgets_dump": | |
1186 from pprint import pformat | |
1187 log.info("Widgets dump:\n{data}".format(data=pformat(self.widgets._widgets))) | |
1188 else: | |
1189 log.warning("Unknown debug action: {action}".format(action=action)) | |
1190 | |
1191 | |
1192 def show_dialog(self, message, title, type="info", answer_cb=None, answer_data=None): | |
1193 """Show a dialog to user | |
1194 | |
1195 Frontends must override this method | |
1196 @param message(unicode): body of the dialog | |
1197 @param title(unicode): title of the dialog | |
1198 @param type(unicode): one of: | |
1199 - "info": information dialog (callbacks not used) | |
1200 - "warning": important information to notice (callbacks not used) | |
1201 - "error": something went wrong (callbacks not used) | |
1202 - "yes/no": a dialog with 2 choices (yes and no) | |
1203 @param answer_cb(callable): method to call on answer. | |
1204 Arguments depend on dialog type: | |
1205 - "yes/no": argument is a boolean (True for yes) | |
1206 @param answer_data(object): data to link on callback | |
1207 """ | |
1208 # FIXME: misnamed method + types are not well chosen. Need to be rethought | |
1209 raise NotImplementedError | |
1210 | |
1211 def show_alert(self, message): | |
1212 # FIXME: doesn't seems used anymore, to remove? | |
1213 pass # FIXME | |
1214 | |
1215 def dialog_failure(self, failure): | |
1216 log.warning("Failure: {}".format(failure)) | |
1217 | |
1218 def progress_id_handler(self, progress_id, profile): | |
1219 """Callback used when an action result in a progress id""" | |
1220 log.info("Progress ID received: {}".format(progress_id)) | |
1221 | |
1222 def is_hidden(self): | |
1223 """Tells if the frontend window is hidden. | |
1224 | |
1225 @return bool | |
1226 """ | |
1227 raise NotImplementedError | |
1228 | |
1229 def param_update_handler(self, name, value, namespace, profile): | |
1230 log.debug( | |
1231 _("param update: [%(namespace)s] %(name)s = %(value)s") | |
1232 % {"namespace": namespace, "name": name, "value": value} | |
1233 ) | |
1234 if (namespace, name) == ("Connection", "JabberID"): | |
1235 log.debug(_("Changing JID to %s") % value) | |
1236 self.profiles[profile].whoami = jid.JID(value) | |
1237 elif (namespace, name) == ("General", C.SHOW_OFFLINE_CONTACTS): | |
1238 self.contact_lists[profile].show_offline_contacts(C.bool(value)) | |
1239 elif (namespace, name) == ("General", C.SHOW_EMPTY_GROUPS): | |
1240 self.contact_lists[profile].show_empty_groups(C.bool(value)) | |
1241 | |
1242 def contact_deleted_handler(self, jid_s, profile): | |
1243 target = jid.JID(jid_s) | |
1244 self.contact_lists[profile].remove_contact(target) | |
1245 | |
1246 def entity_data_updated_handler(self, entity_s, key, value_raw, profile): | |
1247 entity = jid.JID(entity_s) | |
1248 value = data_format.deserialise(value_raw, type_check=None) | |
1249 if key == "nicknames": | |
1250 assert isinstance(value, list) or value is None | |
1251 if entity in self.contact_lists[profile]: | |
1252 self.contact_lists[profile].set_cache(entity, "nicknames", value) | |
1253 self.call_listeners("nicknames", entity, value, profile=profile) | |
1254 elif key == "avatar" and self.AVATARS_HANDLER: | |
1255 assert isinstance(value, dict) or value is None | |
1256 self.contact_lists[profile].set_cache(entity, "avatar", value) | |
1257 self.call_listeners("avatar", entity, value, profile=profile) | |
1258 | |
1259 def action_manager(self, action_data, callback=None, ui_show_cb=None, user_action=True, | |
1260 progress_cb=None, progress_eb=None, profile=C.PROF_KEY_NONE): | |
1261 """Handle backend action | |
1262 | |
1263 @param action_data(dict): action dict as sent by action_launch or returned by an | |
1264 UI action | |
1265 @param callback(None, callback): if not None, callback to use on XMLUI answer | |
1266 @param ui_show_cb(None, callback): if not None, method to call to show the XMLUI | |
1267 @param user_action(bool): if True, the action is a result of a user interaction | |
1268 else the action come from backend direclty (i.e. action_new). | |
1269 This is useful to know if the frontend can display a popup immediately (if | |
1270 True) or if it should add it to a queue that the user can activate later. | |
1271 @param progress_cb(None, callable): method to call when progression is finished. | |
1272 Only make sense if a progress is expected in this action | |
1273 @param progress_eb(None, callable): method to call when something went wrong | |
1274 during progression. | |
1275 Only make sense if a progress is expected in this action | |
1276 """ | |
1277 try: | |
1278 xmlui = action_data.pop("xmlui") | |
1279 except KeyError: | |
1280 pass | |
1281 else: | |
1282 ui = self.xmlui.create( | |
1283 self, | |
1284 xml_data=xmlui, | |
1285 flags=("FROM_BACKEND",) if not user_action else None, | |
1286 callback=callback, | |
1287 profile=profile, | |
1288 ) | |
1289 if ui_show_cb is None: | |
1290 ui.show() | |
1291 else: | |
1292 ui_show_cb(ui) | |
1293 | |
1294 try: | |
1295 progress_id = action_data.pop("progress") | |
1296 except KeyError: | |
1297 pass | |
1298 else: | |
1299 if progress_cb or progress_eb: | |
1300 self.register_progress_cbs(progress_id, progress_cb, progress_eb) | |
1301 self.progress_id_handler(progress_id, profile) | |
1302 | |
1303 def _action_cb(self, data, callback, callback_id, profile): | |
1304 if callback is None: | |
1305 self.action_manager(data, profile=profile) | |
1306 else: | |
1307 callback(data=data, cb_id=callback_id, profile=profile) | |
1308 | |
1309 def action_launch( | |
1310 self, callback_id, data=None, callback=None, profile=C.PROF_KEY_NONE | |
1311 ): | |
1312 """Launch a dynamic action | |
1313 | |
1314 @param callback_id: id of the action to launch | |
1315 @param data: data needed only for certain actions | |
1316 @param callback(callable, None): will be called with the resut | |
1317 if None, self.action_manager will be called | |
1318 else the callable will be called with the following kw parameters: | |
1319 - data: action_data | |
1320 - cb_id: callback id | |
1321 - profile: %(doc_profile)s | |
1322 @param profile: %(doc_profile)s | |
1323 | |
1324 """ | |
1325 if data is None: | |
1326 data = dict() | |
1327 action_cb = lambda data: self._action_cb( | |
1328 data_format.deserialise(data), callback, callback_id, profile | |
1329 ) | |
1330 self.bridge.action_launch( | |
1331 callback_id, data_format.serialise(data), profile, callback=action_cb, | |
1332 errback=self.dialog_failure | |
1333 ) | |
1334 | |
1335 def launch_menu( | |
1336 self, | |
1337 menu_type, | |
1338 path, | |
1339 data=None, | |
1340 callback=None, | |
1341 security_limit=C.SECURITY_LIMIT_MAX, | |
1342 profile=C.PROF_KEY_NONE, | |
1343 ): | |
1344 """Launch a menu manually | |
1345 | |
1346 @param menu_type(unicode): type of the menu to launch | |
1347 @param path(iterable[unicode]): path to the menu | |
1348 @param data: data needed only for certain actions | |
1349 @param callback(callable, None): will be called with the resut | |
1350 if None, self.action_manager will be called | |
1351 else the callable will be called with the following kw parameters: | |
1352 - data: action_data | |
1353 - cb_id: (menu_type, path) tuple | |
1354 - profile: %(doc_profile)s | |
1355 @param profile: %(doc_profile)s | |
1356 | |
1357 """ | |
1358 if data is None: | |
1359 data = dict() | |
1360 action_cb = lambda data: self._action_cb( | |
1361 data, callback, (menu_type, path), profile | |
1362 ) | |
1363 self.bridge.menu_launch( | |
1364 menu_type, | |
1365 path, | |
1366 data, | |
1367 security_limit, | |
1368 profile, | |
1369 callback=action_cb, | |
1370 errback=self.dialog_failure, | |
1371 ) | |
1372 | |
1373 def disconnect(self, profile): | |
1374 log.info("disconnecting") | |
1375 self.call_listeners("disconnect", profile=profile) | |
1376 self.bridge.disconnect(profile) | |
1377 | |
1378 def on_exit(self): | |
1379 """Must be called when the frontend is terminating""" | |
1380 to_unplug = [] | |
1381 for profile, profile_manager in self.profiles.items(): | |
1382 if profile_manager.connected and profile_manager.autodisconnect: | |
1383 # The user wants autodisconnection | |
1384 self.disconnect(profile) | |
1385 to_unplug.append(profile) | |
1386 for profile in to_unplug: | |
1387 self.unplug_profile(profile) |