presence.py 14.4 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
# 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/>.

# Presence handler

Philipp Hörist's avatar
Philipp Hörist committed
17
import time
18

19
import nbxmpp
Philipp Hörist's avatar
Philipp Hörist committed
20 21
from nbxmpp.structs import StanzaHandler
from nbxmpp.const import PresenceType
22

23
from gajim.common import app
24
from gajim.common.i18n import _
25
from gajim.common.nec import NetworkEvent
Philipp Hörist's avatar
Philipp Hörist committed
26
from gajim.common.const import KindConstant
27
from gajim.common.const import ShowConstant
Philipp Hörist's avatar
Philipp Hörist committed
28
from gajim.common.modules.base import BaseModule
29 30


Philipp Hörist's avatar
Philipp Hörist committed
31
class Presence(BaseModule):
32
    def __init__(self, con):
Philipp Hörist's avatar
Philipp Hörist committed
33
        BaseModule.__init__(self, con)
34

35
        self.handlers = [
Philipp Hörist's avatar
Philipp Hörist committed
36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54
            StanzaHandler(name='presence',
                          callback=self._presence_received,
                          priority=50),
            StanzaHandler(name='presence',
                          callback=self._subscribe_received,
                          typ='subscribe',
                          priority=49),
            StanzaHandler(name='presence',
                          callback=self._subscribed_received,
                          typ='subscribed',
                          priority=49),
            StanzaHandler(name='presence',
                          callback=self._unsubscribe_received,
                          typ='unsubscribe',
                          priority=49),
            StanzaHandler(name='presence',
                          callback=self._unsubscribed_received,
                          typ='unsubscribed',
                          priority=49),
55 56 57 58 59 60 61
        ]

        # keep the jids we auto added (transports contacts) to not send the
        # SUBSCRIBED event to GUI
        self.automatically_added = []

        # list of jid to auto-authorize
62
        self._jids_for_auto_auth = set()
63

Philipp Hörist's avatar
Philipp Hörist committed
64
    def _presence_received(self, _con, stanza, properties):
Philipp Hörist's avatar
Philipp Hörist committed
65
        if properties.from_muc:
66 67 68 69 70 71 72
            # MUC occupant presences are already handled in MUC module
            return

        muc = self._con.get_module('MUC').get_manager().get(properties.jid)
        if muc is not None:
            # Presence from the MUC itself, used for MUC avatar
            # handled in VCardAvatars module
Philipp Hörist's avatar
Philipp Hörist committed
73 74
            return

Philipp Hörist's avatar
Philipp Hörist committed
75
        self._log.info('Received from %s', properties.jid)
Philipp Hörist's avatar
Philipp Hörist committed
76 77

        if properties.type == PresenceType.ERROR:
Philipp Hörist's avatar
Philipp Hörist committed
78
            self._log.info('Error: %s %s', properties.jid, properties.error)
Philipp Hörist's avatar
Philipp Hörist committed
79
            return
Philipp Hörist's avatar
Philipp Hörist committed
80 81 82 83 84 85

        if self._account == 'Local':
            app.nec.push_incoming_event(
                NetworkEvent('raw-pres-received',
                             conn=self._con,
                             stanza=stanza))
Philipp Hörist's avatar
Philipp Hörist committed
86
            return
Philipp Hörist's avatar
Philipp Hörist committed
87 88 89 90 91 92

        if properties.is_self_presence:
            app.nec.push_incoming_event(
                NetworkEvent('our-show',
                             conn=self._con,
                             show=properties.show.value))
Philipp Hörist's avatar
Philipp Hörist committed
93
            return
Philipp Hörist's avatar
Philipp Hörist committed
94

95 96 97
        jid = properties.jid.getBare()
        roster_item = self._con.get_module('Roster').get_item(jid)
        if not properties.is_self_bare and roster_item is None:
Philipp Hörist's avatar
Philipp Hörist committed
98
            # Handle only presence from roster contacts
Philipp Hörist's avatar
Philipp Hörist committed
99 100
            self._log.warning('Unknown presence received')
            self._log.warning(stanza)
