1# This file is part of Gajim.
2#
3# Gajim is free software; you can redistribute it and/or modify
4# it under the terms of the GNU General Public License as published
5# by the Free Software Foundation; version 3 only.
6#
7# Gajim is distributed in the hope that it will be useful,
8# but WITHOUT ANY WARRANTY; without even the implied warranty of
9# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10# GNU General Public License for more details.
11#
12# You should have received a copy of the GNU General Public License
13# along with Gajim.  If not, see <http://www.gnu.org/licenses/>.
14
15import logging
16
17import nbxmpp
18from nbxmpp.client import Client as NBXMPPClient
19from nbxmpp.const import StreamError
20from nbxmpp.const import ConnectionType
21
22from gi.repository import GLib
23
24from gajim.common import passwords
25from gajim.common.nec import NetworkEvent
26
27from gajim.common import app
28from gajim.common import helpers
29from gajim.common import modules
30from gajim.common.const import ClientState
31from gajim.common.helpers import get_custom_host
32from gajim.common.helpers import get_user_proxy
33from gajim.common.helpers import warn_about_plain_connection
34from gajim.common.helpers import get_resource
35from gajim.common.helpers import get_idle_status_message
36from gajim.common.idle import Monitor
37from gajim.common.i18n import _
38
39from gajim.common.connection_handlers import ConnectionHandlers
40from gajim.common.connection_handlers_events import MessageSentEvent
41
42from gajim.gui.util import open_window
43
44
45log = logging.getLogger('gajim.client')
46
47
48class Client(ConnectionHandlers):
49    def __init__(self, account):
50        self._client = None
51        self._account = account
52        self.name = account
53        self._hostname = app.settings.get_account_setting(self._account,
54                                                          'hostname')
55        self._user = app.settings.get_account_setting(self._account, 'name')
56        self.password = None
57
58        self._priority = 0
59        self._connect_machine_calls = 0
60        self.addressing_supported = False
61
62        self.is_zeroconf = False
63        self.pep = {}
64        self.roster_supported = True
65
66        self._state = ClientState.DISCONNECTED
67        self._status_sync_on_resume = False
68        self._status = 'online'
69        self._status_message = ''
70        self._idle_status = 'online'
71        self._idle_status_enabled = True
72        self._idle_status_message = ''
73
74        self._reconnect = True
75        self._reconnect_timer_source = None
76        self._destroy_client = False
77        self._remove_account = False
78
79        self._destroyed = False
80
81        self.available_transports = {}
82
83        modules.register_modules(self)
84
85        self._create_client()
86
87        if Monitor.is_available():
88            self._idle_handler_id = Monitor.connect('state-changed',
89                                                    self._idle_state_changed)
90            self._screensaver_handler_id = app.app.connect(
91                'notify::screensaver-active', self._screensaver_state_changed)
92
93        ConnectionHandlers.__init__(self)
94
95    def _set_state(self, state):
96        log.info('State: %s', state)
97        self._state = state
98
99    @property
100    def state(self):
101        return self._state
102
103    @property
104    def account(self):
105        return self._account
106
107    @property
108    def status(self):
109        return self._status
110
111    @property
112    def status_message(self):
113        if self._idle_status_active():
114            return self._idle_status_message
115        return self._status_message
116
117    @property
118    def priority(self):
119        return self._priority
120
121    @property
122    def certificate(self):
123        return self._client.peer_certificate[0]
124
125    @property
126    def features(self):
127        return self._client.features
128
129    @property
130    def local_address(self):
131        address = self._client.local_address
132        if address is not None:
133            return address.to_string().split(':')[0]
134        return None
135
136    def set_remove_account(self, value):
137        # Used by the RemoveAccount Assistant to make the Client
138        # not react to any stream errors that happen while the
139        # account is removed by the server and the connection is killed
140        self._remove_account = value
141
142    def _create_client(self):
143        if self._destroyed:
144            # If we disable an account cleanup() is called and all
145            # modules are unregistered. Because disable_account() does not wait
146            # for the client to properly disconnect, handlers of the
147            # nbxmpp.Client() are emitted after we called cleanup().
148            # After nbxmpp.Client() disconnects and is destroyed we create a
149            # new instance with this method but modules.get_handlers() fails
150            # because modules are already unregistered.
151            # TODO: Make this nicer
152            return
153        log.info('Create new nbxmpp client')
154        self._client = NBXMPPClient(log_context=self._account)
155        self.connection = self._client
156        self._client.set_domain(self._hostname)
157        self._client.set_username(self._user)
158        self._client.set_resource(get_resource(self._account))
159
160        pass_saved = app.settings.get_account_setting(self._account, 'savepass')
161        if pass_saved:
162            # Request password from keyring only if the user chose to save
163            # his password
164            self.password = passwords.get_password(self._account)
165
166        self._client.set_password(self.password)
167        self._client.set_accepted_certificates(
168            app.cert_store.get_certificates())
169
170        self._client.subscribe('resume-failed', self._on_resume_failed)
171        self._client.subscribe('resume-successful', self._on_resume_successful)
172        self._client.subscribe('disconnected', self._on_disconnected)
173        self._client.subscribe('connection-failed', self._on_connection_failed)
174        self._client.subscribe('connected', self._on_connected)
175
176        self._client.subscribe('stanza-sent', self._on_stanza_sent)
177        self._client.subscribe('stanza-received', self._on_stanza_received)
178
179        for handler in modules.get_handlers(self):
180            self._client.register_handler(handler)
181
182    def _on_resume_failed(self, _client, _signal_name):
183        log.info('Resume failed')
184        app.nec.push_incoming_event(NetworkEvent(
185            'our-show', account=self._account, show='offline'))
186        self.get_module('Chatstate').enabled = False
187
188    def _on_resume_successful(self, _client, _signal_name):
189        self._set_state(ClientState.CONNECTED)
190        self._set_client_available()
191
192        if self._status_sync_on_resume:
193            self._status_sync_on_resume = False
194            self.update_presence()
195        else:
196            # Normally show is updated when we receive a presence reflection.
197            # On resume, if show has not changed while offline, we don’t send
198            # a new presence so we have to trigger the event here.
199            app.nec.push_incoming_event(
200                NetworkEvent('our-show',
201                             account=self._account,
202                             show=self._status))
203
204    def _set_client_available(self):
205        self._set_state(ClientState.AVAILABLE)
206        app.nec.push_incoming_event(NetworkEvent('account-connected',
207                                                 account=self._account))
208
209    def disconnect(self, gracefully, reconnect, destroy_client=False):
210        if self._state.is_disconnecting:
211            log.warning('Disconnect already in progress')
212            return
213
214        self._set_state(ClientState.DISCONNECTING)
215        self._reconnect = reconnect
216        self._destroy_client = destroy_client
217
218        log.info('Starting to disconnect %s', self._account)
219        self._client.disconnect(immediate=not gracefully)
220
221    def _on_disconnected(self, _client, _signal_name):
222        log.info('Disconnect %s', self._account)
223        self._set_state(ClientState.DISCONNECTED)
224
225        domain, error, text = self._client.get_error()
226
227        if self._remove_account:
228            # Account was removed via RemoveAccount Assistant.
229            self._reconnect = False
230
231        elif domain == StreamError.BAD_CERTIFICATE:
232            self._reconnect = False
233            self._destroy_client = True
234
235            cert, errors = self._client.peer_certificate
236
237            open_window('SSLErrorDialog',
238                        account=self._account,
239                        client=self,
240                        cert=cert,
241                        error=errors.pop())
242
243        elif domain in (StreamError.STREAM, StreamError.BIND):
244            if error == 'conflict':
245                # Reset resource
246                app.settings.set_account_setting(self._account,
247                                                 'resource',
248                                                 'gajim.$rand')
249
250        elif domain == StreamError.SASL:
251            self._reconnect = False
252            self._destroy_client = True
253
254            if error in ('not-authorized', 'no-password'):
255                def _on_password(password):
256                    self.password = password
257                    self._client.set_password(password)
258                    self._prepare_for_connect()
259
260                app.nec.push_incoming_event(NetworkEvent(
261                    'password-required', conn=self, on_password=_on_password))
262
263            app.nec.push_incoming_event(
264                NetworkEvent('simple-notification',
265                             account=self._account,
266                             type_='connection-failed',
267                             title=_('Authentication failed'),
268                             text=text or error))
269
270        if self._reconnect:
271            self._after_disconnect()
272            self._schedule_reconnect()
273            app.nec.push_incoming_event(
274                NetworkEvent('our-show', account=self._account, show='error'))
275
276        else:
277            self.get_module('Chatstate').enabled = False
278            app.nec.push_incoming_event(NetworkEvent(
279                'our-show', account=self._account, show='offline'))
280            self._after_disconnect()
281
282    def _after_disconnect(self):
283        self._disable_reconnect_timer()
284
285        self.get_module('VCardAvatars').avatar_advertised = False
286
287        app.proxy65_manager.disconnect(self._client)
288        self.terminate_sessions()
289        self.get_module('Bytestream').remove_all_transfers()
290
291        if self._destroy_client:
292            self._client.destroy()
293            self._client = None
294            self._destroy_client = False
295            self._create_client()
296
297        app.nec.push_incoming_event(NetworkEvent('account-disconnected',
298                                                 account=self._account))
299
300    def _on_connection_failed(self, _client, _signal_name):
301        self._schedule_reconnect()
302
303    def _on_connected(self, _client, _signal_name):
304        self._set_state(ClientState.CONNECTED)
305        self.get_module('MUC').get_manager().reset_state()
306        self.get_module('Discovery').discover_server_info()
307        self.get_module('Discovery').discover_account_info()
308        self.get_module('Discovery').discover_server_items()
309        self.get_module('Chatstate').enabled = True
310        self.get_module('MAM').reset_state()
311
312    def _on_stanza_sent(self, _client, _signal_name, stanza):
313        app.nec.push_incoming_event(NetworkEvent('stanza-sent',
314                                                 account=self._account,
315                                                 stanza=stanza))
316
317    def _on_stanza_received(self, _client, _signal_name, stanza):
318        app.nec.push_incoming_event(NetworkEvent('stanza-received',
319                                                 account=self._account,
320                                                 stanza=stanza))
321    def get_own_jid(self):
322        """
323        Return the last full JID we received on a bind event.
324        In case we were never connected it returns the bare JID from config.
325        """
326        if self._client is not None:
327            jid = self._client.get_bound_jid()
328            if jid is not None:
329                return jid
330
331        # This returns the bare jid
332        return nbxmpp.JID.from_string(app.get_jid_from_account(self._account))
333
334    def change_status(self, show, message):
335        if not message:
336            message = ''
337
338        self._idle_status_enabled = show == 'online'
339        self._status_message = message
340
341        if show != 'offline':
342            self._status = show
343
344        if self._state.is_disconnecting:
345            log.warning('Can\'t change status while '
346                        'disconnect is in progress')
347            return
348
349        if self._state.is_disconnected:
350            if show == 'offline':
351                return
352
353            self._prepare_for_connect()
354            return
355
356        if self._state.is_connecting:
357            if show == 'offline':
358                self.disconnect(gracefully=False,
359                                reconnect=False,
360                                destroy_client=True)
361            return
362
363        if self._state.is_reconnect_scheduled:
364            if show == 'offline':
365                self._destroy_client = True
366                self._abort_reconnect()
367            else:
368                self._prepare_for_connect()
369            return
370
371        # We are connected
372        if show == 'offline':
373            self.set_user_activity(None)
374            self.set_user_mood(None)
375            self.set_user_tune(None)
376            self.set_user_location(None)
377            presence = self.get_module('Presence').get_presence(
378                typ='unavailable',
379                status=message,
380                caps=False)
381
382            self.send_stanza(presence)
383            self.disconnect(gracefully=True,
384                            reconnect=False,
385                            destroy_client=True)
386            return
387
388        self.update_presence()
389
390    def update_presence(self, include_muc=True):
391        status, message, idle = self.get_presence_state()
392        self._priority = app.get_priority(self._account, status)
393        self.get_module('Presence').send_presence(
394            priority=self._priority,
395            show=status,
396            status=message,
397            idle_time=idle)
398
399        if include_muc:
400            self.get_module('MUC').update_presence()
401
402    def set_user_activity(self, activity):
403        self.get_module('UserActivity').set_activity(activity)
404
405    def set_user_mood(self, mood):
406        self.get_module('UserMood').set_mood(mood)
407
408    def set_user_tune(self, tune):
409        self.get_module('UserTune').set_tune(tune)
410
411    def set_user_location(self, location):
412        self.get_module('UserLocation').set_location(location)
413
414    def get_module(self, name):
415        return modules.get(self._account, name)
416
417    @helpers.call_counter
418    def connect_machine(self):
419        log.info('Connect machine state: %s', self._connect_machine_calls)
420        if self._connect_machine_calls == 1:
421            self.get_module('MetaContacts').get_metacontacts()
422        elif self._connect_machine_calls == 2:
423            self.get_module('Delimiter').get_roster_delimiter()
424        elif self._connect_machine_calls == 3:
425            self.get_module('Roster').request_roster()
426        elif self._connect_machine_calls == 4:
427            self._finish_connect()
428
429    def _finish_connect(self):
430        self._status_sync_on_resume = False
431        self._set_client_available()
432
433        # We did not resume the stream, so we are not joined any MUCs
434        self.update_presence(include_muc=False)
435
436        self.get_module('Bookmarks').request_bookmarks()
437        self.get_module('SoftwareVersion').set_enabled(True)
438        self.get_module('Annotations').request_annotations()
439        self.get_module('Blocking').get_blocking_list()
440
441        # Inform GUI we just signed in
442        app.nec.push_incoming_event(NetworkEvent(
443            'signed-in', account=self._account, conn=self))
444        modules.send_stored_publish(self._account)
445
446    def send_stanza(self, stanza):
447        """
448        Send a stanza untouched
449        """
450        return self._client.send_stanza(stanza)
451
452    def send_message(self, message):
453        if not self._state.is_available:
454            log.warning('Trying to send message while offline')
455            return
456
457        stanza = self.get_module('Message').build_message_stanza(message)
458        message.stanza = stanza
459
460        if message.contact is None:
461            # Only Single Message should have no contact
462            self._send_message(message)
463            return
464
465        method = message.contact.settings.get('encryption')
466        if not method:
467            self._send_message(message)
468            return
469
470        # TODO: Make extension point return encrypted message
471        extension = 'encrypt'
472        if message.is_groupchat:
473            extension = 'gc_encrypt'
474        app.plugin_manager.extension_point(extension + method,
475                                           self,
476                                           message,
477                                           self._send_message)
478
479    def _send_message(self, message):
480        message.set_sent_timestamp()
481        message.message_id = self.send_stanza(message.stanza)
482
483        app.nec.push_incoming_event(
484            MessageSentEvent(None, jid=message.jid, **vars(message)))
485
486        if message.is_groupchat:
487            return
488
489        self.get_module('Message').log_message(message)
490
491    def send_messages(self, jids, message):
492        if not self._state.is_available:
493            log.warning('Trying to send message while offline')
494            return
495
496        for jid in jids:
497            message = message.copy()
498            message.contact = app.contacts.create_contact(jid, message.account)
499            stanza = self.get_module('Message').build_message_stanza(message)
500            message.stanza = stanza
501            self._send_message(message)
502
503    def _prepare_for_connect(self):
504        custom_host = get_custom_host(self._account)
505        if custom_host is not None:
506            self._client.set_custom_host(*custom_host)
507
508        gssapi = app.settings.get_account_setting(self._account,
509                                                  'enable_gssapi')
510        if gssapi:
511            self._client.set_mechs(['GSSAPI'])
512
513        anonymous = app.settings.get_account_setting(self._account,
514                                                     'anonymous_auth')
515        if anonymous:
516            self._client.set_mechs(['ANONYMOUS'])
517
518        if app.settings.get_account_setting(self._account,
519                                            'use_plain_connection'):
520            self._client.set_connection_types([ConnectionType.PLAIN])
521
522        proxy = get_user_proxy(self._account)
523        if proxy is not None:
524            self._client.set_proxy(proxy)
525
526        self.connect()
527
528    def connect(self, ignored_tls_errors=None):
529        if self._state not in (ClientState.DISCONNECTED,
530                               ClientState.RECONNECT_SCHEDULED):
531            # Do not try to reco while we are already trying
532            return
533
534        log.info('Connect')
535
536        self._client.set_ignored_tls_errors(ignored_tls_errors)
537        self._reconnect = True
538        self._disable_reconnect_timer()
539        self._set_state(ClientState.CONNECTING)
540
541        if warn_about_plain_connection(self._account,
542                                       self._client.connection_types):
543            app.nec.push_incoming_event(NetworkEvent(
544                'plain-connection',
545                account=self._account,
546                connect=self._client.connect,
547                abort=self._abort_reconnect))
548            return
549
550        self._client.connect()
551
552    def _schedule_reconnect(self):
553        self._set_state(ClientState.RECONNECT_SCHEDULED)
554        log.info("Reconnect to %s in 3s", self._account)
555        self._reconnect_timer_source = GLib.timeout_add_seconds(
556            3, self._prepare_for_connect)
557
558    def _abort_reconnect(self):
559        self._set_state(ClientState.DISCONNECTED)
560        self._disable_reconnect_timer()
561        app.nec.push_incoming_event(
562            NetworkEvent('our-show', account=self._account, show='offline'))
563
564        if self._destroy_client:
565            self._client.destroy()
566            self._client = None
567            self._destroy_client = False
568            self._create_client()
569
570    def _disable_reconnect_timer(self):
571        if self._reconnect_timer_source is not None:
572            GLib.source_remove(self._reconnect_timer_source)
573            self._reconnect_timer_source = None
574
575    def _idle_state_changed(self, monitor):
576        state = monitor.state.value
577
578        if monitor.is_awake():
579            self._idle_status = state
580            self._idle_status_message = ''
581            self._update_status()
582            return
583
584        if not app.settings.get(f'auto{state}'):
585            return
586
587        if (state in ('away', 'xa') and self._status == 'online' or
588                state == 'xa' and self._idle_status == 'away'):
589
590            self._idle_status = state
591            self._idle_status_message = get_idle_status_message(
592                state, self._status_message)
593            self._update_status()
594
595    def _update_status(self):
596        if not self._idle_status_enabled:
597            return
598
599        self._status = self._idle_status
600        if self._state.is_available:
601            self.update_presence()
602        else:
603            self._status_sync_on_resume = True
604
605    def _idle_status_active(self):
606        if not Monitor.is_available():
607            return False
608
609        if not self._idle_status_enabled:
610            return False
611
612        return self._idle_status != 'online'
613
614    def get_presence_state(self):
615        if self._idle_status_active():
616            return self._idle_status, self._idle_status_message, True
617        return self._status, self._status_message, False
618
619    @staticmethod
620    def _screensaver_state_changed(application, _param):
621        active = application.get_property('screensaver-active')
622        Monitor.set_extended_away(active)
623
624    def cleanup(self):
625        self._destroyed = True
626        if Monitor.is_available():
627            Monitor.disconnect(self._idle_handler_id)
628            app.app.disconnect(self._screensaver_handler_id)
629        if self._client is not None:
630            # cleanup() is called before nbmxpp.Client has disconnected,
631            # when we disable the account. So we need to unregister
632            # handlers here.
633            # TODO: cleanup() should not be called before disconnect is finished
634            for handler in modules.get_handlers(self):
635                self._client.unregister_handler(handler)
636        modules.unregister_modules(self)
637
638    def quit(self, kill_core):
639        if kill_core and self._state in (ClientState.CONNECTING,
640                                         ClientState.CONNECTED,
641                                         ClientState.AVAILABLE):
642            self.disconnect(gracefully=True, reconnect=False)
643