Mercurial > libervia-backend
comparison sat_frontends/quick_frontend/quick_app.py @ 2562:26edcf3a30eb
core, setup: huge cleaning:
- moved directories from src and frontends/src to sat and sat_frontends, which is the recommanded naming convention
- move twisted directory to root
- removed all hacks from setup.py, and added missing dependencies, it is now clean
- use https URL for website in setup.py
- removed "Environment :: X11 Applications :: GTK", as wix is deprecated and removed
- renamed sat.sh to sat and fixed its installation
- added python_requires to specify Python version needed
- replaced glib2reactor which use deprecated code by gtk3reactor
sat can now be installed directly from virtualenv without using --system-site-packages anymore \o/
author | Goffi <goffi@goffi.org> |
---|---|
date | Mon, 02 Apr 2018 19:44:50 +0200 |
parents | frontends/src/quick_frontend/quick_app.py@0046283a285d |
children | 2e6864b1d577 |
comparison
equal
deleted
inserted
replaced
2561:bd30dc3ffe5a | 2562:26edcf3a30eb |
---|---|
1 #!/usr/bin/env python2 | |
2 # -*- coding: utf-8 -*- | |
3 | |
4 # helper class for making a SAT frontend | |
5 # Copyright (C) 2009-2018 Jérôme Poisson (goffi@goffi.org) | |
6 | |
7 # This program is free software: you can redistribute it and/or modify | |
8 # it under the terms of the GNU Affero General Public License as published by | |
9 # the Free Software Foundation, either version 3 of the License, or | |
10 # (at your option) any later version. | |
11 | |
12 # This program is distributed in the hope that it will be useful, | |
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of | |
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
15 # GNU Affero General Public License for more details. | |
16 | |
17 # You should have received a copy of the GNU Affero General Public License | |
18 # along with this program. If not, see <http://www.gnu.org/licenses/>. | |
19 | |
20 from sat.core.log import getLogger | |
21 log = getLogger(__name__) | |
22 | |
23 from sat.core.i18n import _ | |
24 from sat.core import exceptions | |
25 from sat.tools import trigger | |
26 from sat.tools.common import data_format | |
27 | |
28 from sat_frontends.tools import jid | |
29 from sat_frontends.quick_frontend import quick_widgets | |
30 from sat_frontends.quick_frontend import quick_menus | |
31 from sat_frontends.quick_frontend import quick_blog | |
32 from sat_frontends.quick_frontend import quick_chat, quick_games | |
33 from sat_frontends.quick_frontend import quick_contact_list | |
34 from sat_frontends.quick_frontend.constants import Const as C | |
35 | |
36 import sys | |
37 from collections import OrderedDict | |
38 import time | |
39 | |
40 try: | |
41 # FIXME: to be removed when an acceptable solution is here | |
42 unicode('') # XXX: unicode doesn't exist in pyjamas | |
43 except (TypeError, AttributeError): # Error raised is not the same depending on pyjsbuild options | |
44 unicode = str | |
45 | |
46 | |
47 class ProfileManager(object): | |
48 """Class managing all data relative to one profile, and plugging in mechanism""" | |
49 # TODO: handle waiting XMLUI requests: getWaitingConf doesn't exist anymore | |
50 # and a way to keep some XMLUI request between sessions is expected in backend | |
51 host = None | |
52 bridge = None | |
53 # cache_keys_to_get = ['avatar'] | |
54 | |
55 def __init__(self, profile): | |
56 self.profile = profile | |
57 self.connected = False | |
58 self.whoami = None | |
59 self.notifications = {} # key: bare jid or '' for general, value: notif data | |
60 | |
61 @property | |
62 def autodisconnect(self): | |
63 try: | |
64 autodisconnect = self._autodisconnect | |
65 except AttributeError: | |
66 autodisconnect = False | |
67 return autodisconnect | |
68 | |
69 def plug(self): | |
70 """Plug the profile to the host""" | |
71 # we get the essential params | |
72 self.bridge.asyncGetParamA("JabberID", "Connection", profile_key=self.profile, | |
73 callback=self._plug_profile_jid, errback=self._getParamError) | |
74 | |
75 def _plug_profile_jid(self, jid_s): | |
76 self.whoami = jid.JID(jid_s) # resource might change after the connection | |
77 self.bridge.isConnected(self.profile, callback=self._plug_profile_isconnected) | |
78 | |
79 def _autodisconnectEb(self, failure_): | |
80 # XXX: we ignore error on this parameter, as Libervia can't access it | |
81 log.warning(_("Error while trying to get autodisconnect param, ignoring: {}").format(failure_)) | |
82 self._plug_profile_autodisconnect("false") | |
83 | |
84 def _plug_profile_isconnected(self, connected): | |
85 self.connected = connected | |
86 self.bridge.asyncGetParamA("autodisconnect", "Connection", profile_key=self.profile, | |
87 callback=self._plug_profile_autodisconnect, errback=self._autodisconnectEb) | |
88 | |
89 def _plug_profile_autodisconnect(self, autodisconnect): | |
90 if C.bool(autodisconnect): | |
91 self._autodisconnect = True | |
92 self.bridge.asyncGetParamA("autoconnect", "Connection", profile_key=self.profile, | |
93 callback=self._plug_profile_autoconnect, errback=self._getParamError) | |
94 | |
95 def _plug_profile_autoconnect(self, value_str): | |
96 autoconnect = C.bool(value_str) | |
97 if autoconnect and not self.connected: | |
98 self.host.connect(self.profile, callback=lambda dummy: self._plug_profile_afterconnect()) | |
99 else: | |
100 self._plug_profile_afterconnect() | |
101 | |
102 def _plug_profile_afterconnect(self): | |
103 # Profile can be connected or not | |
104 # we get cached data | |
105 self.connected = True | |
106 self.host.bridge.getFeatures(profile_key=self.profile, callback=self._plug_profile_getFeaturesCb, errback=self._plug_profile_getFeaturesEb) | |
107 | |
108 def _plug_profile_getFeaturesEb(self, failure): | |
109 log.error(u"Couldn't get features: {}".format(failure)) | |
110 self._plug_profile_getFeaturesCb({}) | |
111 | |
112 def _plug_profile_getFeaturesCb(self, features): | |
113 self.host.features = features | |
114 # FIXME: we don't use cached value at the moment, but keep the code for later use | |
115 # it was previously used for avatars, but as we don't get full path here, it's better to request later | |
116 # self.host.bridge.getEntitiesData([], ProfileManager.cache_keys_to_get, profile=self.profile, callback=self._plug_profile_gotCachedValues, errback=self._plug_profile_failedCachedValues) | |
117 self._plug_profile_gotCachedValues({}) | |
118 | |
119 def _plug_profile_failedCachedValues(self, failure): | |
120 log.error(u"Couldn't get cached values: {}".format(failure)) | |
121 self._plug_profile_gotCachedValues({}) | |
122 | |
123 def _plug_profile_gotCachedValues(self, cached_values): | |
124 # add the contact list and its listener | |
125 contact_list = self.host.contact_lists.addProfile(self.profile) | |
126 | |
127 for entity_s, data in cached_values.iteritems(): | |
128 for key, value in data.iteritems(): | |
129 self.host.entityDataUpdatedHandler(entity_s, key, value, self.profile) | |
130 | |
131 if not self.connected: | |
132 self.host.setPresenceStatus(C.PRESENCE_UNAVAILABLE, '', profile=self.profile) | |
133 else: | |
134 | |
135 contact_list.fill() | |
136 self.host.setPresenceStatus(profile=self.profile) | |
137 | |
138 #The waiting subscription requests | |
139 self.bridge.getWaitingSub(self.profile, callback=self._plug_profile_gotWaitingSub) | |
140 | |
141 def _plug_profile_gotWaitingSub(self, waiting_sub): | |
142 for sub in waiting_sub: | |
143 self.host.subscribeHandler(waiting_sub[sub], sub, self.profile) | |
144 | |
145 self.bridge.mucGetRoomsJoined(self.profile, callback=self._plug_profile_gotRoomsJoined) | |
146 | |
147 def _plug_profile_gotRoomsJoined(self, rooms_args): | |
148 #Now we open the MUC window where we already are: | |
149 for room_args in rooms_args: | |
150 self.host.mucRoomJoinedHandler(*room_args, profile=self.profile) | |
151 #Presence must be requested after rooms are filled | |
152 self.host.bridge.getPresenceStatuses(self.profile, callback=self._plug_profile_gotPresences) | |
153 | |
154 def _plug_profile_gotPresences(self, presences): | |
155 def gotEntityData(data, contact): | |
156 for key in ('avatar', 'nick'): | |
157 if key in data: | |
158 self.host.entityDataUpdatedHandler(contact, key, data[key], self.profile) | |
159 | |
160 for contact in presences: | |
161 for res in presences[contact]: | |
162 jabber_id = (u'%s/%s' % (jid.JID(contact).bare, res)) if res else contact | |
163 show = presences[contact][res][0] | |
164 priority = presences[contact][res][1] | |
165 statuses = presences[contact][res][2] | |
166 self.host.presenceUpdateHandler(jabber_id, show, priority, statuses, self.profile) | |
167 self.host.bridge.getEntityData(contact, ['avatar', 'nick'], self.profile, callback=lambda data, contact=contact: gotEntityData(data, contact), errback=lambda failure, contact=contact: log.debug(u"No cache data for {}".format(contact))) | |
168 | |
169 # At this point, profile should be fully plugged | |
170 # and we launch frontend specific method | |
171 self.host.profilePlugged(self.profile) | |
172 | |
173 def _getParamError(self, failure): | |
174 log.error(_("Can't get profile parameter: {msg}").format(msg=failure)) | |
175 | |
176 | |
177 class ProfilesManager(object): | |
178 """Class managing collection of profiles""" | |
179 | |
180 def __init__(self): | |
181 self._profiles = {} | |
182 | |
183 def __contains__(self, profile): | |
184 return profile in self._profiles | |
185 | |
186 def __iter__(self): | |
187 return self._profiles.iterkeys() | |
188 | |
189 def __getitem__(self, profile): | |
190 return self._profiles[profile] | |
191 | |
192 def __len__(self): | |
193 return len(self._profiles) | |
194 | |
195 def iteritems(self): | |
196 return self._profiles.iteritems() | |
197 | |
198 def plug(self, profile): | |
199 if profile in self._profiles: | |
200 raise exceptions.ConflictError('A profile of the name [{}] is already plugged'.format(profile)) | |
201 self._profiles[profile] = ProfileManager(profile) | |
202 self._profiles[profile].plug() | |
203 | |
204 def unplug(self, profile): | |
205 if profile not in self._profiles: | |
206 raise ValueError('The profile [{}] is not plugged'.format(profile)) | |
207 | |
208 # remove the contact list and its listener | |
209 host = self._profiles[profile].host | |
210 host.contact_lists[profile].unplug() | |
211 | |
212 del self._profiles[profile] | |
213 | |
214 def chooseOneProfile(self): | |
215 return self._profiles.keys()[0] | |
216 | |
217 | |
218 class QuickApp(object): | |
219 """This class contain the main methods needed for the frontend""" | |
220 MB_HANDLER = True # Set to False if the frontend doesn't manage microblog | |
221 AVATARS_HANDLER = True # set to False if avatars are not used | |
222 | |
223 def __init__(self, bridge_factory, xmlui, check_options=None, connect_bridge=True): | |
224 """Create a frontend application | |
225 | |
226 @param bridge_factory: method to use to create the Bridge | |
227 @param xmlui: xmlui module | |
228 @param check_options: method to call to check options (usually command line arguments) | |
229 """ | |
230 self.xmlui = xmlui | |
231 self.menus = quick_menus.QuickMenusManager(self) | |
232 ProfileManager.host = self | |
233 self.profiles = ProfilesManager() | |
234 self._plugs_in_progress = set() # profiles currently being plugged, used to (un)lock contact list updates | |
235 self.ready_profiles = set() # profiles which are connected and ready | |
236 self.signals_cache = {} # used to keep signal received between start of plug_profile and when the profile is actualy ready | |
237 self.contact_lists = quick_contact_list.QuickContactListHandler(self) | |
238 self.widgets = quick_widgets.QuickWidgetsManager(self) | |
239 if check_options is not None: | |
240 self.options = check_options() | |
241 else: | |
242 self.options = None | |
243 | |
244 # widgets | |
245 self.selected_widget = None # widget currently selected (must be filled by frontend) | |
246 | |
247 # listeners | |
248 self._listeners = {} # key: listener type ("avatar", "selected", etc), value: list of callbacks | |
249 | |
250 # triggers | |
251 self.trigger = trigger.TriggerManager() # trigger are used to change the default behaviour | |
252 | |
253 ## bridge ## | |
254 self.bridge = bridge_factory() | |
255 ProfileManager.bridge = self.bridge | |
256 if connect_bridge: | |
257 self.connectBridge() | |
258 | |
259 self._notif_id = 0 | |
260 self._notifications = OrderedDict() | |
261 self.features = None | |
262 | |
263 def connectBridge(self): | |
264 self.bridge.bridgeConnect(callback=self._bridgeCb, errback=self._bridgeEb) | |
265 | |
266 def onBridgeConnected(self): | |
267 pass | |
268 | |
269 def _bridgeCb(self): | |
270 self.registerSignal("connected") | |
271 self.registerSignal("disconnected") | |
272 self.registerSignal("actionNew") | |
273 self.registerSignal("newContact") | |
274 self.registerSignal("messageNew") | |
275 self.registerSignal("presenceUpdate") | |
276 self.registerSignal("subscribe") | |
277 self.registerSignal("paramUpdate") | |
278 self.registerSignal("contactDeleted") | |
279 self.registerSignal("entityDataUpdated") | |
280 self.registerSignal("progressStarted") | |
281 self.registerSignal("progressFinished") | |
282 self.registerSignal("progressError") | |
283 self.registerSignal("mucRoomJoined", iface="plugin") | |
284 self.registerSignal("mucRoomLeft", iface="plugin") | |
285 self.registerSignal("mucRoomUserChangedNick", iface="plugin") | |
286 self.registerSignal("mucRoomNewSubject", iface="plugin") | |
287 self.registerSignal("chatStateReceived", iface="plugin") | |
288 self.registerSignal("messageState", iface="plugin") | |
289 self.registerSignal("psEvent", iface="plugin") | |
290 | |
291 # FIXME: do it dynamically | |
292 quick_games.Tarot.registerSignals(self) | |
293 quick_games.Quiz.registerSignals(self) | |
294 quick_games.Radiocol.registerSignals(self) | |
295 self.onBridgeConnected() | |
296 | |
297 def _bridgeEb(self, failure): | |
298 if isinstance(failure, exceptions.BridgeExceptionNoService): | |
299 print(_(u"Can't connect to SàT backend, are you sure it's launched ?")) | |
300 sys.exit(1) | |
301 elif isinstance(failure, exceptions.BridgeInitError): | |
302 print(_(u"Can't init bridge")) | |
303 sys.exit(1) | |
304 else: | |
305 print(_(u"Error while initialising bridge: {}".format(failure))) | |
306 | |
307 @property | |
308 def current_profile(self): | |
309 """Profile that a user would expect to use""" | |
310 try: | |
311 return self.selected_widget.profile | |
312 except (TypeError, AttributeError): | |
313 return self.profiles.chooseOneProfile() | |
314 | |
315 @property | |
316 def visible_widgets(self): | |
317 """widgets currently visible (must be implemented by frontend) | |
318 | |
319 @return (iter[QuickWidget]): iterable on visible widgets | |
320 """ | |
321 raise NotImplementedError | |
322 | |
323 def registerSignal(self, function_name, handler=None, iface="core", with_profile=True): | |
324 """Register a handler for a signal | |
325 | |
326 @param function_name (str): name of the signal to handle | |
327 @param handler (instancemethod): method to call when the signal arrive, None for calling an automatically named handler (function_name + 'Handler') | |
328 @param iface (str): interface of the bridge to use ('core' or 'plugin') | |
329 @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 | |
330 """ | |
331 log.debug(u"registering signal {name}".format(name = function_name)) | |
332 if handler is None: | |
333 handler = getattr(self, "{}{}".format(function_name, 'Handler')) | |
334 if not with_profile: | |
335 self.bridge.register_signal(function_name, handler, iface) | |
336 return | |
337 | |
338 def signalReceived(*args, **kwargs): | |
339 profile = kwargs.get('profile') | |
340 if profile is None: | |
341 if not args: | |
342 raise exceptions.ProfileNotSetError | |
343 profile = args[-1] | |
344 if profile is not None: | |
345 if not self.check_profile(profile): | |
346 if profile in self.profiles: | |
347 # profile is not ready but is in self.profiles, that's mean that it's being connecting and we need to cache the signal | |
348 self.signals_cache.setdefault(profile, []).append((function_name, handler, args, kwargs)) | |
349 return # we ignore signal for profiles we don't manage | |
350 handler(*args, **kwargs) | |
351 self.bridge.register_signal(function_name, signalReceived, iface) | |
352 | |
353 def addListener(self, type_, callback, profiles_filter=None): | |
354 """Add a listener for an event | |
355 | |
356 /!\ don't forget to remove listener when not used anymore (e.g. if you delete a widget) | |
357 @param type_: type of event, can be: | |
358 - avatar: called when avatar data is updated | |
359 args: (entity, avatar file, profile) | |
360 - nick: called when nick data is updated | |
361 args: (entity, new_nick, profile) | |
362 - presence: called when a presence is received | |
363 args: (entity, show, priority, statuses, profile) | |
364 - notification: called when a new notification is emited | |
365 args: (entity, notification_data, profile) | |
366 - notification_clear: called when notifications are cleared | |
367 args: (entity, type_, profile) | |
368 - menu: called when a menu item is added or removed | |
369 args: (type_, path, path_i18n, item) were values are: | |
370 type_: same as in [sat.core.sat_main.SAT.importMenu] | |
371 path: same as in [sat.core.sat_main.SAT.importMenu] | |
372 path_i18n: translated path (or None if the item is removed) | |
373 item: instance of quick_menus.MenuItemBase or None if the item is removed | |
374 - gotMenus: called only once when menu are available (no arg) | |
375 - progressFinished: called when a progressing action has just finished | |
376 args: (progress_id, metadata, profile) | |
377 - progressError: called when a progressing action failed | |
378 args: (progress_id, error_msg, profile): | |
379 @param callback: method to call on event | |
380 @param profiles_filter (set[unicode]): if set and not empty, the | |
381 listener will be callable only by one of the given profiles. | |
382 """ | |
383 assert type_ in C.LISTENERS | |
384 self._listeners.setdefault(type_, OrderedDict())[callback] = profiles_filter | |
385 | |
386 def removeListener(self, type_, callback): | |
387 """Remove a callback from listeners | |
388 | |
389 @param type_: same as for [addListener] | |
390 @param callback: callback to remove | |
391 """ | |
392 assert type_ in C.LISTENERS | |
393 self._listeners[type_].pop(callback) | |
394 | |
395 def callListeners(self, type_, *args, **kwargs): | |
396 """Call the methods which listen type_ event. If a profiles filter has | |
397 been register with a listener and profile argument is not None, the | |
398 listener will be called only if profile is in the profiles filter list. | |
399 | |
400 @param type_: same as for [addListener] | |
401 @param *args: arguments sent to callback | |
402 @param **kwargs: keywords argument, mainly used to pass "profile" when needed | |
403 """ | |
404 assert type_ in C.LISTENERS | |
405 try: | |
406 listeners = self._listeners[type_] | |
407 except KeyError: | |
408 pass | |
409 else: | |
410 profile = kwargs.get("profile") | |
411 for listener, profiles_filter in listeners.iteritems(): | |
412 if profile is None or not profiles_filter or profile in profiles_filter: | |
413 listener(*args, **kwargs) | |
414 | |
415 def check_profile(self, profile): | |
416 """Tell if the profile is currently followed by the application, and ready""" | |
417 return profile in self.ready_profiles | |
418 | |
419 def postInit(self, profile_manager): | |
420 """Must be called after initialization is done, do all automatic task (auto plug profile) | |
421 | |
422 @param profile_manager: instance of a subclass of Quick_frontend.QuickProfileManager | |
423 """ | |
424 if self.options and self.options.profile: | |
425 profile_manager.autoconnect([self.options.profile]) | |
426 | |
427 def profilePlugged(self, profile): | |
428 """Method called when the profile is fully plugged, to launch frontend specific workflow | |
429 | |
430 /!\ if you override the method and don't call the parent, be sure to add the profile to ready_profiles ! | |
431 if you don't, all signals will stay in cache | |
432 | |
433 @param profile(unicode): %(doc_profile)s | |
434 """ | |
435 self._plugs_in_progress.remove(profile) | |
436 self.ready_profiles.add(profile) | |
437 | |
438 # profile is ready, we can call send signals that where is cache | |
439 cached_signals = self.signals_cache.pop(profile, []) | |
440 for function_name, handler, args, kwargs in cached_signals: | |
441 log.debug(u"Calling cached signal [%s] with args %s and kwargs %s" % (function_name, args, kwargs)) | |
442 handler(*args, **kwargs) | |
443 | |
444 self.callListeners('profilePlugged', profile=profile) | |
445 if not self._plugs_in_progress: | |
446 self.contact_lists.lockUpdate(False) | |
447 | |
448 def connect(self, profile, callback=None, errback=None): | |
449 if not callback: | |
450 callback = lambda dummy: None | |
451 if not errback: | |
452 def errback(failure): | |
453 log.error(_(u"Can't connect profile [%s]") % failure) | |
454 try: | |
455 module = failure.module | |
456 except AttributeError: | |
457 module = '' | |
458 try: | |
459 message = failure.message | |
460 except AttributeError: | |
461 message = 'error' | |
462 try: | |
463 fullname = failure.fullname | |
464 except AttributeError: | |
465 fullname = 'error' | |
466 if module.startswith('twisted.words.protocols.jabber') and failure.condition == "not-authorized": | |
467 self.launchAction(C.CHANGE_XMPP_PASSWD_ID, {}, profile=profile) | |
468 else: | |
469 self.showDialog(message, fullname, 'error') | |
470 self.bridge.connect(profile, callback=callback, errback=errback) | |
471 | |
472 def plug_profiles(self, profiles): | |
473 """Tell application which profiles must be used | |
474 | |
475 @param profiles: list of valid profile names | |
476 """ | |
477 self.contact_lists.lockUpdate() | |
478 self._plugs_in_progress.update(profiles) | |
479 self.plugging_profiles() | |
480 for profile in profiles: | |
481 self.profiles.plug(profile) | |
482 | |
483 def plugging_profiles(self): | |
484 """Method to subclass to manage frontend specific things to do | |
485 | |
486 will be called when profiles are choosen and are to be plugged soon | |
487 """ | |
488 pass | |
489 | |
490 def unplug_profile(self, profile): | |
491 """Tell the application to not follow anymore the profile""" | |
492 if not profile in self.profiles: | |
493 raise ValueError("The profile [{}] is not plugged".format(profile)) | |
494 self.profiles.unplug(profile) | |
495 | |
496 def clear_profile(self): | |
497 self.profiles.clear() | |
498 | |
499 def newWidget(self, widget): | |
500 raise NotImplementedError | |
501 | |
502 # bridge signals hanlers | |
503 | |
504 def connectedHandler(self, profile, jid_s): | |
505 """Called when the connection is made. | |
506 | |
507 @param jid_s (unicode): the JID that we were assigned by the server, | |
508 as the resource might differ from the JID we asked for. | |
509 """ | |
510 log.debug(_("Connected")) | |
511 self.profiles[profile].whoami = jid.JID(jid_s) | |
512 self.setPresenceStatus(profile=profile) | |
513 self.contact_lists[profile].fill() | |
514 | |
515 def disconnectedHandler(self, profile): | |
516 """called when the connection is closed""" | |
517 log.debug(_("Disconnected")) | |
518 self.contact_lists[profile].disconnect() | |
519 self.setPresenceStatus(C.PRESENCE_UNAVAILABLE, '', profile=profile) | |
520 | |
521 def actionNewHandler(self, action_data, id_, security_limit, profile): | |
522 self.actionManager(action_data, user_action=False, profile=profile) | |
523 | |
524 def newContactHandler(self, jid_s, attributes, groups, profile): | |
525 entity = jid.JID(jid_s) | |
526 groups = list(groups) | |
527 self.contact_lists[profile].setContact(entity, groups, attributes, in_roster=True) | |
528 | |
529 def messageNewHandler(self, uid, timestamp, from_jid_s, to_jid_s, msg, subject, type_, extra, profile): | |
530 from_jid = jid.JID(from_jid_s) | |
531 to_jid = jid.JID(to_jid_s) | |
532 if not self.trigger.point("messageNewTrigger", uid, timestamp, from_jid, to_jid, msg, subject, type_, extra, profile=profile): | |
533 return | |
534 | |
535 from_me = from_jid.bare == self.profiles[profile].whoami.bare | |
536 target = to_jid if from_me else from_jid | |
537 contact_list = self.contact_lists[profile] | |
538 # we want to be sure to have at least one QuickChat instance | |
539 self.widgets.getOrCreateWidget(quick_chat.QuickChat, target, type_=C.CHAT_ONE2ONE, on_new_widget=None, profile=profile) | |
540 | |
541 if not from_jid in contact_list and from_jid.bare != self.profiles[profile].whoami.bare: | |
542 #XXX: needed to show entities which haven't sent any | |
543 # presence information and which are not in roster | |
544 contact_list.setContact(from_jid) | |
545 | |
546 # we dispatch the message in the widgets | |
547 for widget in self.widgets.getWidgets(quick_chat.QuickChat, target=target, profiles=(profile,)): | |
548 widget.messageNew(uid, timestamp, from_jid, target, msg, subject, type_, extra, profile) | |
549 | |
550 def messageStateHandler(self, uid, status, profile): | |
551 for widget in self.widgets.getWidgets(quick_chat.QuickChat, profiles=(profile,)): | |
552 widget.onMessageState(uid, status, profile) | |
553 | |
554 def messageSend(self, to_jid, message, subject=None, mess_type="auto", extra=None, callback=None, errback=None, profile_key=C.PROF_KEY_NONE): | |
555 if subject is None: | |
556 subject = {} | |
557 if extra is None: | |
558 extra = {} | |
559 if callback is None: | |
560 callback = lambda dummy=None: None # FIXME: optional argument is here because pyjamas doesn't support callback without arg with json proxy | |
561 if errback is None: | |
562 errback = lambda failure: self.showDialog(failure.fullname, failure.message, "error") | |
563 | |
564 if not self.trigger.point("messageSendTrigger", to_jid, message, subject, mess_type, extra, callback, errback, profile_key=profile_key): | |
565 return | |
566 | |
567 self.bridge.messageSend(unicode(to_jid), message, subject, mess_type, extra, profile_key, callback=callback, errback=errback) | |
568 | |
569 def setPresenceStatus(self, show='', status=None, profile=C.PROF_KEY_NONE): | |
570 raise NotImplementedError | |
571 | |
572 def presenceUpdateHandler(self, entity_s, show, priority, statuses, profile): | |
573 | |
574 log.debug(_(u"presence update for %(entity)s (show=%(show)s, priority=%(priority)s, statuses=%(statuses)s) [profile:%(profile)s]") | |
575 % {'entity': entity_s, C.PRESENCE_SHOW: show, C.PRESENCE_PRIORITY: priority, C.PRESENCE_STATUSES: statuses, 'profile': profile}) | |
576 entity = jid.JID(entity_s) | |
577 | |
578 if entity == self.profiles[profile].whoami: | |
579 if show == C.PRESENCE_UNAVAILABLE: | |
580 self.setPresenceStatus(C.PRESENCE_UNAVAILABLE, '', profile=profile) | |
581 else: | |
582 # FIXME: try to retrieve user language status before fallback to default | |
583 status = statuses.get(C.PRESENCE_STATUSES_DEFAULT, None) | |
584 self.setPresenceStatus(show, status, profile=profile) | |
585 return | |
586 | |
587 self.callListeners('presence', entity, show, priority, statuses, profile=profile) | |
588 | |
589 def mucRoomJoinedHandler(self, room_jid_s, occupants, user_nick, subject, profile): | |
590 """Called when a MUC room is joined""" | |
591 log.debug(u"Room [{room_jid}] joined by {profile}, users presents:{users}".format(room_jid=room_jid_s, profile=profile, users=occupants.keys())) | |
592 room_jid = jid.JID(room_jid_s) | |
593 self.widgets.getOrCreateWidget(quick_chat.QuickChat, room_jid, type_=C.CHAT_GROUP, nick=user_nick, occupants=occupants, subject=subject, profile=profile) | |
594 self.contact_lists[profile].setSpecial(room_jid, C.CONTACT_SPECIAL_GROUP) | |
595 # chat_widget.update() | |
596 | |
597 def mucRoomLeftHandler(self, room_jid_s, profile): | |
598 """Called when a MUC room is left""" | |
599 log.debug(u"Room [%(room_jid)s] left by %(profile)s" % {'room_jid': room_jid_s, 'profile': profile}) | |
600 room_jid = jid.JID(room_jid_s) | |
601 chat_widget = self.widgets.getWidget(quick_chat.QuickChat, room_jid, profile) | |
602 if chat_widget: | |
603 self.widgets.deleteWidget(chat_widget) | |
604 self.contact_lists[profile].removeContact(room_jid) | |
605 | |
606 def mucRoomUserChangedNickHandler(self, room_jid_s, old_nick, new_nick, profile): | |
607 """Called when an user joined a MUC room""" | |
608 room_jid = jid.JID(room_jid_s) | |
609 chat_widget = self.widgets.getOrCreateWidget(quick_chat.QuickChat, room_jid, type_=C.CHAT_GROUP, profile=profile) | |
610 chat_widget.changeUserNick(old_nick, new_nick) | |
611 log.debug(u"user [%(old_nick)s] is now known as [%(new_nick)s] in room [%(room_jid)s]" % {'old_nick': old_nick, 'new_nick': new_nick, 'room_jid': room_jid}) | |
612 | |
613 def mucRoomNewSubjectHandler(self, room_jid_s, subject, profile): | |
614 """Called when subject of MUC room change""" | |
615 room_jid = jid.JID(room_jid_s) | |
616 chat_widget = self.widgets.getOrCreateWidget(quick_chat.QuickChat, room_jid, type_=C.CHAT_GROUP, profile=profile) | |
617 chat_widget.setSubject(subject) | |
618 log.debug(u"new subject for room [%(room_jid)s]: %(subject)s" % {'room_jid': room_jid, "subject": subject}) | |
619 | |
620 def chatStateReceivedHandler(self, from_jid_s, state, profile): | |
621 """Called when a new chat state (XEP-0085) is received. | |
622 | |
623 @param from_jid_s (unicode): JID of a contact or C.ENTITY_ALL | |
624 @param state (unicode): new state | |
625 @param profile (unicode): current profile | |
626 """ | |
627 from_jid = jid.JID(from_jid_s) | |
628 for widget in self.widgets.getWidgets(quick_chat.QuickChat, profiles=(profile,)): | |
629 widget.onChatState(from_jid, state, profile) | |
630 | |
631 def notify(self, type_, entity=None, message=None, subject=None, callback=None, cb_args=None, widget=None, profile=C.PROF_KEY_NONE): | |
632 """Trigger an event notification | |
633 | |
634 @param type_(unicode): notifation kind, | |
635 one of C.NOTIFY_* constant or any custom type specific to frontend | |
636 @param entity(jid.JID, None): entity involved in the notification | |
637 if entity is in contact list, a indicator may be added in front of it | |
638 @param message(unicode, None): message of the notification | |
639 @param subject(unicode, None): subject of the notification | |
640 @param callback(callable, None): method to call when notification is selected | |
641 @param cb_args(list, None): list of args for callback | |
642 @param widget(object, None): widget where the notification happened | |
643 """ | |
644 assert type_ in C.NOTIFY_ALL | |
645 notif_dict = self.profiles[profile].notifications | |
646 key = '' if entity is None else entity.bare | |
647 type_notifs = notif_dict.setdefault(key, {}).setdefault(type_, []) | |
648 notif_data = { | |
649 'id': self._notif_id, | |
650 'time': time.time(), | |
651 'entity': entity, | |
652 'callback': callback, | |
653 'cb_args': cb_args, | |
654 'message': message, | |
655 'subject': subject, | |
656 } | |
657 if widget is not None: | |
658 notif_data[widget] = widget | |
659 type_notifs.append(notif_data) | |
660 self._notifications[self._notif_id] = notif_data | |
661 self.callListeners('notification', entity, notif_data, profile=profile) | |
662 | |
663 def getNotifs(self, entity=None, type_=None, exact_jid=None, profile=C.PROF_KEY_NONE): | |
664 """return notifications for given entity | |
665 | |
666 @param entity(jid.JID, None, C.ENTITY_ALL): jid of the entity to check | |
667 bare jid to get all notifications, full jid to filter on resource | |
668 None to get general notifications | |
669 C.ENTITY_ALL to get all notifications | |
670 @param type_(unicode, None): notification type to filter | |
671 None to get all notifications | |
672 @param exact_jid(bool, None): if True, only return notifications from | |
673 exact entity jid (i.e. not including other resources) | |
674 None for automatic selection (True for full jid, False else) | |
675 False to get resources notifications | |
676 False doesn't do anything if entity is not a bare jid | |
677 @return (iter[dict]): notifications | |
678 """ | |
679 main_notif_dict = self.profiles[profile].notifications | |
680 | |
681 if entity is C.ENTITY_ALL: | |
682 selected_notifs = main_notif_dict.itervalues() | |
683 exact_jid = False | |
684 else: | |
685 if entity is None: | |
686 key = '' | |
687 exact_jid = False | |
688 else: | |
689 key = entity.bare | |
690 if exact_jid is None: | |
691 exact_jid = bool(entity.resource) | |
692 selected_notifs = (main_notif_dict.setdefault(key, {}),) | |
693 | |
694 for notifs_from_select in selected_notifs: | |
695 | |
696 if type_ is None: | |
697 type_notifs = notifs_from_select.itervalues() | |
698 else: | |
699 type_notifs = (notifs_from_select.get(type_, []),) | |
700 | |
701 for notifs in type_notifs: | |
702 for notif in notifs: | |
703 if exact_jid and notif['entity'] != entity: | |
704 continue | |
705 yield notif | |
706 | |
707 def clearNotifs(self, entity, type_=None, profile=C.PROF_KEY_NONE): | |
708 """return notifications for given entity | |
709 | |
710 @param entity(jid.JID, None): bare jid of the entity to check | |
711 None to clear general notifications (but keep entities ones) | |
712 @param type_(unicode, None): notification type to filter | |
713 None to clear all notifications | |
714 @return (list[dict]): list of notifications | |
715 """ | |
716 notif_dict = self.profiles[profile].notifications | |
717 key = '' if entity is None else entity.bare | |
718 try: | |
719 if type_ is None: | |
720 del notif_dict[key] | |
721 else: | |
722 del notif_dict[key][type_] | |
723 except KeyError: | |
724 return | |
725 self.callListeners('notificationsClear', entity, type_, profile=profile) | |
726 | |
727 def psEventHandler(self, category, service_s, node, event_type, data, profile): | |
728 """Called when a PubSub event is received. | |
729 | |
730 @param category(unicode): event category (e.g. "PEP", "MICROBLOG") | |
731 @param service_s (unicode): pubsub service | |
732 @param node (unicode): pubsub node | |
733 @param event_type (unicode): event type (one of C.PUBLISH, C.RETRACT, C.DELETE) | |
734 @param data (dict): event data | |
735 """ | |
736 service_s = jid.JID(service_s) | |
737 | |
738 if category == C.PS_MICROBLOG and self.MB_HANDLER: | |
739 if event_type == C.PS_PUBLISH: | |
740 if not 'content' in data: | |
741 log.warning("No content found in microblog data") | |
742 return | |
743 _groups = set(data_format.dict2iter('group', data)) or None # FIXME: check if [] make sense (instead of None) | |
744 | |
745 for wid in self.widgets.getWidgets(quick_blog.QuickBlog): | |
746 wid.addEntryIfAccepted(service_s, node, data, _groups, profile) | |
747 | |
748 try: | |
749 comments_node, comments_service = data['comments_node'], data['comments_service'] | |
750 except KeyError: | |
751 pass | |
752 else: | |
753 self.bridge.mbGet(comments_service, comments_node, C.NO_LIMIT, [], {"subscribe":C.BOOL_TRUE}, profile=profile) | |
754 elif event_type == C.PS_RETRACT: | |
755 for wid in self.widgets.getWidgets(quick_blog.QuickBlog): | |
756 wid.deleteEntryIfPresent(service_s, node, data['id'], profile) | |
757 pass | |
758 else: | |
759 log.warning("Unmanaged PubSub event type {}".format(event_type)) | |
760 | |
761 def progressStartedHandler(self, pid, metadata, profile): | |
762 log.info(u"Progress {} started".format(pid)) | |
763 | |
764 def progressFinishedHandler(self, pid, metadata, profile): | |
765 log.info(u"Progress {} finished".format(pid)) | |
766 self.callListeners('progressFinished', pid, metadata, profile=profile) | |
767 | |
768 def progressErrorHandler(self, pid, err_msg, profile): | |
769 log.warning(u"Progress {pid} error: {err_msg}".format(pid=pid, err_msg=err_msg)) | |
770 self.callListeners('progressError', pid, err_msg, profile=profile) | |
771 | |
772 def _subscribe_cb(self, answer, data): | |
773 entity, profile = data | |
774 type_ = "subscribed" if answer else "unsubscribed" | |
775 self.bridge.subscription(type_, unicode(entity.bare), profile_key=profile) | |
776 | |
777 def subscribeHandler(self, type, raw_jid, profile): | |
778 """Called when a subsciption management signal is received""" | |
779 entity = jid.JID(raw_jid) | |
780 if type == "subscribed": | |
781 # this is a subscription confirmation, we just have to inform user | |
782 # TODO: call self.getEntityMBlog to add the new contact blogs | |
783 self.showDialog(_("The contact %s has accepted your subscription") % entity.bare, _('Subscription confirmation')) | |
784 elif type == "unsubscribed": | |
785 # this is a subscription refusal, we just have to inform user | |
786 self.showDialog(_("The contact %s has refused your subscription") % entity.bare, _('Subscription refusal'), 'error') | |
787 elif type == "subscribe": | |
788 # this is a subscriptionn request, we have to ask for user confirmation | |
789 # TODO: use sat.stdui.ui_contact_list to display the groups selector | |
790 self.showDialog(_("The contact %s wants to subscribe to your presence.\nDo you accept ?") % entity.bare, _('Subscription confirmation'), 'yes/no', answer_cb=self._subscribe_cb, answer_data=(entity, profile)) | |
791 | |
792 def showDialog(self, message, title, type="info", answer_cb=None, answer_data=None): | |
793 raise NotImplementedError | |
794 | |
795 def showAlert(self, message): | |
796 pass #FIXME | |
797 | |
798 def dialogFailure(self, failure): | |
799 log.warning(u"Failure: {}".format(failure)) | |
800 | |
801 def progressIdHandler(self, progress_id, profile): | |
802 """Callback used when an action result in a progress id""" | |
803 log.info(u"Progress ID received: {}".format(progress_id)) | |
804 | |
805 def isHidden(self): | |
806 """Tells if the frontend window is hidden. | |
807 | |
808 @return bool | |
809 """ | |
810 raise NotImplementedError | |
811 | |
812 def paramUpdateHandler(self, name, value, namespace, profile): | |
813 log.debug(_(u"param update: [%(namespace)s] %(name)s = %(value)s") % {'namespace': namespace, 'name': name, 'value': value}) | |
814 if (namespace, name) == ("Connection", "JabberID"): | |
815 log.debug(_(u"Changing JID to %s") % value) | |
816 self.profiles[profile].whoami = jid.JID(value) | |
817 elif (namespace, name) == ('General', C.SHOW_OFFLINE_CONTACTS): | |
818 self.contact_lists[profile].showOfflineContacts(C.bool(value)) | |
819 elif (namespace, name) == ('General', C.SHOW_EMPTY_GROUPS): | |
820 self.contact_lists[profile].showEmptyGroups(C.bool(value)) | |
821 | |
822 def contactDeletedHandler(self, jid_s, profile): | |
823 target = jid.JID(jid_s) | |
824 self.contact_lists[profile].removeContact(target) | |
825 | |
826 def entityDataUpdatedHandler(self, entity_s, key, value, profile): | |
827 entity = jid.JID(entity_s) | |
828 if key == "nick": # this is the roster nick, not the MUC nick | |
829 if entity in self.contact_lists[profile]: | |
830 self.contact_lists[profile].setCache(entity, 'nick', value) | |
831 self.callListeners('nick', entity, value, profile=profile) | |
832 elif key == "avatar" and self.AVATARS_HANDLER: | |
833 if value and entity in self.contact_lists[profile]: | |
834 self.getAvatar(entity, ignore_cache=True, profile=profile) | |
835 | |
836 def actionManager(self, action_data, callback=None, ui_show_cb=None, user_action=True, profile=C.PROF_KEY_NONE): | |
837 """Handle backend action | |
838 | |
839 @param action_data(dict): action dict as sent by launchAction or returned by an UI action | |
840 @param callback(None, callback): if not None, callback to use on XMLUI answer | |
841 @param ui_show_cb(None, callback): if not None, method to call to show the XMLUI | |
842 @param user_action(bool): if True, the action is a result of a user interaction | |
843 else the action come from backend direclty (i.e. actionNew) | |
844 """ | |
845 try: | |
846 xmlui = action_data.pop('xmlui') | |
847 except KeyError: | |
848 pass | |
849 else: | |
850 ui = self.xmlui.create(self, xml_data=xmlui, flags=("FROM_BACKEND",) if not user_action else None, callback=callback, profile=profile) | |
851 if ui_show_cb is None: | |
852 ui.show() | |
853 else: | |
854 ui_show_cb(ui) | |
855 | |
856 try: | |
857 progress_id = action_data.pop('progress') | |
858 except KeyError: | |
859 pass | |
860 else: | |
861 self.progressIdHandler(progress_id, profile) | |
862 | |
863 # we ignore metadata | |
864 action_data = {k:v for k,v in action_data.iteritems() if not k.startswith("meta_")} | |
865 | |
866 if action_data: | |
867 raise exceptions.DataError(u"Not all keys in action_data are managed ({keys})".format(keys=', '.join(action_data.keys()))) | |
868 | |
869 | |
870 def _actionCb(self, data, callback, callback_id, profile): | |
871 if callback is None: | |
872 self.actionManager(data, profile=profile) | |
873 else: | |
874 callback(data=data, cb_id=callback_id, profile=profile) | |
875 | |
876 def launchAction(self, callback_id, data=None, callback=None, profile=C.PROF_KEY_NONE): | |
877 """Launch a dynamic action | |
878 | |
879 @param callback_id: id of the action to launch | |
880 @param data: data needed only for certain actions | |
881 @param callback(callable, None): will be called with the resut | |
882 if None, self.actionManager will be called | |
883 else the callable will be called with the following kw parameters: | |
884 - data: action_data | |
885 - cb_id: callback id | |
886 - profile: %(doc_profile)s | |
887 @param profile: %(doc_profile)s | |
888 | |
889 """ | |
890 if data is None: | |
891 data = dict() | |
892 action_cb = lambda data: self._actionCb(data, callback, callback_id, profile) | |
893 self.bridge.launchAction(callback_id, data, profile, callback=action_cb, errback=self.dialogFailure) | |
894 | |
895 def launchMenu(self, menu_type, path, data=None, callback=None, security_limit=C.SECURITY_LIMIT_MAX, profile=C.PROF_KEY_NONE): | |
896 """Launch a menu manually | |
897 | |
898 @param menu_type(unicode): type of the menu to launch | |
899 @param path(iterable[unicode]): path to the menu | |
900 @param data: data needed only for certain actions | |
901 @param callback(callable, None): will be called with the resut | |
902 if None, self.actionManager will be called | |
903 else the callable will be called with the following kw parameters: | |
904 - data: action_data | |
905 - cb_id: (menu_type, path) tuple | |
906 - profile: %(doc_profile)s | |
907 @param profile: %(doc_profile)s | |
908 | |
909 """ | |
910 if data is None: | |
911 data = dict() | |
912 action_cb = lambda data: self._actionCb(data, callback, (menu_type, path), profile) | |
913 self.bridge.menuLaunch(menu_type, path, data, security_limit, profile, callback=action_cb, errback=self.dialogFailure) | |
914 | |
915 def _avatarGetCb(self, avatar_path, entity, contact_list, profile): | |
916 path = avatar_path or self.getDefaultAvatar(entity) | |
917 contact_list.setCache(entity, "avatar", path) | |
918 self.callListeners('avatar', entity, path, profile=profile) | |
919 | |
920 def _avatarGetEb(self, failure, entity, contact_list): | |
921 log.warning(u"Can't get avatar: {}".format(failure)) | |
922 contact_list.setCache(entity, "avatar", self.getDefaultAvatar(entity)) | |
923 | |
924 def getAvatar(self, entity, cache_only=True, hash_only=False, ignore_cache=False, profile=C.PROF_KEY_NONE): | |
925 """return avatar path for an entity | |
926 | |
927 @param entity(jid.JID): entity to get avatar from | |
928 @param cache_only(bool): if False avatar will be requested if not in cache | |
929 with current vCard based implementation, it's better to keep True | |
930 except if we request avatars for roster items | |
931 @param hash_only(bool): if True avatar hash is returned, else full path | |
932 @param ignore_cache(bool): if False, won't check local cache and will request backend in every case | |
933 @return (unicode, None): avatar full path (None if no avatar found) | |
934 """ | |
935 contact_list = self.contact_lists[profile] | |
936 if ignore_cache: | |
937 avatar = None | |
938 else: | |
939 avatar = contact_list.getCache(entity, "avatar", bare_default=None) | |
940 if avatar is None: | |
941 self.bridge.avatarGet( | |
942 unicode(entity), | |
943 cache_only, | |
944 hash_only, | |
945 profile=profile, | |
946 callback=lambda path: self._avatarGetCb(path, entity, contact_list, profile), | |
947 errback=lambda failure: self._avatarGetEb(failure, entity, contact_list)) | |
948 # we set avatar to empty string to avoid requesting several time the same avatar | |
949 # while we are waiting for avatarGet result | |
950 contact_list.setCache(entity, "avatar", "") | |
951 return avatar | |
952 | |
953 def getDefaultAvatar(self, entity=None): | |
954 """return default avatar to use with given entity | |
955 | |
956 must be implemented by frontend | |
957 @param entity(jid.JID): entity for which a default avatar is needed | |
958 """ | |
959 raise NotImplementedError | |
960 | |
961 def disconnect(self, profile): | |
962 log.info("disconnecting") | |
963 self.callListeners('disconnect', profile=profile) | |
964 self.bridge.disconnect(profile) | |
965 | |
966 def onExit(self): | |
967 """Must be called when the frontend is terminating""" | |
968 to_unplug = [] | |
969 for profile, profile_manager in self.profiles.iteritems(): | |
970 if profile_manager.connected and profile_manager.autodisconnect: | |
971 #The user wants autodisconnection | |
972 self.disconnect(profile) | |
973 to_unplug.append(profile) | |
974 for profile in to_unplug: | |
975 self.unplug_profile(profile) |