correctly set transient window for muc error dialogs. Fixes #6943
[gajim.git] / src / session.py
bloba4e9de595e6d337a1b6bad5048a3c779d16656c6
1 # -*- coding:utf-8 -*-
2 ## src/session.py
3 ##
4 ## Copyright (C) 2008-2010 Yann Leboulanger <asterix AT lagaule.org>
5 ## Copyright (C) 2008 Brendan Taylor <whateley AT gmail.com>
6 ## Jonathan Schleifer <js-gajim AT webkeks.org>
7 ## Stephan Erb <steve-e AT h3c.de>
8 ##
9 ## This file is part of Gajim.
11 ## Gajim is free software; you can redistribute it and/or modify
12 ## it under the terms of the GNU General Public License as published
13 ## by the Free Software Foundation; version 3 only.
15 ## Gajim is distributed in the hope that it will be useful,
16 ## but WITHOUT ANY WARRANTY; without even the implied warranty of
17 ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18 ## GNU General Public License for more details.
20 ## You should have received a copy of the GNU General Public License
21 ## along with Gajim. If not, see <http://www.gnu.org/licenses/>.
24 from common import helpers
26 from common import exceptions
27 from common import gajim
28 from common import stanza_session
29 from common import contacts
30 from common import ged
31 from common.connection_handlers_events import ChatstateReceivedEvent
33 import common.xmpp
35 import message_control
37 import notify
39 import dialogs
40 import negotiation
42 class ChatControlSession(stanza_session.EncryptedStanzaSession):
43 def __init__(self, conn, jid, thread_id, type_='chat'):
44 stanza_session.EncryptedStanzaSession.__init__(self, conn, jid, thread_id,
45 type_='chat')
46 gajim.ged.register_event_handler('decrypted-message-received', ged.GUI1,
47 self._nec_decrypted_message_received)
49 self.control = None
51 def detach_from_control(self):
52 if self.control:
53 self.control.set_session(None)
54 gajim.ged.remove_event_handler('decrypted-message-received',
55 ged.GUI1, self._nec_decrypted_message_received)
57 def acknowledge_termination(self):
58 self.detach_from_control()
59 stanza_session.EncryptedStanzaSession.acknowledge_termination(self)
61 def terminate(self, send_termination = True):
62 stanza_session.EncryptedStanzaSession.terminate(self, send_termination)
63 self.detach_from_control()
65 def _nec_decrypted_message_received(self, obj):
66 """
67 Dispatch a received <message> stanza
68 """
69 if obj.session != self:
70 return
71 if self.resource != obj.resource:
72 self.resource = obj.resource
73 if self.control and self.control.resource:
74 self.control.change_resource(self.resource)
76 msg_id = None
78 if obj.mtype == 'chat':
79 if not obj.stanza.getTag('body') and obj.chatstate is None:
80 return
82 log_type = 'chat_msg_recv'
83 else:
84 log_type = 'single_msg_recv'
86 if self.is_loggable() and obj.msgtxt:
87 try:
88 if obj.xhtml and gajim.config.get('log_xhtml_messages'):
89 msg_to_log = obj.xhtml
90 else:
91 msg_to_log = obj.msgtxt
92 msg_id = gajim.logger.write(log_type, obj.fjid,
93 msg_to_log, tim=obj.timestamp, subject=obj.subject)
94 except exceptions.PysqliteOperationalError, e:
95 self.conn.dispatch('ERROR', (_('Disk WriteError'), str(e)))
96 except exceptions.DatabaseMalformed:
97 pritext = _('Database Error')
98 sectext = _('The database file (%s) cannot be read. Try to '
99 'repair it (see http://trac.gajim.org/wiki/DatabaseBackup) '
100 'or remove it (all history will be lost).') % \
101 common.logger.LOG_DB_PATH
102 self.conn.dispatch('ERROR', (pritext, sectext))
104 obj.msg_id = msg_id
106 treat_as = gajim.config.get('treat_incoming_messages')
107 if treat_as:
108 obj.mtype = treat_as
109 pm = False
110 if obj.gc_control and obj.resource:
111 # It's a Private message
112 pm = True
113 obj.mtype = 'pm'
115 # Handle chat states
116 contact = gajim.contacts.get_contact(self.conn.name, obj.jid,
117 obj.resource)
118 if contact:
119 if contact.composing_xep != 'XEP-0085': # We cache xep85 support
120 contact.composing_xep = obj.composing_xep
121 if self.control and self.control.type_id == \
122 message_control.TYPE_CHAT:
123 if obj.chatstate is not None:
124 # other peer sent us reply, so he supports jep85 or jep22
125 contact.chatstate = obj.chatstate
126 if contact.our_chatstate == 'ask': # we were jep85 disco?
127 contact.our_chatstate = 'active' # no more
128 gajim.nec.push_incoming_event(ChatstateReceivedEvent(None,
129 conn=obj.conn, msg_obj=obj))
130 elif contact.chatstate != 'active':
131 # got no valid jep85 answer, peer does not support it
132 contact.chatstate = False
133 elif obj.chatstate == 'active':
134 # Brand new message, incoming.
135 contact.our_chatstate = obj.chatstate
136 contact.chatstate = obj.chatstate
137 if msg_id: # Do not overwrite an existing msg_id with None
138 contact.msg_id = msg_id
140 # THIS MUST BE AFTER chatstates handling
141 # AND BEFORE playsound (else we ear sounding on chatstates!)
142 if not obj.msgtxt: # empty message text
143 return True
145 if gajim.config.get_per('accounts', self.conn.name,
146 'ignore_unknown_contacts') and not gajim.contacts.get_contacts(
147 self.conn.name, obj.jid) and not pm:
148 return True
150 highest_contact = gajim.contacts.get_contact_with_highest_priority(
151 self.conn.name, obj.jid)
153 # does this resource have the highest priority of any available?
154 is_highest = not highest_contact or not highest_contact.resource or \
155 obj.resource == highest_contact.resource or highest_contact.show ==\
156 'offline'
158 if not pm and is_highest:
159 jid_of_control = obj.jid
160 else:
161 jid_of_control = obj.fjid
163 if not self.control:
164 ctrl = gajim.interface.msg_win_mgr.get_control(jid_of_control,
165 self.conn.name)
166 if ctrl:
167 self.control = ctrl
168 self.control.set_session(self)
170 if not pm:
171 self.roster_message2(obj)
173 if gajim.interface.remote_ctrl:
174 gajim.interface.remote_ctrl.raise_signal('NewMessage', (
175 self.conn.name, [obj.fjid, obj.msgtxt, obj.timestamp,
176 obj.encrypted, obj.mtype, obj.subject, obj.chatstate, msg_id,
177 obj.composing_xep, obj.user_nick, obj.xhtml, obj.form_node]))
179 def roster_message2(self, obj):
181 Display the message or show notification in the roster
183 contact = None
184 jid = obj.jid
185 resource = obj.resource
186 # if chat window will be for specific resource
187 resource_for_chat = resource
189 fjid = jid
191 # Try to catch the contact with correct resource
192 if resource:
193 fjid = jid + '/' + resource
194 contact = gajim.contacts.get_contact(obj.conn.name, jid, resource)
196 highest_contact = gajim.contacts.get_contact_with_highest_priority(
197 obj.conn.name, jid)
198 if not contact:
199 # If there is another resource, it may be a message from an
200 # invisible resource
201 lcontact = gajim.contacts.get_contacts(obj.conn.name, jid)
202 if (len(lcontact) > 1 or (lcontact and lcontact[0].resource and \
203 lcontact[0].show != 'offline')) and jid.find('@') > 0:
204 contact = gajim.contacts.copy_contact(highest_contact)
205 contact.resource = resource
206 contact.priority = 0
207 contact.show = 'offline'
208 contact.status = ''
209 gajim.contacts.add_contact(obj.conn.name, contact)
211 else:
212 # Default to highest prio
213 fjid = jid
214 resource_for_chat = None
215 contact = highest_contact
217 if not contact:
218 # contact is not in roster
219 contact = gajim.interface.roster.add_to_not_in_the_roster(
220 obj.conn.name, jid, obj.user_nick)
222 if not self.control:
223 ctrl = gajim.interface.msg_win_mgr.get_control(fjid, self.conn.name)
224 if ctrl:
225 self.control = ctrl
226 self.control.set_session(self)
227 else:
228 # if no control exists and message comes from highest prio,
229 # the new control shouldn't have a resource
230 if highest_contact and contact.resource == \
231 highest_contact.resource and jid != gajim.get_jid_from_account(
232 self.conn.name):
233 fjid = jid
234 resource_for_chat = None
236 obj.popup = helpers.allow_popup_window(self.conn.name)
237 obj.resource_for_chat = resource_for_chat
239 type_ = 'chat'
240 event_type = 'message_received'
242 if obj.mtype == 'normal':
243 type_ = 'normal'
244 event_type = 'single_message_received'
246 if self.control and obj.mtype != 'normal':
247 obj.show_in_roster = False
248 obj.show_in_systray = False
249 else:
250 obj.show_in_roster = notify.get_show_in_roster(event_type,
251 self.conn.name, contact, self)
252 obj.show_in_systray = notify.get_show_in_systray(event_type,
253 self.conn.name, contact)
255 if not self.control:
256 event = gajim.events.create_event(type_, (obj.msgtxt, obj.subject,
257 obj.mtype, obj.timestamp, obj.encrypted, obj.resource,
258 obj.msg_id, obj.xhtml, self, obj.form_node, obj.displaymarking),
259 show_in_roster=obj.show_in_roster,
260 show_in_systray=obj.show_in_systray)
262 gajim.events.add_event(self.conn.name, fjid, event)
264 def roster_message(self, jid, msg, tim, encrypted=False, msg_type='',
265 subject=None, resource='', msg_id=None, user_nick='', xhtml=None,
266 form_node=None, displaymarking=None):
268 Display the message or show notification in the roster
270 contact = None
271 # if chat window will be for specific resource
272 resource_for_chat = resource
274 fjid = jid
276 # Try to catch the contact with correct resource
277 if resource:
278 fjid = jid + '/' + resource
279 contact = gajim.contacts.get_contact(self.conn.name, jid, resource)
281 highest_contact = gajim.contacts.get_contact_with_highest_priority(
282 self.conn.name, jid)
283 if not contact:
284 # If there is another resource, it may be a message from an invisible
285 # resource
286 lcontact = gajim.contacts.get_contacts(self.conn.name, jid)
287 if (len(lcontact) > 1 or (lcontact and lcontact[0].resource and \
288 lcontact[0].show != 'offline')) and jid.find('@') > 0:
289 contact = gajim.contacts.copy_contact(highest_contact)
290 contact.resource = resource
291 if resource:
292 fjid = jid + '/' + resource
293 contact.priority = 0
294 contact.show = 'offline'
295 contact.status = ''
296 gajim.contacts.add_contact(self.conn.name, contact)
298 else:
299 # Default to highest prio
300 fjid = jid
301 resource_for_chat = None
302 contact = highest_contact
304 if not contact:
305 # contact is not in roster
306 contact = gajim.interface.roster.add_to_not_in_the_roster(
307 self.conn.name, jid, user_nick)
309 if not self.control:
310 ctrl = gajim.interface.msg_win_mgr.get_control(fjid, self.conn.name)
311 if ctrl:
312 self.control = ctrl
313 self.control.set_session(self)
314 else:
315 # if no control exists and message comes from highest prio, the new
316 # control shouldn't have a resource
317 if highest_contact and contact.resource == highest_contact.resource\
318 and not jid == gajim.get_jid_from_account(self.conn.name):
319 fjid = jid
320 resource_for_chat = None
322 # Do we have a queue?
323 no_queue = len(gajim.events.get_events(self.conn.name, fjid)) == 0
325 popup = helpers.allow_popup_window(self.conn.name)
327 if msg_type == 'normal' and popup: # it's single message to be autopopuped
328 dialogs.SingleMessageWindow(self.conn.name, contact.jid,
329 action='receive', from_whom=jid, subject=subject, message=msg,
330 resource=resource, session=self, form_node=form_node)
331 return
333 # We print if window is opened and it's not a single message
334 if self.control and msg_type != 'normal':
335 typ = ''
337 if msg_type == 'error':
338 typ = 'error'
340 self.control.print_conversation(msg, typ, tim=tim, encrypted=encrypted,
341 subject=subject, xhtml=xhtml, displaymarking=displaymarking)
343 if msg_id:
344 gajim.logger.set_read_messages([msg_id])
346 return
348 # We save it in a queue
349 type_ = 'chat'
350 event_type = 'message_received'
352 if msg_type == 'normal':
353 type_ = 'normal'
354 event_type = 'single_message_received'
356 show_in_roster = notify.get_show_in_roster(event_type, self.conn.name,
357 contact, self)
358 show_in_systray = notify.get_show_in_systray(event_type, self.conn.name,
359 contact)
361 event = gajim.events.create_event(type_, (msg, subject, msg_type, tim,
362 encrypted, resource, msg_id, xhtml, self, form_node, displaymarking),
363 show_in_roster=show_in_roster, show_in_systray=show_in_systray)
365 gajim.events.add_event(self.conn.name, fjid, event)
367 if popup:
368 if not self.control:
369 self.control = gajim.interface.new_chat(contact,
370 self.conn.name, resource=resource_for_chat, session=self)
372 if len(gajim.events.get_events(self.conn.name, fjid)):
373 self.control.read_queue()
374 else:
375 if no_queue: # We didn't have a queue: we change icons
376 gajim.interface.roster.draw_contact(jid, self.conn.name)
378 gajim.interface.roster.show_title() # we show the * or [n]
379 # Select the big brother contact in roster, it's visible because it has
380 # events.
381 family = gajim.contacts.get_metacontacts_family(self.conn.name, jid)
382 if family:
383 nearby_family, bb_jid, bb_account = \
384 gajim.contacts.get_nearby_family_and_big_brother(family,
385 self.conn.name)
386 else:
387 bb_jid, bb_account = jid, self.conn.name
388 gajim.interface.roster.select_contact(bb_jid, bb_account)
390 # ---- ESessions stuff ---
392 def handle_negotiation(self, form):
393 if form.getField('accept') and not form['accept'] in ('1', 'true'):
394 self.cancelled_negotiation()
395 return
397 # encrypted session states. these are described in stanza_session.py
399 try:
400 if form.getType() == 'form' and 'security' in form.asDict():
401 security_options = [x[1] for x in form.getField('security').\
402 getOptions()]
403 if security_options == ['none']:
404 self.respond_archiving(form)
405 else:
406 # bob responds
408 # we don't support 3-message negotiation as the responder
409 if 'dhkeys' in form.asDict():
410 self.fail_bad_negotiation('3 message negotiation not '
411 'supported when responding', ('dhkeys',))
412 return
414 negotiated, not_acceptable, ask_user = \
415 self.verify_options_bob(form)
417 if ask_user:
418 def accept_nondefault_options(is_checked):
419 self.dialog.destroy()
420 negotiated.update(ask_user)
421 self.respond_e2e_bob(form, negotiated,
422 not_acceptable)
424 def reject_nondefault_options():
425 self.dialog.destroy()
426 for key in ask_user.keys():
427 not_acceptable.append(key)
428 self.respond_e2e_bob(form, negotiated,
429 not_acceptable)
431 self.dialog = dialogs.YesNoDialog(_('Confirm these '
432 'session options'), _('''The remote client wants '
433 'to negotiate an session with these features:
437 Are these options acceptable?''') % (negotiation.describe_features(
438 ask_user)),
439 on_response_yes=accept_nondefault_options,
440 on_response_no=reject_nondefault_options)
441 else:
442 self.respond_e2e_bob(form, negotiated, not_acceptable)
444 return
446 elif self.status == 'requested-archiving' and form.getType() == \
447 'submit':
448 try:
449 self.archiving_accepted(form)
450 except exceptions.NegotiationError, details:
451 self.fail_bad_negotiation(details)
453 return
455 # alice accepts
456 elif self.status == 'requested-e2e' and form.getType() == 'submit':
457 negotiated, not_acceptable, ask_user = self.verify_options_alice(
458 form)
460 if ask_user:
461 def accept_nondefault_options(is_checked):
462 dialog.destroy()
464 negotiated.update(ask_user)
466 try:
467 self.accept_e2e_alice(form, negotiated)
468 except exceptions.NegotiationError, details:
469 self.fail_bad_negotiation(details)
471 def reject_nondefault_options():
472 self.reject_negotiation()
473 dialog.destroy()
475 dialog = dialogs.YesNoDialog(_('Confirm these session options'),
476 _('The remote client selected these options:\n\n%s\n\n'
477 'Continue with the session?') % (
478 negotiation.describe_features(ask_user)),
479 on_response_yes = accept_nondefault_options,
480 on_response_no = reject_nondefault_options)
481 else:
482 try:
483 self.accept_e2e_alice(form, negotiated)
484 except exceptions.NegotiationError, details:
485 self.fail_bad_negotiation(details)
487 return
488 elif self.status == 'responded-archiving' and form.getType() == \
489 'result':
490 try:
491 self.we_accept_archiving(form)
492 except exceptions.NegotiationError, details:
493 self.fail_bad_negotiation(details)
495 return
496 elif self.status == 'responded-e2e' and form.getType() == 'result':
497 try:
498 self.accept_e2e_bob(form)
499 except exceptions.NegotiationError, details:
500 self.fail_bad_negotiation(details)
502 return
503 elif self.status == 'identified-alice' and form.getType() == 'result':
504 try:
505 self.final_steps_alice(form)
506 except exceptions.NegotiationError, details:
507 self.fail_bad_negotiation(details)
509 return
510 except exceptions.Cancelled:
511 # user cancelled the negotiation
513 self.reject_negotiation()
515 return
517 if form.getField('terminate') and\
518 form.getField('terminate').getValue() in ('1', 'true'):
519 self.acknowledge_termination()
521 self.conn.delete_session(str(self.jid), self.thread_id)
523 return
525 # non-esession negotiation. this isn't very useful, but i'm keeping it
526 # around to test my test suite.
527 if form.getType() == 'form':
528 if not self.control:
529 jid, resource = gajim.get_room_and_nick_from_fjid(str(self.jid))
531 account = self.conn.name
532 contact = gajim.contacts.get_contact(account, str(self.jid),
533 resource)
535 if not contact:
536 contact = gajim.contacts.create_contact(jid=jid, account=account,
537 resource=resource, show=self.conn.get_status())
539 gajim.interface.new_chat(contact, account, resource=resource,
540 session=self)
542 negotiation.FeatureNegotiationWindow(account, str(self.jid), self,
543 form)