message.py 13.9 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
# This file is part of Gajim.
#
# Gajim is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published
# by the Free Software Foundation; version 3 only.
#
# Gajim is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Gajim. If not, see <http://www.gnu.org/licenses/>.

# Message handler

import time

import nbxmpp
Philipp Hörist's avatar
Philipp Hörist committed
20
from nbxmpp.namespaces import Namespace
Philipp Hörist's avatar
Philipp Hörist committed
21
from nbxmpp.structs import StanzaHandler
Philipp Hörist's avatar
Philipp Hörist committed
22
from nbxmpp.util import generate_id
23 24

from gajim.common import app
25 26
from gajim.common.nec import NetworkEvent
from gajim.common.helpers import AdditionalDataDict
27
from gajim.common.const import KindConstant
Philipp Hörist's avatar
Philipp Hörist committed
28
from gajim.common.modules.base import BaseModule
Philipp Hörist's avatar
Philipp Hörist committed
29
from gajim.common.modules.util import get_eme_message
30
from gajim.common.modules.security_labels import parse_securitylabel
31 32 33 34 35
from gajim.common.modules.misc import parse_correction
from gajim.common.modules.misc import parse_oob
from gajim.common.modules.misc import parse_xhtml


Philipp Hörist's avatar
Philipp Hörist committed
36
class Message(BaseModule):
37
    def __init__(self, con):
Philipp Hörist's avatar
Philipp Hörist committed
38
        BaseModule.__init__(self, con)
39

Philipp Hörist's avatar
Philipp Hörist committed
40
        self.handlers = [
41 42 43
            StanzaHandler(name='message',
                          callback=self._check_if_unknown_contact,
                          priority=41),
Philipp Hörist's avatar
Philipp Hörist committed
44 45 46
            StanzaHandler(name='message',
                          callback=self._message_received,
                          priority=50),
47 48 49 50
            StanzaHandler(name='message',
                          typ='error',
                          callback=self._message_error_received,
                          priority=50),
Philipp Hörist's avatar
Philipp Hörist committed
51
        ]
52 53

        # XEPs for which this message module should not be executed
Philipp Hörist's avatar
Philipp Hörist committed
54 55
        self._message_namespaces = set([Namespace.ROSTERX,
                                        Namespace.IBB])
56

57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78
    def _check_if_unknown_contact(self, _con, stanza, properties):
        if (properties.type.is_groupchat or
                properties.is_muc_pm or
                properties.is_self_message or
                properties.is_mam_message):
            return

        if self._con.get_own_jid().getDomain() == str(properties.jid):
            # Server message
            return

        if not app.config.get_per('accounts',
                                  self._account,
                                  'ignore_unknown_contacts'):
            return

        jid = properties.jid.getBare()
        if self._con.get_module('Roster').get_item(jid) is None:
            self._log.warning('Ignore message from unknown contact: %s', jid)
            self._log.warning(stanza)
            raise nbxmpp.NodeProcessed

Philipp Hörist's avatar
Philipp Hörist committed
79
    def _message_received(self, _con, stanza, properties):
80 81 82
        if (properties.is_mam_message or
                properties.is_pubsub or
                properties.type.is_error):
Philipp Hörist's avatar
Philipp Hörist committed
83
            return
84 85 86 87 88 89
        # Check if a child of the message contains any
        # namespaces that we handle in other modules.
        # nbxmpp executes less common handlers last
        if self._message_namespaces & set(stanza.getProperties()):
            return

Philipp Hörist's avatar
Philipp Hörist committed
90
        self._log.info('Received from %s', stanza.getFrom())
91 92 93 94 95 96 97

        app.nec.push_incoming_event(NetworkEvent(
            'raw-message-received',
            conn=self._con,
            stanza=stanza,
            account=self._account))

98
        if properties.is_carbon_message and properties.carbon.is_sent:
Philipp Hörist's avatar
Philipp Hörist committed
99 100 101
            # Ugly, we treat the from attr as the remote jid,
            # to make that work with sent carbons we have to do this.
            # TODO: Check where in Gajim and plugins we depend on that behavior