Philipp Hörist's avatar
Philipp Hörist committed
101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137
            return

        show = properties.show.value
        if properties.type.is_unavailable:
            show = 'offline'

        event_attrs = {
            'conn': self._con,
            'stanza': stanza,
            'prio': properties.priority,
            'need_add_in_roster': False,
            'popup': False,
            'ptype': properties.type.value,
            'jid': properties.jid.getBare(),
            'resource': properties.jid.getResource(),
            'id_': properties.id,
            'fjid': str(properties.jid),
            'timestamp': properties.timestamp,
            'avatar_sha': properties.avatar_sha,
            'user_nick': properties.nickname,
            'idle_time': properties.idle_timestamp,
            'show': show,
            'new_show': show,
            'old_show': 0,
            'status': properties.status,
            'contact_list': [],
            'contact': None,
        }

        event_ = NetworkEvent('presence-received', **event_attrs)

        # TODO: Refactor
        self._update_contact(event_, properties)

        app.nec.push_incoming_event(event_)

    def _update_contact(self, event, properties):
Philipp Hörist's avatar
Philipp Hörist committed
138
        # Note: A similar method also exists in connection_zeroconf
Philipp Hörist's avatar
Philipp Hörist committed
139 140 141 142
        jid = properties.jid.getBare()
        resource = properties.jid.getResource()

        status_strings = ['offline', 'error', 'online', 'chat', 'away',
Philipp Hörist's avatar
Philipp Hörist committed
143
                          'xa', 'dnd']
Philipp Hörist's avatar
Philipp Hörist committed
144 145 146 147 148 149

        event.new_show = status_strings.index(event.show)

        # Update contact
        contact_list = app.contacts.get_contacts(self._account, jid)
        if not contact_list:
Philipp Hörist's avatar
Philipp Hörist committed
150
            self._log.warning('No contact found')
151 152
            return

Philipp Hörist's avatar
Philipp Hörist committed
153
        event.contact_list = contact_list
Philipp Hörist's avatar
Philipp Hörist committed
154

Philipp Hörist's avatar
Philipp Hörist committed
155 156 157 158
        contact = app.contacts.get_contact_strict(self._account,
                                                  properties.jid.getBare(),
                                                  properties.jid.getResource())
        if contact is None:
159 160
            contact = app.contacts.get_first_contact_from_jid(self._account,
                                                              jid)
Philipp Hörist's avatar
Philipp Hörist committed
161
            if contact is None:
Philipp Hörist's avatar
Philipp Hörist committed
162
                self._log.warning('First contact not found')
Philipp Hörist's avatar
Philipp Hörist committed
163 164
                return

165 166
            if (self._is_resource_known(contact_list) and
                    not app.jid_is_transport(jid)):
Philipp Hörist's avatar
Philipp Hörist committed
167 168 169 170 171 172 173 174 175
                # Another resource of an existing contact connected
                # Add new contact
                event.old_show = 0
                contact = app.contacts.copy_contact(contact)
                contact.resource = resource
                app.contacts.add_contact(self._account, contact)
            else:
                # Convert the inital roster contact to a contact with resource
                contact.resource = resource
Philipp Hörist's avatar
Philipp Hörist committed
176 177 178
                event.old_show = 0
                if contact.show in status_strings:
                    event.old_show = status_strings.index(contact.show)
Philipp Hörist's avatar
Philipp Hörist committed
179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200

            event.need_add_in_roster = True

        elif contact.show in status_strings:
            event.old_show = status_strings.index(contact.show)

        # Update contact with presence data
        contact.show = event.show
        contact.status = properties.status
        contact.priority = properties.priority
        contact.idle_time = properties.idle_timestamp

        event.contact = contact

        if not app.jid_is_transport(jid) and len(contact_list) == 1:
            # It's not an agent
            if event.old_show == 0 and event.new_show > 1:
                if not jid in app.newly_added[self._account]:
                    app.newly_added[self._account].append(jid)
                if jid in app.to_be_removed[self._account]:
                    app.to_be_removed[self._account].remove(jid)
            elif event.old_show > 1 and event.new_show == 0 and \
201
            self._con.state.is_available:
