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>
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
35 import message_control
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
,
46 gajim
.ged
.register_event_handler('decrypted-message-received', ged
.GUI1
,
47 self
._nec
_decrypted
_message
_received
)
51 def detach_from_control(self
):
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
):
67 Dispatch a received <message> stanza
69 if obj
.session
!= self
:
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
)
78 if obj
.mtype
== 'chat':
79 if not obj
.stanza
.getTag('body') and obj
.chatstate
is None:
82 log_type
= 'chat_msg_recv'
84 log_type
= 'single_msg_recv'
86 if self
.is_loggable() and obj
.msgtxt
:
88 if obj
.xhtml
and gajim
.config
.get('log_xhtml_messages'):
89 msg_to_log
= obj
.xhtml
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
))
106 treat_as
= gajim
.config
.get('treat_incoming_messages')
110 if obj
.gc_control
and obj
.resource
:
111 # It's a Private message
116 contact
= gajim
.contacts
.get_contact(self
.conn
.name
, obj
.jid
,
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
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
:
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
==\
158 if not pm
and is_highest
:
159 jid_of_control
= obj
.jid
161 jid_of_control
= obj
.fjid
164 ctrl
= gajim
.interface
.msg_win_mgr
.get_control(jid_of_control
,
168 self
.control
.set_session(self
)
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
185 resource
= obj
.resource
186 # if chat window will be for specific resource
187 resource_for_chat
= resource
191 # Try to catch the contact with correct 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(
199 # If there is another resource, it may be a message from an
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
207 contact
.show
= 'offline'
209 gajim
.contacts
.add_contact(obj
.conn
.name
, contact
)
212 # Default to highest prio
214 resource_for_chat
= None
215 contact
= highest_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
)
223 ctrl
= gajim
.interface
.msg_win_mgr
.get_control(fjid
, self
.conn
.name
)
226 self
.control
.set_session(self
)
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(
234 resource_for_chat
= None
236 obj
.popup
= helpers
.allow_popup_window(self
.conn
.name
)
237 obj
.resource_for_chat
= resource_for_chat
240 event_type
= 'message_received'
242 if obj
.mtype
== '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
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
)
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
271 # if chat window will be for specific resource
272 resource_for_chat
= resource
276 # Try to catch the contact with correct 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(
284 # If there is another resource, it may be a message from an invisible
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
292 fjid
= jid
+ '/' + resource
294 contact
.show
= 'offline'
296 gajim
.contacts
.add_contact(self
.conn
.name
, contact
)
299 # Default to highest prio
301 resource_for_chat
= None
302 contact
= highest_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
)
310 ctrl
= gajim
.interface
.msg_win_mgr
.get_control(fjid
, self
.conn
.name
)
313 self
.control
.set_session(self
)
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
):
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
)
333 # We print if window is opened and it's not a single message
334 if self
.control
and msg_type
!= 'normal':
337 if msg_type
== 'error':
340 self
.control
.print_conversation(msg
, typ
, tim
=tim
, encrypted
=encrypted
,
341 subject
=subject
, xhtml
=xhtml
, displaymarking
=displaymarking
)
344 gajim
.logger
.set_read_messages([msg_id
])
348 # We save it in a queue
350 event_type
= 'message_received'
352 if msg_type
== 'normal':
354 event_type
= 'single_message_received'
356 show_in_roster
= notify
.get_show_in_roster(event_type
, self
.conn
.name
,
358 show_in_systray
= notify
.get_show_in_systray(event_type
, self
.conn
.name
,
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
)
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()
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
381 family
= gajim
.contacts
.get_metacontacts_family(self
.conn
.name
, jid
)
383 nearby_family
, bb_jid
, bb_account
= \
384 gajim
.contacts
.get_nearby_family_and_big_brother(family
,
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()
397 # encrypted session states. these are described in stanza_session.py
400 if form
.getType() == 'form' and 'security' in form
.asDict():
401 security_options
= [x
[1] for x
in form
.getField('security').\
403 if security_options
== ['none']:
404 self
.respond_archiving(form
)
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',))
414 negotiated
, not_acceptable
, ask_user
= \
415 self
.verify_options_bob(form
)
418 def accept_nondefault_options(is_checked
):
419 self
.dialog
.destroy()
420 negotiated
.update(ask_user
)
421 self
.respond_e2e_bob(form
, negotiated
,
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
,
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(
439 on_response_yes
=accept_nondefault_options
,
440 on_response_no
=reject_nondefault_options
)
442 self
.respond_e2e_bob(form
, negotiated
, not_acceptable
)
446 elif self
.status
== 'requested-archiving' and form
.getType() == \
449 self
.archiving_accepted(form
)
450 except exceptions
.NegotiationError
, details
:
451 self
.fail_bad_negotiation(details
)
456 elif self
.status
== 'requested-e2e' and form
.getType() == 'submit':
457 negotiated
, not_acceptable
, ask_user
= self
.verify_options_alice(
461 def accept_nondefault_options(is_checked
):
464 negotiated
.update(ask_user
)
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()
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
)
483 self
.accept_e2e_alice(form
, negotiated
)
484 except exceptions
.NegotiationError
, details
:
485 self
.fail_bad_negotiation(details
)
488 elif self
.status
== 'responded-archiving' and form
.getType() == \
491 self
.we_accept_archiving(form
)
492 except exceptions
.NegotiationError
, details
:
493 self
.fail_bad_negotiation(details
)
496 elif self
.status
== 'responded-e2e' and form
.getType() == 'result':
498 self
.accept_e2e_bob(form
)
499 except exceptions
.NegotiationError
, details
:
500 self
.fail_bad_negotiation(details
)
503 elif self
.status
== 'identified-alice' and form
.getType() == 'result':
505 self
.final_steps_alice(form
)
506 except exceptions
.NegotiationError
, details
:
507 self
.fail_bad_negotiation(details
)
510 except exceptions
.Cancelled
:
511 # user cancelled the negotiation
513 self
.reject_negotiation()
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
)
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':
529 jid
, resource
= gajim
.get_room_and_nick_from_fjid(self
.jid
)
531 account
= self
.conn
.name
532 contact
= gajim
.contacts
.get_contact(account
, self
.jid
, resource
)
535 contact
= gajim
.contacts
.create_contact(jid
=jid
, account
=account
,
536 resource
=resource
, show
=self
.conn
.get_status())
538 gajim
.interface
.new_chat(contact
, account
, resource
=resource
,
541 negotiation
.FeatureNegotiationWindow(account
, self
.jid
, self
, form
)