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