Philipp Hörist's avatar
Philipp Hörist committed
202 203 204 205 206 207 208 209 210 211 212
                if not jid in app.to_be_removed[self._account]:
                    app.to_be_removed[self._account].append(jid)
                if jid in app.newly_added[self._account]:
                    app.newly_added[self._account].remove(jid)

        if app.jid_is_transport(jid):
            return

        if properties.type.is_unavailable:
            # TODO: This causes problems when another
            # resource signs off!
213 214
            self._con.get_module('Bytestream').stop_all_active_file_transfers(
                contact)
Philipp Hörist's avatar
Philipp Hörist committed
215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231
        self._log_presence(properties)

    @staticmethod
    def _is_resource_known(contact_list):
        if len(contact_list) > 1:
            return True

        if contact_list[0].resource == '':
            return False
        return contact_list[0].show not in ('not in roster', 'offline')

    def _log_presence(self, properties):
        if not app.config.get('log_contact_status_changes'):
            return
        if not app.config.should_log(self._account, properties.jid.getBare()):
            return

232
        show = ShowConstant[properties.show.name]
Philipp Hörist's avatar
Philipp Hörist committed
233
        if properties.type.is_unavailable:
234
            show = ShowConstant.OFFLINE
Philipp Hörist's avatar
Philipp Hörist committed
235 236 237 238 239 240 241 242 243 244 245

        app.logger.insert_into_logs(self._account,
                                    properties.jid.getBare(),
                                    time.time(),
                                    KindConstant.STATUS,
                                    message=properties.status,
                                    show=show)

    def _subscribe_received(self, _con, _stanza, properties):
        jid = properties.jid.getBare()
        fjid = str(properties.jid)
246

247 248 249
        is_transport = app.jid_is_transport(fjid)
        auto_auth = app.config.get_per('accounts', self._account, 'autoauth')

Philipp Hörist's avatar
Philipp Hörist committed
250 251 252 253
        self._log.info('Received Subscribe: %s, transport: %s, '
                       'auto_auth: %s, user_nick: %s',
                       properties.jid, is_transport,
                       auto_auth, properties.nickname)
Philipp Hörist's avatar
Philipp Hörist committed
254

255
        if auto_auth or jid in self._jids_for_auto_auth:
256
            self.send_presence(fjid, 'subscribed')
257
            self._jids_for_auto_auth.discard(jid)
258 259
            self._log.info('Auto respond with subscribed: %s', jid)
            return
260

Philipp Hörist's avatar
Philipp Hörist committed
261 262
        status = (properties.status or
                  _('I would like to add you to my roster.'))
263 264 265 266 267 268 269

        app.nec.push_incoming_event(NetworkEvent(
            'subscribe-presence-received',
            conn=self._con,
            jid=jid,
            fjid=fjid,
            status=status,
Philipp Hörist's avatar
Philipp Hörist committed
270
            user_nick=properties.nickname,
271 272 273 274
            is_transport=is_transport))

        raise nbxmpp.NodeProcessed

Philipp Hörist's avatar
Philipp Hörist committed
275 276
    def _subscribed_received(self, _con, _stanza, properties):
        jid = properties.jid.getBare()
Philipp Hörist's avatar
Philipp Hörist committed
277
        self._log.info('Received Subscribed: %s', properties.jid)
278 279 280 281 282 283
        if jid in self.automatically_added:
            self.automatically_added.remove(jid)
            raise nbxmpp.NodeProcessed

        app.nec.push_incoming_event(NetworkEvent(
            'subscribed-presence-received',
284 285
            account=self._account,
            jid=properties.jid))
286 287
        raise nbxmpp.NodeProcessed

Philipp Hörist's avatar
Philipp Hörist committed
288 289
    def _unsubscribe_received(self, _con, _stanza, properties):
        self._log.info('Received Unsubscribe: %s', properties.jid)
290 291
        raise nbxmpp.NodeProcessed

Philipp Hörist's avatar
Philipp Hörist committed
292
    def _unsubscribed_received(self, _con, _stanza, properties):
Philipp Hörist's avatar
Philipp Hörist committed
293
        self._log.info('Received Unsubscribed: %s', properties.jid)
294 295
        app.nec.push_incoming_event(NetworkEvent(
            'unsubscribed-presence-received',
Philipp Hörist's avatar
Philipp Hörist committed
296
            conn=self._con, jid=properties.jid.getBare()))
