root/trunk/src/session.py

Revision 10687, 15.2 kB (checked in by asterix, 8 days ago)

don't consider buggy messages as groupchat messages if there is no nickname. Fixes #3700

Line 
1# -*- coding:utf-8 -*-
2## src/session.py
3##
4## Copyright (C) 2008 Yann Leboulanger <asterix AT lagaule.org>
5##                    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.
10##
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.
14##
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.
19##
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/>.
22##
23
24from common import helpers
25
26from common import exceptions
27from common import gajim
28from common import stanza_session
29from common import contacts
30
31import common.xmpp
32
33import message_control
34
35import notify
36
37import dialogs
38import negotiation
39
40class ChatControlSession(stanza_session.EncryptedStanzaSession):
41        def __init__(self, conn, jid, thread_id, type_='chat'):
42                stanza_session.EncryptedStanzaSession.__init__(self, conn, jid, thread_id,
43                        type_='chat')
44
45                self.control = None
46
47        def detach_from_control(self):
48                if self.control:
49                        self.control.no_autonegotiation = False
50                        self.control.set_session(None)
51
52        def acknowledge_termination(self):
53                self.detach_from_control()
54                stanza_session.EncryptedStanzaSession.acknowledge_termination(self)
55
56        def terminate(self):
57                stanza_session.EncryptedStanzaSession.terminate(self)
58                self.detach_from_control()
59
60        def get_chatstate(self, msg, msgtxt):
61                '''extracts chatstate from a <message/> stanza'''
62                composing_xep = None
63                chatstate = None
64
65                # chatstates - look for chatstate tags in a message if not delayed
66                delayed = msg.getTag('x', namespace=common.xmpp.NS_DELAY) is not None
67                if not delayed:
68                        composing_xep = False
69                        children = msg.getChildren()
70                        for child in children:
71                                if child.getNamespace() == 'http://jabber.org/protocol/chatstates':
72                                        chatstate = child.getName()
73                                        composing_xep = 'XEP-0085'
74                                        break
75                        # No XEP-0085 support, fallback to XEP-0022
76                        if not chatstate:
77                                chatstate_child = msg.getTag('x', namespace=common.xmpp.NS_EVENT)
78                                if chatstate_child:
79                                        chatstate = 'active'
80                                        composing_xep = 'XEP-0022'
81                                        if not msgtxt and chatstate_child.getTag('composing'):
82                                                chatstate = 'composing'
83
84                return (composing_xep, chatstate)
85
86        def received(self, full_jid_with_resource, msgtxt, tim, encrypted, msg):
87                '''dispatch a received <message> stanza'''
88                msg_type = msg.getType()
89                subject = msg.getSubject()
90
91                if not msg_type or msg_type not in ('chat', 'groupchat', 'error'):
92                        msg_type = 'normal'
93
94                msg_id = None
95
96                # XEP-0172 User Nickname
97                user_nick = msg.getTagData('nick')
98                if not user_nick:
99                        user_nick = ''
100
101                form_node = None
102                for xtag in msg.getTags('x'):
103                        if xtag.getNamespace() == common.xmpp.NS_DATA:
104                                form_node = xtag
105                                break
106
107                composing_xep, chatstate = self.get_chatstate(msg, msgtxt)
108
109                xhtml = msg.getXHTML()
110
111                if msg_type == 'chat':
112                        if not msg.getTag('body') and chatstate is None:
113                                return
114
115                        log_type = 'chat_msg_recv'
116                else:
117                        log_type = 'single_msg_recv'
118
119                if self.is_loggable() and msgtxt:
120                        try:
121                                msg_id = gajim.logger.write(log_type, full_jid_with_resource,
122                                        msgtxt, tim=tim, subject=subject)
123                        except exceptions.PysqliteOperationalError, e:
124                                self.conn.dispatch('ERROR', (_('Disk WriteError'), str(e)))
125
126                treat_as = gajim.config.get('treat_incoming_messages')
127                if treat_as:
128                        msg_type = treat_as
129
130                jid = gajim.get_jid_without_resource(full_jid_with_resource)
131                resource = gajim.get_resource_from_jid(full_jid_with_resource)
132
133                if gajim.config.get('ignore_incoming_xhtml'):
134                        xhtml = None
135                if gajim.jid_is_transport(jid):
136                        jid = jid.replace('@', '')
137
138                groupchat_control = gajim.interface.msg_win_mgr.get_gc_control(jid,
139                        self.conn.name)
140
141                if not groupchat_control and \
142                jid in gajim.interface.minimized_controls[self.conn.name]:
143                        groupchat_control = gajim.interface.minimized_controls[self.conn.name]\
144                                [jid]
145
146                pm = False
147                if groupchat_control and groupchat_control.type_id == \
148                message_control.TYPE_GC and resource:
149                        # It's a Private message
150                        pm = True
151                        msg_type = 'pm'
152
153                highest_contact = gajim.contacts.get_contact_with_highest_priority(
154                        self.conn.name, jid)
155
156                # does this resource have the highest priority of any available?
157                is_highest = not highest_contact or not highest_contact.resource or \
158                        resource == highest_contact.resource or highest_contact.show == \
159                                'offline'
160
161                # Handle chat states
162                contact = gajim.contacts.get_contact(self.conn.name, jid, resource)
163                if contact:
164                        if contact.composing_xep != 'XEP-0085': # We cache xep85 support
165                                contact.composing_xep = composing_xep
166                        if self.control and self.control.type_id == message_control.TYPE_CHAT:
167                                if chatstate is not None:
168                                        # other peer sent us reply, so he supports jep85 or jep22
169                                        contact.chatstate = chatstate
170                                        if contact.our_chatstate == 'ask': # we were jep85 disco?
171                                                contact.our_chatstate = 'active' # no more
172                                        self.control.handle_incoming_chatstate()
173                                elif contact.chatstate != 'active':
174                                        # got no valid jep85 answer, peer does not support it
175                                        contact.chatstate = False
176                        elif chatstate == 'active':
177                                # Brand new message, incoming.
178                                contact.our_chatstate = chatstate
179                                contact.chatstate = chatstate
180                                if msg_id: # Do not overwrite an existing msg_id with None
181                                        contact.msg_id = msg_id
182
183                # THIS MUST BE AFTER chatstates handling
184                # AND BEFORE playsound (else we ear sounding on chatstates!)
185                if not msgtxt: # empty message text
186                        return
187
188                if gajim.config.get('ignore_unknown_contacts') and \
189                not gajim.contacts.get_contacts(self.conn.name, jid) and not pm:
190                        return
191
192                if not contact:
193                        # contact is not in the roster, create a fake one to display
194                        # notification
195                        contact = contacts.Contact(jid=jid, resource=resource)
196
197                advanced_notif_num = notify.get_advanced_notification('message_received',
198                        self.conn.name, contact)
199
200                if not pm and is_highest:
201                        jid_of_control = jid
202                else:
203                        jid_of_control = full_jid_with_resource
204
205                if not self.control:
206                        ctrl = gajim.interface.msg_win_mgr.get_control(jid_of_control,
207                                self.conn.name)
208                        if ctrl:
209                                self.control = ctrl
210                                self.control.set_session(self)
211
212                # Is it a first or next message received ?
213                first = False
214                if not self.control and not gajim.events.get_events(self.conn.name, \
215                jid_of_control, [msg_type]):
216                        first = True
217
218                if pm:
219                        nickname = resource
220                        if self.control:
221                                # print if a control is open
222                                self.control.print_conversation(msgtxt, tim=tim, xhtml=xhtml,
223                                        encrypted=encrypted)
224                        else:
225                                # otherwise pass it off to the control to be queued
226                                groupchat_control.on_private_message(nickname, msgtxt, tim,
227                                        xhtml, self, msg_id=msg_id, encrypted=encrypted)
228                else:
229                        self.roster_message(jid, msgtxt, tim, encrypted, msg_type,
230                                subject, resource, msg_id, user_nick, advanced_notif_num,
231                                xhtml=xhtml, form_node=form_node)
232
233                        nickname = gajim.get_name_from_jid(self.conn.name, jid)
234
235                # Check and do wanted notifications
236                msg = msgtxt
237                if subject:
238                        msg = _('Subject: %s') % subject + '\n' + msg
239                focused = False
240
241                if self.control:
242                        parent_win = self.control.parent_win
243                        if self.control == parent_win.get_active_control() and \
244                        parent_win.window.has_focus:
245                                focused = True
246
247                notify.notify('new_message', jid_of_control, self.conn.name, [msg_type,
248                        first, nickname, msg, focused], advanced_notif_num)
249
250                if gajim.interface.remote_ctrl:
251                        gajim.interface.remote_ctrl.raise_signal('NewMessage', (self.conn.name,
252                                [full_jid_with_resource, msgtxt, tim, encrypted, msg_type, subject,
253                                chatstate, msg_id, composing_xep, user_nick, xhtml, form_node]))
254
255        def roster_message(self, jid, msg, tim, encrypted=False, msg_type='',
256        subject=None, resource='', msg_id=None, user_nick='',
257        advanced_notif_num=None, xhtml=None, form_node=None):
258                '''display the message or show notification in the roster'''
259                contact = None
260                # if chat window will be for specific resource
261                resource_for_chat = resource
262
263                fjid = jid
264
265                # Try to catch the contact with correct resource
266                if resource:
267                        fjid = jid + '/' + resource
268                        contact = gajim.contacts.get_contact(self.conn.name, jid, resource)
269
270                highest_contact = gajim.contacts.get_contact_with_highest_priority(
271                        self.conn.name, jid)
272                if not contact:
273                        # If there is another resource, it may be a message from an invisible
274                        # resource
275                        lcontact = gajim.contacts.get_contacts(self.conn.name, jid)
276                        if (len(lcontact) > 1 or (lcontact and lcontact[0].resource and \
277                        lcontact[0].show != 'offline')) and jid.find('@') > 0:
278                                contact = gajim.contacts.copy_contact(highest_contact)
279                                contact.resource = resource
280                                if resource:
281                                        fjid = jid + '/' + resource
282                                contact.priority = 0
283                                contact.show = 'offline'
284                                contact.status = ''
285                                gajim.contacts.add_contact(self.conn.name, contact)
286
287                        else:
288                                # Default to highest prio
289                                fjid = jid
290                                resource_for_chat = None
291                                contact = highest_contact
292
293                if not contact:
294                        # contact is not in roster
295                        contact = gajim.interface.roster.add_to_not_in_the_roster(
296                                self.conn.name, jid, user_nick)
297
298                if not self.control:
299                        # if no control exists and message comes from highest prio, the new
300                        # control shouldn't have a resource
301                        if highest_contact and contact.resource == highest_contact.resource \
302                        and not jid == gajim.get_jid_from_account(self.conn.name):
303                                fjid = jid
304                                resource_for_chat = None
305
306                # Do we have a queue?
307                no_queue = len(gajim.events.get_events(self.conn.name, fjid)) == 0
308
309                popup = helpers.allow_popup_window(self.conn.name, advanced_notif_num)
310
311                if msg_type == 'normal' and popup: # it's single message to be autopopuped
312                        dialogs.SingleMessageWindow(self.conn.name, contact.jid,
313                                action='receive', from_whom=jid, subject=subject, message=msg,
314                                resource=resource, session=self, form_node=form_node)
315                        return
316
317                # We print if window is opened and it's not a single message
318                if self.control and msg_type != 'normal':
319                        typ = ''
320
321                        if msg_type == 'error':
322                                typ = 'status'
323
324                        self.control.print_conversation(msg, typ, tim=tim, encrypted=encrypted,
325                                subject=subject, xhtml=xhtml)
326
327                        if msg_id:
328                                gajim.logger.set_read_messages([msg_id])
329
330                        return
331
332                # We save it in a queue
333                type_ = 'chat'
334                event_type = 'message_received'
335
336                if msg_type == 'normal':
337                        type_ = 'normal'
338                        event_type = 'single_message_received'
339
340                show_in_roster = notify.get_show_in_roster(event_type, self.conn.name,
341                        contact, self)
342                show_in_systray = notify.get_show_in_systray(event_type, self.conn.name,
343                        contact)
344
345                event = gajim.events.create_event(type_, (msg, subject, msg_type, tim,
346                        encrypted, resource, msg_id, xhtml, self, form_node),
347                        show_in_roster=show_in_roster, show_in_systray=show_in_systray)
348
349                gajim.events.add_event(self.conn.name, fjid, event)
350
351                if popup:
352                        if not self.control:
353                                self.control = gajim.interface.new_chat(contact,
354                                        self.conn.name, resource=resource_for_chat, session=self)
355
356                                if len(gajim.events.get_events(self.conn.name, fjid)):
357                                        self.control.read_queue()
358                else:
359                        if no_queue: # We didn't have a queue: we change icons
360                                gajim.interface.roster.draw_contact(jid, self.conn.name)
361
362                        gajim.interface.roster.show_title() # we show the * or [n]
363                # Select contact row in roster.
364                gajim.interface.roster.select_contact(jid, self.conn.name)
365
366        # ---- ESessions stuff ---
367
368        def handle_negotiation(self, form):
369                if form.getField('accept') and not form['accept'] in ('1', 'true'):
370                        self.cancelled_negotiation()
371                        return
372
373                # encrypted session states. these are described in stanza_session.py
374
375                try:
376                        # bob responds
377                        if form.getType() == 'form' and 'security' in form.asDict():
378                                # we don't support 3-message negotiation as the responder
379                                if 'dhkeys' in form.asDict():
380                                        self.fail_bad_negotiation('3 message negotiation not supported '
381                                                'when responding', ('dhkeys',))
382                                        return
383
384                                negotiated, not_acceptable, ask_user = self.verify_options_bob(form)
385
386                                if ask_user:
387                                        def accept_nondefault_options(is_checked):
388                                                self.dialog.destroy()
389                                                negotiated.update(ask_user)
390                                                self.respond_e2e_bob(form, negotiated, not_acceptable)
391
392                                        def reject_nondefault_options():
393                                                self.dialog.destroy()
394                                                for key in ask_user.keys():
395                                                        not_acceptable.append(key)
396                                                self.respond_e2e_bob(form, negotiated, not_acceptable)
397
398                                        self.dialog = dialogs.YesNoDialog(_('Confirm these session '
399                                                'options'),
400                                                _('''The remote client wants to negotiate an session with these features:
401
402        %s
403
404        Are these options acceptable?''') % (negotiation.describe_features(
405                                                ask_user)),
406                                                on_response_yes=accept_nondefault_options,
407                                                on_response_no=reject_nondefault_options)
408                                else:
409                                        self.respond_e2e_bob(form, negotiated, not_acceptable)
410
411                                return
412
413                        # alice accepts
414                        elif self.status == 'requested-e2e' and form.getType() == 'submit':
415                                negotiated, not_acceptable, ask_user = self.verify_options_alice(
416                                        form)
417
418                                if ask_user:
419                                        def accept_nondefault_options(is_checked):
420                                                dialog.destroy()
421
422                                                negotiated.update(ask_user)
423
424                                                try:
425                                                        self.accept_e2e_alice(form, negotiated)
426                                                except exceptions.NegotiationError, details:
427                                                        self.fail_bad_negotiation(details)
428
429                                        def reject_nondefault_options():
430                                                self.reject_negotiation()
431                                                dialog.destroy()
432
433                                        dialog = dialogs.YesNoDialog(_('Confirm these session options'),
434                                                _('The remote client selected these options:\n\n%s\n\n'
435                                                'Continue with the session?') % (
436                                                negotiation.describe_features(ask_user)),
437                                                on_response_yes = accept_nondefault_options,
438                                                on_response_no = reject_nondefault_options)
439                                else:
440                                        try:
441                                                self.accept_e2e_alice(form, negotiated)
442                                        except exceptions.NegotiationError, details:
443                                                self.fail_bad_negotiation(details)
444
445                                return
446                        elif self.status == 'responded-e2e' and form.getType() == 'result':
447                                try:
448                                        self.accept_e2e_bob(form)
449                                except exceptions.NegotiationError, details:
450                                        self.fail_bad_negotiation(details)
451
452                                return
453                        elif self.status == 'identified-alice' and form.getType() == 'result':
454                                try:
455                                        self.final_steps_alice(form)
456                                except exceptions.NegotiationError, details:
457                                        self.fail_bad_negotiation(details)
458
459                                return
460                except exceptions.Cancelled:
461                        # user cancelled the negotiation
462
463                        self.reject_negotiation()
464
465                        return
466
467                if form.getField('terminate') and\
468                form.getField('terminate').getValue() in ('1', 'true'):
469                        self.acknowledge_termination()
470
471                        self.conn.delete_session(str(self.jid), self.thread_id)
472
473                        return
474
475                # non-esession negotiation. this isn't very useful, but i'm keeping it
476                # around to test my test suite.
477                if form.getType() == 'form':
478                        if not self.control:
479                                jid, resource = gajim.get_room_and_nick_from_fjid(self.jid)
480
481                                account = self.conn.name
482                                contact = gajim.contacts.get_contact(account, self.jid, resource)
483
484                                if not contact:
485                                        contact = gajim.contacts.create_contact(jid=jid,
486                                                resource=resource, show=self.conn.get_status())
487
488                                gajim.interface.new_chat(contact, account, resource=resource,
489                                        session=self)
490
491                        negotiation.FeatureNegotiationWindow(account, self.jid, self, form)
492
493# vim: se ts=3:
Note: See TracBrowser for help on using the browser.