Philipp Hörist's avatar
Philipp Hörist committed
102
            stanza.setFrom(stanza.getTo())
103 104

        from_ = stanza.getFrom()
Philipp Hörist's avatar
Philipp Hörist committed
105 106 107
        fjid = str(from_)
        jid = from_.getBare()
        resource = from_.getResource()
108

Philipp Hörist's avatar
Philipp Hörist committed
109
        type_ = properties.type
110

Philipp Hörist's avatar
Philipp Hörist committed
111
        stanza_id, message_id = self._get_unique_id(properties)
112

113 114 115 116 117 118 119 120 121 122 123 124
        if properties.type.is_groupchat and properties.has_server_delay:
            # Only for XEP-0045 MUC History
            # Dont check for message text because the message could be encrypted
            if app.logger.deduplicate_muc_message(self._account,
                                                  properties.jid.getBare(),
                                                  properties.jid.getResource(),
                                                  properties.timestamp,
                                                  properties.id):
                raise nbxmpp.NodeProcessed

        if (properties.is_self_message or properties.is_muc_pm):
            archive_jid = self._con.get_own_jid().getStripped()
125 126
            if app.logger.find_stanza_id(self._account,
                                         archive_jid,
127
                                         stanza_id,
Philipp Hörist's avatar
Philipp Hörist committed
128 129
                                         message_id,
                                         properties.type.is_groupchat):
130 131
                return

Philipp Hörist's avatar
Philipp Hörist committed
132
        msgtxt = properties.body
133 134 135 136 137 138 139 140

        # TODO: remove all control UI stuff
        gc_control = app.interface.msg_win_mgr.get_gc_control(
            jid, self._account)
        if not gc_control:
            minimized = app.interface.minimized_controls[self._account]
            gc_control = minimized.get(jid)
        session = None
Philipp Hörist's avatar
Philipp Hörist committed
141 142
        if not properties.type.is_groupchat:
            if properties.is_muc_pm and properties.type.is_error:
143
                session = self._con.find_session(fjid, properties.thread)
144 145 146 147
                if not session:
                    session = self._con.get_latest_session(fjid)
                if not session:
                    session = self._con.make_new_session(
148
                        fjid, properties.thread, type_='pm')
149
            else:
150 151
                session = self._con.get_or_create_session(
                    fjid, properties.thread)
152

153
            if properties.thread and not session.received_thread_id:
154 155 156 157
                session.received_thread_id = True

            session.last_receive = time.time()

158 159 160 161 162 163
        additional_data = AdditionalDataDict()

        if properties.has_user_delay:
            additional_data.set_value(
                'gajim', 'user_timestamp', properties.user_timestamp)

Philipp Hörist's avatar
Philipp Hörist committed
164
        parse_oob(properties, additional_data)
Philipp Hörist's avatar
Philipp Hörist committed
165
        parse_xhtml(properties, additional_data)
Philipp Hörist's avatar
Philipp Hörist committed
166 167 168 169 170 171 172 173 174 175 176 177

        app.nec.push_incoming_event(NetworkEvent('update-client-info',
                                                 account=self._account,
                                                 jid=jid,
                                                 resource=resource))

        if properties.is_encrypted:
            additional_data['encrypted'] = properties.encrypted.additional_data
        else:
            if properties.eme is not None:
                msgtxt = get_eme_message(properties.eme)

178 179 180 181
        event_attr = {
            'conn': self._con,
            'stanza': stanza,
            'account': self._account,
182
            'additional_data': additional_data,
183 184 185
            'fjid': fjid,
            'jid': jid,
            'resource': resource,
186
            'stanza_id': stanza_id,
Philipp Hörist's avatar
Philipp Hörist committed
187
            'unique_id': stanza_id or message_id,
188
            'correct_id': parse_correction(properties),
189 190
            'msgtxt': msgtxt,
            'session': session,
191
            'delayed': properties.user_timestamp is not None,
Philipp Hörist's avatar
Philipp Hörist committed
192
            'gc_control': gc_control,
193 194
            'popup': False,
            'msg_log_id': None,
Philipp Hörist's avatar
Philipp Hörist committed
195
            'displaymarking': parse_securitylabel(stanza),
196
            'properties': properties,
197 198
        }