297 298 299
        raise nbxmpp.NodeProcessed

    def subscribed(self, jid):
300
        if not app.account_is_available(self._account):
301
            return
Philipp Hörist's avatar
Philipp Hörist committed
302
        self._log.info('Subscribed: %s', jid)
303
        self.send_presence(jid, 'subscribed')
304 305

    def unsubscribed(self, jid):
306
        if not app.account_is_available(self._account):
307
            return
308

Philipp Hörist's avatar
Philipp Hörist committed
309
        self._log.info('Unsubscribed: %s', jid)
310
        self._jids_for_auto_auth.discard(jid)
311
        self.send_presence(jid, 'unsubscribed')
312 313

    def unsubscribe(self, jid, remove_auth=True):
314
        if not app.account_is_available(self._account):
315 316
            return
        if remove_auth:
317
            self._con.get_module('Roster').del_item(jid)
318 319 320 321 322
            jid_list = app.config.get_per('contacts')
            for j in jid_list:
                if j.startswith(jid):
                    app.config.del_per('contacts', j)
        else:
Philipp Hörist's avatar
Philipp Hörist committed
323
            self._log.info('Unsubscribe from %s', jid)
324
            self._jids_for_auto_auth.discard(jid)
325 326
            self._con.get_module('Roster').unsubscribe(jid)
            self._con.get_module('Roster').set_item(jid)
327

328
    def subscribe(self, jid, msg=None, name='', groups=None, auto_auth=False):
329
        if not app.account_is_available(self._account):
330 331 332 333
            return
        if groups is None:
            groups = []

Philipp Hörist's avatar
Philipp Hörist committed
334
        self._log.info('Request Subscription to %s', jid)
335 336

        if auto_auth:
Philipp Hörist's avatar
Philipp Hörist committed
337
            self._jids_for_auto_auth.add(jid)
338 339 340 341 342 343 344 345 346 347 348

        infos = {'jid': jid}
        if name:
            infos['name'] = name
        iq = nbxmpp.Iq('set', nbxmpp.NS_ROSTER)
        query = iq.setQuery()
        item = query.addChild('item', attrs=infos)
        for group in groups:
            item.addChild('group').setData(group)
        self._con.connection.send(iq)

349 350 351 352
        self.send_presence(jid,
                           'subscribe',
                           status=msg,
                           nick=app.nicks[self._account])
353

354 355
    def get_presence(self, to=None, typ=None, priority=None,
                     show=None, status=None, nick=None, caps=True,
Philipp Hörist's avatar
Philipp Hörist committed
356
                     idle_time=None):
357 358 359 360
        if show not in ('chat', 'away', 'xa', 'dnd'):
            # Gajim sometimes passes invalid show values here
            # until this is fixed this is a workaround
            show = None
361 362 363 364 365 366 367 368 369
        presence = nbxmpp.Presence(to, typ, priority, show, status)
        if nick is not None:
            nick_tag = presence.setTag('nick', namespace=nbxmpp.NS_NICK)
            nick_tag.setData(nick)

        if idle_time is not None:
            idle_node = presence.setTag('idle', namespace=nbxmpp.NS_IDLE)
            idle_node.setAttr('since', idle_time)

Philipp Hörist's avatar
Philipp Hörist committed
370 371 372 373
        if not self._con.avatar_conversion:
            # XEP-0398 not supported by server so
            # we add the avatar sha to our presence
            self._con.get_module('VCardAvatars').add_update_node(presence)
374

375 376 377 378 379
        caps = self._con.get_module('Caps').caps
        if caps is not None and typ != 'unavailable':
            presence.setTag('c',
                            namespace=nbxmpp.NS_CAPS,
                            attrs=caps._asdict())
380 381 382

        return presence

383 384 385 386
    def send_presence(self, *args, **kwargs):
        if not app.account_is_connected(self._account):
            return
        presence = self.get_presence(*args, **kwargs)
387 388
        app.plugin_manager.extension_point(
            'send-presence', self._account, presence)
Philipp Hörist's avatar
Philipp Hörist committed
389
        self._log.debug('Send presence:\n%s', presence)
390 391
        self._con.connection.send(presence)

392 393 394

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