comparison frontends/src/quick_frontend/quick_app.py @ 1265:e3a9ea76de35 frontends_multi_profiles

quick_frontend, primitivus: multi-profiles refactoring part 1 (big commit, sorry :p): This refactoring allow primitivus to manage correctly several profiles at once, with various other improvments: - profile_manager can now plug several profiles at once, requesting password when needed. No more profile plug specific method is used anymore in backend, instead a "validated" key is used in actions - Primitivus widget are now based on a common "PrimitivusWidget" classe which mainly manage the decoration so far - all widgets are treated in the same way (contactList, Chat, Progress, etc), no more chat_wins specific behaviour - widgets are created in a dedicated manager, with facilities to react on new widget creation or other events - quick_frontend introduce a new QuickWidget class, which aims to be as generic and flexible as possible. It can manage several targets (jids or something else), and several profiles - each widget class return a Hash according to its target. For example if given a target jid and a profile, a widget class return a hash like (target.bare, profile), the same widget will be used for all resources of the same jid - better management of CHAT_GROUP mode for Chat widgets - some code moved from Primitivus to QuickFrontend, the final goal is to have most non backend code in QuickFrontend, and just graphic code in subclasses - no more (un)escapePrivate/PRIVATE_PREFIX - contactList improved a lot: entities not in roster and special entities (private MUC conversations) are better managed - resources can be displayed in Primitivus, and their status messages - profiles are managed in QuickFrontend with dedicated managers This is work in progress, other frontends are broken. Urwid SàText need to be updated. Most of features of Primitivus should work as before (or in a better way ;))
author Goffi <goffi@goffi.org>
date Wed, 10 Dec 2014 19:00:09 +0100
parents e56dfe0378a1
children faa1129559b8
comparison
equal deleted inserted replaced
1264:60dfa2f5d61f 1265:e3a9ea76de35
19 19
20 from sat.core.i18n import _ 20 from sat.core.i18n import _
21 import sys 21 import sys
22 from sat.core.log import getLogger 22 from sat.core.log import getLogger
23 log = getLogger(__name__) 23 log = getLogger(__name__)
24 from sat_frontends.tools.jid import JID 24 from sat.core import exceptions
25 from sat_frontends.bridge.DBus import DBusBridgeFrontend 25 from sat_frontends.bridge.DBus import DBusBridgeFrontend
26 from sat.core import exceptions 26 from sat_frontends.tools import jid
27 from sat_frontends.quick_frontend.quick_utils import escapePrivate, unescapePrivate 27 from sat_frontends.quick_frontend.quick_widgets import QuickWidgetsManager
28 from sat_frontends.quick_frontend import quick_chat
28 from optparse import OptionParser 29 from optparse import OptionParser
29 30
30 from sat_frontends.quick_frontend.constants import Const as C 31 from sat_frontends.quick_frontend.constants import Const as C
31 32
33
34 class ProfileManager(object):
35 """Class managing all data relative to one profile, and plugging in mechanism"""
36 host = None
37 bridge = None
38
39 def __init__(self, profile):
40 self.profile = profile
41 self.whoami = None
42 self.data = {}
43
44 def __getitem__(self, key):
45 return self.data[key]
46
47 def __setitem__(self, key, value):
48 self.data[key] = value
49
50 def plug(self):
51 """Plug the profile to the host"""
52 # we get the essential params
53 self.bridge.asyncGetParamA("JabberID", "Connection", profile_key=self.profile,
54 callback=self._plug_profile_jid, errback=self._getParamError)
55
56 def _plug_profile_jid(self, _jid):
57 self.whoami = jid.JID(_jid)
58 self.bridge.asyncGetParamA("autoconnect", "Connection", profile_key=self.profile,
59 callback=self._plug_profile_autoconnect, errback=self._getParamError)
60
61 def _plug_profile_autoconnect(self, value_str):
62 autoconnect = C.bool(value_str)
63 if autoconnect and not self.bridge.isConnected(self.profile):
64 self.host.asyncConnect(self.profile, callback=lambda dummy: self._plug_profile_afterconnect())
65 else:
66 self._plug_profile_afterconnect()
67
68 def _plug_profile_afterconnect(self):
69 # Profile can be connected or not
70 # TODO: watched plugin
71 contact_list = self.host.addContactList(self.profile)
72
73 if not self.bridge.isConnected(self.profile):
74 self.host.setStatusOnline(False, profile=self.profile)
75 else:
76 self.host.setStatusOnline(True, profile=self.profile)
77
78 contact_list.fill()
79
80 #The waiting subscription requests
81 waitingSub = self.bridge.getWaitingSub(self.profile)
82 for sub in waitingSub:
83 self.host.subscribeHandler(waitingSub[sub], sub, self.profile)
84
85 #Now we open the MUC window where we already are:
86 for room_args in self.bridge.getRoomsJoined(self.profile):
87 self.host.roomJoinedHandler(*room_args, profile=self.profile)
88
89 for subject_args in self.bridge.getRoomsSubjects(self.profile):
90 self.host.roomNewSubjectHandler(*subject_args, profile=self.profile)
91
92 #Finaly, we get the waiting confirmation requests
93 for confirm_id, confirm_type, data in self.bridge.getWaitingConf(self.profile):
94 self.host.askConfirmationHandler(confirm_id, confirm_type, data, self.profile)
95
96 def _getParamError(self, ignore):
97 log.error(_("Can't get profile parameter"))
98
99
100 class ProfilesManager(object):
101 """Class managing collection of profiles"""
102
103 def __init__(self):
104 self._profiles = {}
105
106 def __contains__(self, profile):
107 return profile in self._profiles
108
109 def __iter__(self):
110 return self._profiles.iterkeys()
111
112 def __getitem__(self, profile):
113 return self._profiles[profile]
114
115 def __len__(self):
116 return len(self._profiles)
117
118 def plug(self, profile):
119 if profile in self._profiles:
120 raise exceptions.ConflictError('A profile of the name [{}] is already plugged'.format(profile))
121 self._profiles[profile] = ProfileManager(profile)
122 self._profiles[profile].plug()
123
124 def unplug(self, profile):
125 if profile not in self._profiles:
126 raise ValueError('The profile [{}] is not plugged'.format(profile))
127 del self._profiles[profile]
128
129 def chooseOneProfile(self):
130 return self._profiles.keys()[0]
32 131
33 class QuickApp(object): 132 class QuickApp(object):
34 """This class contain the main methods needed for the frontend""" 133 """This class contain the main methods needed for the frontend"""
35 134
36 def __init__(self, single_profile=True): 135 def __init__(self):
37 self.profiles = {} 136 ProfileManager.host = self
38 self.single_profile = single_profile 137 self.profiles = ProfilesManager()
138 self.contact_lists = {}
139 self.widgets = QuickWidgetsManager(self)
39 self.check_options() 140 self.check_options()
141
142 # widgets
143 self.visible_widgets = set() # widgets currently visible (must be filled by frontend)
144 self.selected_widget = None # widget currently selected (must be filled by frontend)
40 145
41 ## bridge ## 146 ## bridge ##
42 try: 147 try:
43 self.bridge = DBusBridgeFrontend() 148 self.bridge = DBusBridgeFrontend()
44 except exceptions.BridgeExceptionNoService: 149 except exceptions.BridgeExceptionNoService:
45 print(_(u"Can't connect to SàT backend, are you sure it's launched ?")) 150 print(_(u"Can't connect to SàT backend, are you sure it's launched ?"))
46 sys.exit(1) 151 sys.exit(1)
47 except exceptions.BridgeInitError: 152 except exceptions.BridgeInitError:
48 print(_(u"Can't init bridge")) 153 print(_(u"Can't init bridge"))
49 sys.exit(1) 154 sys.exit(1)
155 ProfileManager.bridge = self.bridge
50 self.registerSignal("connected") 156 self.registerSignal("connected")
51 self.registerSignal("disconnected") 157 self.registerSignal("disconnected")
52 self.registerSignal("newContact") 158 self.registerSignal("newContact")
53 self.registerSignal("newMessage", self._newMessage) 159 self.registerSignal("newMessage", self._newMessage)
54 self.registerSignal("newAlert") 160 self.registerSignal("newAlert")
82 self.registerSignal("quizGameAnswerResult", iface="plugin") 188 self.registerSignal("quizGameAnswerResult", iface="plugin")
83 self.registerSignal("quizGameTimerExpired", iface="plugin") 189 self.registerSignal("quizGameTimerExpired", iface="plugin")
84 self.registerSignal("quizGameTimerRestarted", iface="plugin") 190 self.registerSignal("quizGameTimerRestarted", iface="plugin")
85 self.registerSignal("chatStateReceived", iface="plugin") 191 self.registerSignal("chatStateReceived", iface="plugin")
86 192
87 self.current_action_ids = set() 193 self.current_action_ids = set() # FIXME: to be removed
88 self.current_action_ids_cb = {} 194 self.current_action_ids_cb = {} # FIXME: to be removed
89 self.media_dir = self.bridge.getConfig('', 'media_dir') 195 self.media_dir = self.bridge.getConfig('', 'media_dir')
196
197 @property
198 def current_profile(self):
199 """Profile that a user would expect to use"""
200 try:
201 return self.selected_widget.profile
202 except (TypeError, AttributeError):
203 return self.profiles.chooseOneProfile()
90 204
91 def registerSignal(self, functionName, handler=None, iface="core", with_profile=True): 205 def registerSignal(self, functionName, handler=None, iface="core", with_profile=True):
92 """Register a handler for a signal 206 """Register a handler for a signal
93 207
94 @param functionName (str): name of the signal to handle 208 @param functionName (str): name of the signal to handle
113 handler(*args, **kwargs) 227 handler(*args, **kwargs)
114 self.bridge.register(functionName, signalReceived, iface) 228 self.bridge.register(functionName, signalReceived, iface)
115 229
116 def check_profile(self, profile): 230 def check_profile(self, profile):
117 """Tell if the profile is currently followed by the application""" 231 """Tell if the profile is currently followed by the application"""
118 return profile in self.profiles.keys() 232 return profile in self.profiles
119 233
120 def postInit(self): 234 def postInit(self, profile_manager):
121 """Must be called after initialization is done, do all automatic task (auto plug profile)""" 235 """Must be called after initialization is done, do all automatic task (auto plug profile)
236
237 @param profile_manager: instance of a subclass of Quick_frontend.QuickProfileManager
238 """
122 if self.options.profile: 239 if self.options.profile:
123 if not self.bridge.getProfileName(self.options.profile): 240 profile_manager.autoconnect([self.options.profile])
124 log.error(_("Trying to plug an unknown profile (%s)" % self.options.profile))
125 else:
126 self.plug_profile(self.options.profile)
127 241
128 def check_options(self): 242 def check_options(self):
129 """Check command line options""" 243 """Check command line options"""
130 usage = _(""" 244 usage = _("""
131 %prog [options] 245 %prog [options]
132 246
133 %prog --help for options list 247 %prog --help for options list
134 """) 248 """)
135 parser = OptionParser(usage=usage) 249 parser = OptionParser(usage=usage) # TODO: use argparse
136 250
137 parser.add_option("-p", "--profile", help=_("Select the profile to use")) 251 parser.add_option("-p", "--profile", help=_("Select the profile to use"))
138 252
139 (self.options, args) = parser.parse_args() 253 (self.options, args) = parser.parse_args()
140 if self.options.profile: 254 if self.options.profile:
141 self.options.profile = self.options.profile.decode('utf-8') 255 self.options.profile = self.options.profile.decode('utf-8')
142 return args 256 return args
143
144 def _getParamError(self, ignore):
145 log.error(_("Can't get profile parameter"))
146
147 def plug_profile(self, profile_key='@DEFAULT@'):
148 """Tell application which profile must be used"""
149 if self.single_profile and self.profiles:
150 log.error(_('There is already one profile plugged (we are in single profile mode) !'))
151 return
152 profile = self.bridge.getProfileName(profile_key)
153 if not profile:
154 log.error(_("The profile asked doesn't exist"))
155 return
156 if profile in self.profiles:
157 log.warning(_("The profile is already plugged"))
158 return
159 self.profiles[profile] = {}
160 if self.single_profile:
161 self.profile = profile # FIXME: must be refactored (multi profiles are not managed correclty)
162 raw_menus = self.bridge.getMenus("", C.NO_SECURITY_LIMIT )
163 menus = self.profiles[profile]['menus'] = {}
164 for raw_menu in raw_menus:
165 id_, type_, path, path_i18n = raw_menu
166 menus_data = menus.setdefault(type_, [])
167 menus_data.append((id_, path, path_i18n))
168 self.launchAction(C.AUTHENTICATE_PROFILE_ID, {'caller': 'plug_profile'}, profile_key=profile)
169
170 def plug_profile_1(self, profile):
171 ###now we get the essential params###
172 self.bridge.asyncGetParamA("JabberID", "Connection", profile_key=profile,
173 callback=lambda _jid: self.plug_profile_2(_jid, profile), errback=self._getParamError)
174
175 def plug_profile_2(self, _jid, profile):
176 self.profiles[profile]['whoami'] = JID(_jid)
177 self.bridge.asyncGetParamA("autoconnect", "Connection", profile_key=profile,
178 callback=lambda value: self.plug_profile_3(value == "true", profile), errback=self._getParamError)
179
180 def plug_profile_3(self, autoconnect, profile):
181 self.bridge.asyncGetParamA("Watched", "Misc", profile_key=profile,
182 callback=lambda watched: self.plug_profile_4(watched, autoconnect, profile), errback=self._getParamError)
183 257
184 def asyncConnect(self, profile, callback=None, errback=None): 258 def asyncConnect(self, profile, callback=None, errback=None):
185 if not callback: 259 if not callback:
186 callback = lambda dummy: None 260 callback = lambda dummy: None
187 if not errback: 261 if not errback:
188 def errback(failure): 262 def errback(failure):
189 log.error(_(u"Can't connect profile [%s]") % failure) 263 log.error(_(u"Can't connect profile [%s]") % failure)
190 if failure.module.startswith('twisted.words.protocols.jabber') and failure.condition == "not-authorized": 264 if failure.module.startswith('twisted.words.protocols.jabber') and failure.condition == "not-authorized":
191 self.launchAction(C.CHANGE_XMPP_PASSWD_ID, {}, profile_key=profile) 265 self.launchAction(C.CHANGE_XMPP_PASSWD_ID, {}, profile=profile)
192 else: 266 else:
193 self.showDialog(failure.message, failure.fullname, 'error') 267 self.showDialog(failure.message, failure.fullname, 'error')
194 self.bridge.asyncConnect(profile, callback=callback, errback=errback) 268 self.bridge.asyncConnect(profile, callback=callback, errback=errback)
195 269
196 def plug_profile_4(self, watched, autoconnect, profile): 270 def plug_profiles(self, profiles):
197 if autoconnect and not self.bridge.isConnected(profile): 271 """Tell application which profiles must be used
198 #Does the user want autoconnection ? 272
199 self.asyncConnect(profile, callback=lambda dummy: self.plug_profile_5(watched, autoconnect, profile)) 273 @param profiles: list of valid profile names
200 else: 274 """
201 self.plug_profile_5(watched, autoconnect, profile) 275 self.plugging_profiles()
202 276 for profile in profiles:
203 def plug_profile_5(self, watched, autoconnect, profile): 277 self.profiles.plug(profile)
204 self.profiles[profile]['watched'] = watched.split() # TODO: put this in a plugin 278
205 279 def plugging_profiles(self):
206 ## misc ## 280 """Method to subclass to manage frontend specific things to do
207 self.profiles[profile]['onlineContact'] = set() # FIXME: temporary 281
208 282 will be called when profiles are choosen and are to be plugged soon
209 #TODO: manage multi-profiles here 283 """
210 if not self.bridge.isConnected(profile): 284 raise NotImplementedError
211 self.setStatusOnline(False)
212 else:
213 self.setStatusOnline(True)
214
215 ### now we fill the contact list ###
216 for contact in self.bridge.getContacts(profile):
217 self.newContactHandler(*contact, profile=profile)
218
219 presences = self.bridge.getPresenceStatuses(profile)
220 for contact in presences:
221 for res in presences[contact]:
222 jabber_id = ('%s/%s' % (JID(contact).bare, res)) if res else contact
223 show = presences[contact][res][0]
224 priority = presences[contact][res][1]
225 statuses = presences[contact][res][2]
226 self.presenceUpdateHandler(jabber_id, show, priority, statuses, profile)
227 data = self.bridge.getEntityData(contact, ['avatar', 'nick'], profile)
228 for key in ('avatar', 'nick'):
229 if key in data:
230 self.entityDataUpdatedHandler(contact, key, data[key], profile)
231
232 #The waiting subscription requests
233 waitingSub = self.bridge.getWaitingSub(profile)
234 for sub in waitingSub:
235 self.subscribeHandler(waitingSub[sub], sub, profile)
236
237 #Now we open the MUC window where we already are:
238 for room_args in self.bridge.getRoomsJoined(profile):
239 self.roomJoinedHandler(*room_args, profile=profile)
240
241 for subject_args in self.bridge.getRoomsSubjects(profile):
242 self.roomNewSubjectHandler(*subject_args, profile=profile)
243
244 #Finaly, we get the waiting confirmation requests
245 for confirm_id, confirm_type, data in self.bridge.getWaitingConf(profile):
246 self.askConfirmationHandler(confirm_id, confirm_type, data, profile)
247 285
248 def unplug_profile(self, profile): 286 def unplug_profile(self, profile):
249 """Tell the application to not follow anymore the profile""" 287 """Tell the application to not follow anymore the profile"""
250 if not profile in self.profiles: 288 if not profile in self.profiles:
251 log.warning(_("This profile is not plugged")) 289 raise ValueError("The profile [{}] is not plugged".format(profile))
252 return
253 self.profiles.remove(profile) 290 self.profiles.remove(profile)
254 291
255 def clear_profile(self): 292 def clear_profile(self):
256 self.profiles.clear() 293 self.profiles.clear()
294
295 def newWidget(self, widget):
296 raise NotImplementedError
257 297
258 def connectedHandler(self, profile): 298 def connectedHandler(self, profile):
259 """called when the connection is made""" 299 """called when the connection is made"""
260 log.debug(_("Connected")) 300 log.debug(_("Connected"))
261 self.setStatusOnline(True) 301 self.setStatusOnline(True, profile=profile)
262 302
263 def disconnectedHandler(self, profile): 303 def disconnectedHandler(self, profile):
264 """called when the connection is closed""" 304 """called when the connection is closed"""
265 log.debug(_("Disconnected")) 305 log.debug(_("Disconnected"))
266 self.contact_list.clearContacts() 306 self.contact_lists[profile].clearContacts()
267 self.setStatusOnline(False) 307 self.setStatusOnline(False, profile=profile)
268 308
269 def newContactHandler(self, JabberId, attributes, groups, profile): 309 def newContactHandler(self, JabberId, attributes, groups, profile):
270 entity = JID(JabberId) 310 entity = jid.JID(JabberId)
271 _groups = list(groups) 311 _groups = list(groups)
272 self.contact_list.replace(entity, _groups, attributes) 312 self.contact_lists[profile].setContact(entity, _groups, attributes, in_roster=True)
273 313
274 def _newMessage(self, from_jid_s, msg, type_, to_jid_s, extra, profile): 314 def _newMessage(self, from_jid_s, msg, type_, to_jid_s, extra, profile):
275 """newMessage premanagement: a dirty hack to manage private messages 315 from_jid = jid.JID(from_jid_s)
276 316 to_jid = jid.JID(to_jid_s)
277 if a private MUC message is detected, from_jid or to_jid is prefixed and resource is escaped
278 """
279 # FIXME: must be refactored for 0.6
280 from_jid = JID(from_jid_s)
281 to_jid = JID(to_jid_s)
282
283 from_me = from_jid.bare == self.profiles[profile]['whoami'].bare
284 win = to_jid if from_me else from_jid
285
286 if ((type_ != "groupchat" and self.contact_list.getSpecial(win) == "MUC") and
287 (type_ != C.MESS_TYPE_INFO or (type_ == C.MESS_TYPE_INFO and win.resource))):
288 #we have a private message in a MUC room
289 #XXX: normaly we use bare jid as key, here we need the full jid
290 # so we cheat by replacing the "/" before the resource by
291 # a "@", so the jid is invalid,
292 new_jid = escapePrivate(win)
293 if from_me:
294 to_jid = new_jid
295 else:
296 from_jid = new_jid
297 if new_jid not in self.contact_list:
298 self.contact_list.add(new_jid, [C.GROUP_NOT_IN_ROSTER])
299
300 self.newMessageHandler(from_jid, to_jid, msg, type_, extra, profile) 317 self.newMessageHandler(from_jid, to_jid, msg, type_, extra, profile)
301 318
302 def newMessageHandler(self, from_jid, to_jid, msg, type_, extra, profile): 319 def newMessageHandler(self, from_jid, to_jid, msg, type_, extra, profile):
303 from_me = from_jid.bare == self.profiles[profile]['whoami'].bare 320 from_me = from_jid.bare == self.profiles[profile].whoami.bare
304 win = to_jid if from_me else from_jid 321 target = to_jid if from_me else from_jid
305 322
306 self.current_action_ids = set() 323 chat_type = C.CHAT_GROUP if type_ == C.MESS_TYPE_GROUPCHAT else C.CHAT_ONE2ONE
307 self.current_action_ids_cb = {} 324
308 325 chat_widget = self.widgets.getOrCreateWidget(quick_chat.QuickChat, target, type_=chat_type, profile=profile)
309 timestamp = extra.get('archive') 326
310 if type_ == C.MESS_TYPE_INFO: 327 self.current_action_ids = set() # FIXME: to be removed
311 self.chat_wins[win.bare].printInfo(msg, timestamp=float(timestamp) if timestamp else None) 328 self.current_action_ids_cb = {} # FIXME: to be removed
312 else: 329
313 self.chat_wins[win.bare].printMessage(from_jid, msg, profile, float(timestamp) if timestamp else None) 330 chat_widget.newMessage(from_jid, target, msg, type_, extra, profile)
314 331
315 def sendMessage(self, to_jid, message, subject='', mess_type="auto", extra={}, callback=None, errback=None, profile_key="@NONE@"): 332 def sendMessage(self, to_jid, message, subject='', mess_type="auto", extra={}, callback=None, errback=None, profile_key=C.PROF_KEY_NONE):
316 if to_jid.startswith(C.PRIVATE_PREFIX):
317 to_jid = unescapePrivate(to_jid)
318 mess_type = "chat"
319 if callback is None: 333 if callback is None:
320 callback = lambda: None 334 callback = lambda: None
321 if errback is None: 335 if errback is None:
322 errback = lambda failure: self.showDialog(failure.fullname, failure.message, "error") 336 errback = lambda failure: self.showDialog(failure.fullname, failure.message, "error")
323 self.bridge.sendMessage(to_jid, message, subject, mess_type, extra, profile_key, callback=callback, errback=errback) 337 self.bridge.sendMessage(to_jid, message, subject, mess_type, extra, profile_key, callback=callback, errback=errback)
324 338
325 def newAlertHandler(self, msg, title, alert_type, profile): 339 def newAlertHandler(self, msg, title, alert_type, profile):
326 assert alert_type in ['INFO', 'ERROR'] 340 assert alert_type in ['INFO', 'ERROR']
327 self.showDialog(unicode(msg), unicode(title), alert_type.lower()) 341 self.showDialog(unicode(msg), unicode(title), alert_type.lower())
328 342
329 def setStatusOnline(self, online=True, show="", statuses={}): 343 def setStatusOnline(self, online=True, show="", statuses={}, profile=C.PROF_KEY_NONE):
330 raise NotImplementedError 344 raise NotImplementedError
331 345
332 def presenceUpdateHandler(self, jabber_id, show, priority, statuses, profile): 346 def presenceUpdateHandler(self, entity_s, show, priority, statuses, profile):
333 347
334 log.debug(_("presence update for %(jid)s (show=%(show)s, priority=%(priority)s, statuses=%(statuses)s) [profile:%(profile)s]") 348 log.debug(_("presence update for %(entity)s (show=%(show)s, priority=%(priority)s, statuses=%(statuses)s) [profile:%(profile)s]")
335 % {'jid': jabber_id, 'show': show, 'priority': priority, 'statuses': statuses, 'profile': profile}) 349 % {'entity': entity_s, C.PRESENCE_SHOW: show, C.PRESENCE_PRIORITY: priority, C.PRESENCE_STATUSES: statuses, 'profile': profile})
336 from_jid = JID(jabber_id) 350 entity = jid.JID(entity_s)
337 351
338 if from_jid == self.profiles[profile]['whoami']: 352 if entity == self.profiles[profile].whoami:
339 if show == "unavailable": 353 if show == "unavailable":
340 self.setStatusOnline(False) 354 self.setStatusOnline(False, profile=profile)
341 else: 355 else:
342 self.setStatusOnline(True, show, statuses) 356 self.setStatusOnline(True, show, statuses, profile=profile)
343 return 357 return
344 358
345 presences = self.profiles[profile].setdefault('presences', {}) 359 # #FIXME: must be moved in a plugin
346 360 # if entity.bare in self.profiles[profile].data.get('watched',[]) and not entity.bare in self.profiles[profile]['onlineContact']:
347 if show != 'unavailable': 361 # self.showAlert(_("Watched jid [%s] is connected !") % entity.bare)
348 362
349 #FIXME: must be moved in a plugin 363 self.contact_lists[profile].updatePresence(entity, show, priority, statuses)
350 if from_jid.bare in self.profiles[profile].get('watched',[]) and not from_jid.bare in self.profiles[profile]['onlineContact']: 364
351 self.showAlert(_("Watched jid [%s] is connected !") % from_jid.bare) 365 def roomJoinedHandler(self, room_jid_s, room_nicks, user_nick, profile):
352
353 presences[jabber_id] = {'show': show, 'priority': priority, 'statuses': statuses}
354 self.profiles[profile].setdefault('onlineContact',set()).add(from_jid) # FIXME onlineContact is useless with CM, must be removed
355
356 #TODO: vcard data (avatar)
357
358 if show == "unavailable" and from_jid in self.profiles[profile].get('onlineContact',set()):
359 try:
360 del presences[jabber_id]
361 except KeyError:
362 pass
363 self.profiles[profile]['onlineContact'].remove(from_jid)
364
365 # check if the contact is connected with another resource, use the one with highest priority
366 jids = [jid for jid in presences if JID(jid).bare == from_jid.bare]
367 if jids:
368 max_jid = max(jids, key=lambda jid: presences[jid]['priority'])
369 data = presences[max_jid]
370 max_priority = data['priority']
371 if show == "unavailable": # do not check the priority here, because 'unavailable' has a dummy one
372 from_jid = JID(max_jid)
373 show, priority, statuses = data['show'], data['priority'], data['statuses']
374 if not jids or priority >= max_priority:
375 # case 1: not jids means all resources are disconnected, send the 'unavailable' presence
376 # case 2: update (or confirm) with the values of the resource which takes precedence
377 self.contact_list.updatePresence(from_jid, show, priority, statuses)
378
379 def roomJoinedHandler(self, room_jid, room_nicks, user_nick, profile):
380 """Called when a MUC room is joined""" 366 """Called when a MUC room is joined"""
381 log.debug(_("Room [%(room_jid)s] joined by %(profile)s, users presents:%(users)s") % {'room_jid': room_jid, 'profile': profile, 'users': room_nicks}) 367 log.debug("Room [%(room_jid)s] joined by %(profile)s, users presents:%(users)s" % {'room_jid': room_jid_s, 'profile': profile, 'users': room_nicks})
382 self.chat_wins[room_jid].setUserNick(user_nick) 368 room_jid = jid.JID(room_jid_s)
383 self.chat_wins[room_jid].setType("group") 369 chat_widget = self.widgets.getOrCreateWidget(quick_chat.QuickChat, room_jid, type_='group', profile=profile)
384 self.chat_wins[room_jid].id = room_jid 370 chat_widget.setUserNick(user_nick)
385 self.chat_wins[room_jid].setPresents(list(set([user_nick] + room_nicks))) 371 chat_widget.id = room_jid # FIXME: to be removed
386 self.contact_list.setSpecial(JID(room_jid), "MUC", show=True) 372 chat_widget.setPresents(list(set([user_nick] + room_nicks)))
373 self.contact_lists[profile].setSpecial(room_jid, C.CONTACT_SPECIAL_GROUP)
387 374
388 def roomLeftHandler(self, room_jid_s, profile): 375 def roomLeftHandler(self, room_jid_s, profile):
389 """Called when a MUC room is left""" 376 """Called when a MUC room is left"""
390 log.debug(_("Room [%(room_jid)s] left by %(profile)s") % {'room_jid': room_jid_s, 'profile': profile}) 377 log.debug("Room [%(room_jid)s] left by %(profile)s" % {'room_jid': room_jid_s, 'profile': profile})
391 del self.chat_wins[room_jid_s] 378 del self.chat_wins[room_jid_s]
392 self.contact_list.remove(JID(room_jid_s)) 379 self.contact_lists[profile].remove(jid.JID(room_jid_s))
393 380
394 def roomUserJoinedHandler(self, room_jid, user_nick, user_data, profile): 381 def roomUserJoinedHandler(self, room_jid_s, user_nick, user_data, profile):
395 """Called when an user joined a MUC room""" 382 """Called when an user joined a MUC room"""
396 if room_jid in self.chat_wins: 383 room_jid = jid.JID(room_jid_s)
397 self.chat_wins[room_jid].replaceUser(user_nick) 384 chat_widget = self.widgets.getOrCreateWidget(quick_chat.QuickChat, room_jid, type_='group', profile=profile)
398 log.debug(_("user [%(user_nick)s] joined room [%(room_jid)s]") % {'user_nick': user_nick, 'room_jid': room_jid}) 385 chat_widget.replaceUser(user_nick)
399 386 log.debug("user [%(user_nick)s] joined room [%(room_jid)s]" % {'user_nick': user_nick, 'room_jid': room_jid})
400 def roomUserLeftHandler(self, room_jid, user_nick, user_data, profile): 387
388 def roomUserLeftHandler(self, room_jid_s, user_nick, user_data, profile):
401 """Called when an user joined a MUC room""" 389 """Called when an user joined a MUC room"""
402 if room_jid in self.chat_wins: 390 room_jid = jid.JID(room_jid_s)
403 self.chat_wins[room_jid].removeUser(user_nick) 391 chat_widget = self.widgets.getOrCreateWidget(quick_chat.QuickChat, room_jid, type_='group', profile=profile)
404 log.debug(_("user [%(user_nick)s] left room [%(room_jid)s]") % {'user_nick': user_nick, 'room_jid': room_jid}) 392 chat_widget.removeUser(user_nick)
405 393 log.debug("user [%(user_nick)s] left room [%(room_jid)s]" % {'user_nick': user_nick, 'room_jid': room_jid})
406 def roomUserChangedNickHandler(self, room_jid, old_nick, new_nick, profile): 394
395 def roomUserChangedNickHandler(self, room_jid_s, old_nick, new_nick, profile):
407 """Called when an user joined a MUC room""" 396 """Called when an user joined a MUC room"""
408 if room_jid in self.chat_wins: 397 room_jid = jid.JID(room_jid_s)
409 self.chat_wins[room_jid].changeUserNick(old_nick, new_nick) 398 chat_widget = self.widgets.getOrCreateWidget(quick_chat.QuickChat, room_jid, type_='group', profile=profile)
410 log.debug(_("user [%(old_nick)s] is now known as [%(new_nick)s] in room [%(room_jid)s]") % {'old_nick': old_nick, 'new_nick': new_nick, 'room_jid': room_jid}) 399 chat_widget.changeUserNick(old_nick, new_nick)
411 400 log.debug("user [%(old_nick)s] is now known as [%(new_nick)s] in room [%(room_jid)s]" % {'old_nick': old_nick, 'new_nick': new_nick, 'room_jid': room_jid})
412 def roomNewSubjectHandler(self, room_jid, subject, profile): 401
402 def roomNewSubjectHandler(self, room_jid_s, subject, profile):
413 """Called when subject of MUC room change""" 403 """Called when subject of MUC room change"""
414 if room_jid in self.chat_wins: 404 room_jid = jid.JID(room_jid_s)
415 self.chat_wins[room_jid].setSubject(subject) 405 chat_widget = self.widgets.getOrCreateWidget(quick_chat.QuickChat, room_jid, type_='group', profile=profile)
416 log.debug(_("new subject for room [%(room_jid)s]: %(subject)s") % {'room_jid': room_jid, "subject": subject}) 406 chat_widget.setSubject(subject)
417 407 log.debug("new subject for room [%(room_jid)s]: %(subject)s" % {'room_jid': room_jid, "subject": subject})
418 def tarotGameStartedHandler(self, room_jid, referee, players, profile): 408
409 def tarotGameStartedHandler(self, room_jid_s, referee, players, profile):
419 log.debug(_("Tarot Game Started \o/")) 410 log.debug(_("Tarot Game Started \o/"))
420 if room_jid in self.chat_wins: 411 room_jid = jid.JID(room_jid_s)
421 self.chat_wins[room_jid].startGame("Tarot", referee, players) 412 chat_widget = self.widgets.getOrCreateWidget(quick_chat.QuickChat, room_jid, type_='group', profile=profile)
422 log.debug(_("new Tarot game started by [%(referee)s] in room [%(room_jid)s] with %(players)s") % {'referee': referee, 'room_jid': room_jid, 'players': [str(player) for player in players]}) 413 chat_widget.startGame("Tarot", referee, players)
414 log.debug("new Tarot game started by [%(referee)s] in room [%(room_jid)s] with %(players)s" % {'referee': referee, 'room_jid': room_jid, 'players': [str(player) for player in players]})
423 415
424 def tarotGameNewHandler(self, room_jid, hand, profile): 416 def tarotGameNewHandler(self, room_jid, hand, profile):
425 log.debug(_("New Tarot Game")) 417 log.debug(_("New Tarot Game"))
426 if room_jid in self.chat_wins: 418 if room_jid in self.chat_wins:
427 self.chat_wins[room_jid].getGame("Tarot").newGame(hand) 419 self.chat_wins[room_jid].getGame("Tarot").newGame(hand)
499 """Called when the question is not answered, and we still have time""" 491 """Called when the question is not answered, and we still have time"""
500 if room_jid in self.chat_wins: 492 if room_jid in self.chat_wins:
501 self.chat_wins[room_jid].getGame("Quiz").quizGameTimerRestartedHandler(time_left) 493 self.chat_wins[room_jid].getGame("Quiz").quizGameTimerRestartedHandler(time_left)
502 494
503 def chatStateReceivedHandler(self, from_jid_s, state, profile): 495 def chatStateReceivedHandler(self, from_jid_s, state, profile):
504 """Callback when a new chat state is received. 496 """Called when a new chat state is received.
497
505 @param from_jid_s: JID of the contact who sent his state, or '@ALL@' 498 @param from_jid_s: JID of the contact who sent his state, or '@ALL@'
506 @param state: new state (string) 499 @param state: new state (string)
507 @profile: current profile 500 @profile: current profile
508 """ 501 """
509 502 from_jid = jid.JID(from_jid_s) if from_jid_s != C.ENTITY_ALL else C.ENTITY_ALL
510 if from_jid_s == '@ALL@': 503 for widget in self.visible_widgets:
511 target = '@ALL@' 504 if isinstance(widget, quick_chat.QuickChat):
512 nick = C.ALL_OCCUPANTS 505 if from_jid == C.ENTITY_ALL or from_jid.bare == widget.target.bare:
513 else: 506 widget.updateChatState(from_jid, state)
514 from_jid = JID(from_jid_s)
515 target = from_jid.bare
516 nick = from_jid.resource
517
518 for bare in self.chat_wins.keys():
519 if target == '@ALL' or target == bare:
520 chat_win = self.chat_wins[bare]
521 if chat_win.type == 'one2one':
522 chat_win.updateChatState(state)
523 elif chat_win.type == 'group':
524 chat_win.updateChatState(state, nick=nick)
525 507
526 def _subscribe_cb(self, answer, data): 508 def _subscribe_cb(self, answer, data):
527 entity, profile = data 509 entity, profile = data
528 if answer: 510 if answer:
529 self.bridge.subscription("subscribed", entity.bare, profile_key=profile) 511 self.bridge.subscription("subscribed", entity.bare, profile_key=profile)
530 else: 512 else:
531 self.bridge.subscription("unsubscribed", entity.bare, profile_key=profile) 513 self.bridge.subscription("unsubscribed", entity.bare, profile_key=profile)
532 514
533 def subscribeHandler(self, type, raw_jid, profile): 515 def subscribeHandler(self, type, raw_jid, profile):
534 """Called when a subsciption management signal is received""" 516 """Called when a subsciption management signal is received"""
535 entity = JID(raw_jid) 517 entity = jid.JID(raw_jid)
536 if type == "subscribed": 518 if type == "subscribed":
537 # this is a subscription confirmation, we just have to inform user 519 # this is a subscription confirmation, we just have to inform user
538 self.showDialog(_("The contact %s has accepted your subscription") % entity.bare, _('Subscription confirmation')) 520 self.showDialog(_("The contact %s has accepted your subscription") % entity.bare, _('Subscription confirmation'))
539 elif type == "unsubscribed": 521 elif type == "unsubscribed":
540 # this is a subscription refusal, we just have to inform user 522 # this is a subscription refusal, we just have to inform user
551 533
552 def paramUpdateHandler(self, name, value, namespace, profile): 534 def paramUpdateHandler(self, name, value, namespace, profile):
553 log.debug(_("param update: [%(namespace)s] %(name)s = %(value)s") % {'namespace': namespace, 'name': name, 'value': value}) 535 log.debug(_("param update: [%(namespace)s] %(name)s = %(value)s") % {'namespace': namespace, 'name': name, 'value': value})
554 if (namespace, name) == ("Connection", "JabberID"): 536 if (namespace, name) == ("Connection", "JabberID"):
555 log.debug(_("Changing JID to %s") % value) 537 log.debug(_("Changing JID to %s") % value)
556 self.profiles[profile]['whoami'] = JID(value) 538 self.profiles[profile].whoami = jid.JID(value)
557 elif (namespace, name) == ("Misc", "Watched"): 539 elif (namespace, name) == ("Misc", "Watched"):
558 self.profiles[profile]['watched'] = value.split() 540 self.profiles[profile]['watched'] = value.split()
559 elif (namespace, name) == ('General', C.SHOW_OFFLINE_CONTACTS): 541 elif (namespace, name) == ('General', C.SHOW_OFFLINE_CONTACTS):
560 self.contact_list.showOfflineContacts(C.bool(value)) 542 self.contact_lists[profile].showOfflineContacts(C.bool(value))
561 elif (namespace, name) == ('General', C.SHOW_EMPTY_GROUPS): 543 elif (namespace, name) == ('General', C.SHOW_EMPTY_GROUPS):
562 self.contact_list.showEmptyGroups(C.bool(value)) 544 self.contact_lists[profile].showEmptyGroups(C.bool(value))
563 545
564 def contactDeletedHandler(self, jid, profile): 546 def contactDeletedHandler(self, jid, profile):
565 target = JID(jid) 547 target = jid.JID(jid)
566 self.contact_list.remove(target) 548 self.contact_lists[profile].remove(target)
567 try: 549
568 self.profiles[profile]['onlineContact'].remove(target.bare) 550 def entityDataUpdatedHandler(self, entity_s, key, value, profile):
569 except KeyError: 551 entity = jid.JID(entity_s)
570 pass
571
572 def entityDataUpdatedHandler(self, jid_str, key, value, profile):
573 jid = JID(jid_str)
574 if key == "nick": 552 if key == "nick":
575 if jid in self.contact_list: 553 if entity in self.contact_lists[profile]:
576 self.contact_list.setCache(jid, 'nick', value) 554 self.contact_lists[profile].setCache(entity, 'nick', value)
577 self.contact_list.replace(jid)
578 elif key == "avatar": 555 elif key == "avatar":
579 if jid in self.contact_list: 556 if entity in self.contact_lists[profile]:
580 filename = self.bridge.getAvatarFile(value) 557 filename = self.bridge.getAvatarFile(value)
581 self.contact_list.setCache(jid, 'avatar', filename) 558 self.contact_lists[profile].setCache(entity, 'avatar', filename)
582 self.contact_list.replace(jid)
583 559
584 def askConfirmationHandler(self, confirm_id, confirm_type, data, profile): 560 def askConfirmationHandler(self, confirm_id, confirm_type, data, profile):
585 raise NotImplementedError 561 raise NotImplementedError
586 562
587 def actionResultHandler(self, type, id, data, profile): 563 def actionResultHandler(self, type, id, data, profile):
588 raise NotImplementedError 564 raise NotImplementedError
589 565
590 def launchAction(self, callback_id, data=None, profile_key="@NONE@"): 566 def launchAction(self, callback_id, data=None, callback=None, profile="@NONE@"):
591 """ Launch a dynamic action 567 """Launch a dynamic action
592 @param callback_id: id of the action to launch 568 @param callback_id: id of the action to launch
593 @param data: data needed only for certain actions 569 @param data: data needed only for certain actions
594 @param profile_key: %(doc_profile_key)s 570 @param callback: if not None and 'validated' key is present, it will be called with the following parameters:
571 - callback_id
572 - data
573 - profile_key
574 @param profile_key: %(doc_profile)s
595 575
596 """ 576 """
597 raise NotImplementedError 577 raise NotImplementedError
598 578
599 def onExit(self): 579 def onExit(self):
600 """Must be called when the frontend is terminating""" 580 """Must be called when the frontend is terminating"""
601 #TODO: mange multi-profile here 581 for profile in self.profiles:
602 try: 582 if self.bridge.isConnected(profile):
603 if self.bridge.isConnected(self.profile): 583 if C.bool(self.bridge.getParamA("autodisconnect", "Connection", profile_key=profile)):
604 if self.bridge.getParamA("autodisconnect", "Connection", profile_key=self.profile) == "true":
605 #The user wants autodisconnection 584 #The user wants autodisconnection
606 self.bridge.disconnect(self.profile) 585 self.bridge.disconnect(profile)
607 except:
608 pass