Philipp Hörist's avatar
Philipp Hörist committed
199 200
        if type_.is_groupchat:
            if not msgtxt:
201 202
                return

Philipp Hörist's avatar
Philipp Hörist committed
203 204 205 206 207
            event_attr.update({
                'room_jid': jid,
            })
            event = NetworkEvent('gc-message-received', **event_attr)
            app.nec.push_incoming_event(event)
208 209
            # TODO: Some plugins modify msgtxt in the GUI event
            self._log_muc_message(event)
210 211 212
            return

        app.nec.push_incoming_event(
Philipp Hörist's avatar
Philipp Hörist committed
213
            NetworkEvent('decrypted-message-received', **event_attr))
214

215
    def _message_error_received(self, _con, _stanza, properties):
Philipp Hörist's avatar
Philipp Hörist committed
216
        jid = properties.jid.copy()
217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233
        if not properties.is_muc_pm:
            jid.setBare()

        self._log.info(properties.error)

        app.logger.set_message_error(app.get_jid_from_account(self._account),
                                     jid,
                                     properties.id,
                                     properties.error)

        app.nec.push_incoming_event(
            NetworkEvent('message-error',
                         account=self._account,
                         jid=jid,
                         room_jid=jid,
                         message_id=properties.id,
                         error=properties.error))
234

235 236 237 238
    def _log_muc_message(self, event):
        self._check_for_mam_compliance(event.room_jid, event.stanza_id)

        if (app.config.should_log(self._account, event.jid) and
239
                event.msgtxt and event.properties.muc_nickname):
240 241 242
            # if not event.nick, it means message comes from room itself
            # usually it hold description and can be send at each connection
            # so don't store it in logs
243 244 245 246 247 248 249 250 251 252
            app.logger.insert_into_logs(
                self._account,
                event.jid,
                event.properties.timestamp,
                KindConstant.GC_MSG,
                message=event.msgtxt,
                contact_name=event.properties.muc_nickname,
                additional_data=event.additional_data,
                stanza_id=event.stanza_id,
                message_id=event.properties.id)
253

Philipp Hörist's avatar
Philipp Hörist committed
254
    def _check_for_mam_compliance(self, room_jid, stanza_id):
255
        disco_info = app.logger.get_last_disco_info(room_jid)
Philipp Hörist's avatar
Philipp Hörist committed
256
        if stanza_id is None and disco_info.mam_namespace == Namespace.MAM_2:
Philipp Hörist's avatar
Philipp Hörist committed
257
            self._log.warning('%s announces mam:2 without stanza-id', room_jid)
258

Philipp Hörist's avatar
Philipp Hörist committed
259 260 261 262
    def _get_unique_id(self, properties):
        if properties.is_self_message:
            # Deduplicate self message with message-id
            return None, properties.id
263

Philipp Hörist's avatar
Philipp Hörist committed
264
        if properties.stanza_id is None:
265
            return None, None
266

Philipp Hörist's avatar
Philipp Hörist committed
267
        if properties.type.is_groupchat:
268
            disco_info = app.logger.get_last_disco_info(
Philipp Hörist's avatar
Philipp Hörist committed
269
                properties.jid.getBare())
Philipp Hörist's avatar
Philipp Hörist committed
270

Philipp Hörist's avatar
Philipp Hörist committed
271
            if disco_info.mam_namespace != Namespace.MAM_2:
Philipp Hörist's avatar
Philipp Hörist committed
272 273
                return None, None

Philipp Hörist's avatar
Philipp Hörist committed
274 275
            archive = properties.jid
        else:
Philipp Hörist's avatar
Philipp Hörist committed
276 277
            if not self._con.get_module('MAM').available:
                return None, None
Philipp Hörist's avatar
Philipp Hörist committed
278

Philipp Hörist's avatar
Philipp Hörist committed
279
            archive = self._con.get_own_jid()
280

Philipp Hörist's avatar
Philipp Hörist committed
281 282
        if archive.bareMatch(properties.stanza_id.by):
            return properties.stanza_id.id, None
283 284
        # stanza-id not added by the archive, ignore it.
        return None, None
285

Philipp Hörist's avatar
Philipp Hörist committed
286 287 288 289 290 291 292 293 294 295 296
    def build_message_stanza(self, message):
        own_jid = self._con.get_own_jid()

        stanza = nbxmpp.Message(to=message.jid,
                                body=message.message,
                                typ=message.type_,
                                subject=message.subject,
                                xhtml=message.xhtml)

        if message.correct_id:
            stanza.setTag('replace', attrs={'id': message.correct_id},
Philipp Hörist's avatar
Philipp Hörist committed
297
                          namespace=Namespace.CORRECT)
Philipp Hörist's avatar
Philipp Hörist committed
298 299 300 301 302 303 304 305 306 307 308

        # XEP-0359
        message.message_id = generate_id()
        stanza.setID(message.message_id)
        stanza.setOriginID(message.message_id)

        if message.label:
            stanza.addChild(node=message.label)

        # XEP-0172: user_nickname
        if message.user_nick:
Philipp Hörist's avatar
Philipp Hörist committed
309
            stanza.setTag('nick', namespace=Namespace.NICK).setData(
Philipp Hörist's avatar
Philipp Hörist committed
310 311 312 313 314 315 316 317
                message.user_nick)

        # XEP-0203
        # TODO: Seems delayed is not set anywhere
        if message.delayed:
            timestamp = time.strftime('%Y-%m-%dT%H:%M:%SZ',
                                      time.gmtime(message.delayed))
            stanza.addChild('delay',
Philipp Hörist's avatar
Philipp Hörist committed
318
                            namespace=Namespace.DELAY2,
Philipp Hörist's avatar
Philipp Hörist committed
319 320 321 322
                            attrs={'from': str(own_jid), 'stamp': timestamp})

        # XEP-0224
        if message.attention:
Philipp Hörist's avatar
Philipp Hörist committed
323
            stanza.setTag('attention', namespace=Namespace.ATTENTION)
Philipp Hörist's avatar
Philipp Hörist committed
324 325 326

        # XEP-0066
        if message.oob_url is not None:
Philipp Hörist's avatar
Philipp Hörist committed
327
            oob = stanza.addChild('x', namespace=Namespace.X_OOB)
Philipp Hörist's avatar
Philipp Hörist committed
328 329 330 331 332 333 334 335
            oob.addChild('url').setData(message.oob_url)

        # XEP-0184
        if not own_jid.bareMatch(message.jid):
            if message.message and not message.is_groupchat:
                stanza.setReceiptRequest()

        # Mark Message as MUC PM
336
        if message.contact.is_pm_contact:
Philipp Hörist's avatar
Philipp Hörist committed
337
            stanza.setTag('x', namespace=Namespace.MUC_USER)
Philipp Hörist's avatar
Philipp Hörist committed
338 339 340

        # XEP-0085
        if message.chatstate is not None:
Philipp Hörist's avatar
Philipp Hörist committed
341
            stanza.setTag(message.chatstate, namespace=Namespace.CHATSTATES)
Philipp Hörist's avatar
Philipp Hörist committed
342 343
            if not message.message:
                stanza.setTag('no-store',
Philipp Hörist's avatar
Philipp Hörist committed
344
                              namespace=Namespace.MSG_HINTS)
Philipp Hörist's avatar
Philipp Hörist committed
345

346 347 348 349 350
        # Add other nodes
        if message.nodes is not None:
            for node in message.nodes:
                stanza.addChild(node=node)

Philipp Hörist's avatar
Philipp Hörist committed
351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372
        return stanza

    def log_message(self, message):
        if not message.is_loggable:
            return

        if not app.config.should_log(self._account, message.jid):
            return

        if message.message is None:
            return

        app.logger.insert_into_logs(self._account,
                                    message.jid,
                                    message.timestamp,
                                    message.kind,
                                    message=message.message,
                                    subject=message.subject,
                                    additional_data=message.additional_data,
                                    message_id=message.message_id,
                                    stanza_id=message.message_id)

373 374 375

def get_instance(*args, **kwargs):
    return Message(*args, **kwargs), 'Message'