diff --git a/debian/control b/debian/control index af2d291..1af550b 100644 --- a/debian/control +++ b/debian/control @@ -1,27 +1,27 @@ Source: sipclients Section: python Priority: optional Maintainer: Adrian Georgescu Uploaders: Tijmen de Mes -Build-Depends: debhelper (>= 11), dh-python, python (>= 2.7) +Build-Depends: debhelper (>= 11), dh-python, python3 Standards-Version: 4.5.0 Homepage: http://sipsimpleclient.org Package: sipclients Section: net Architecture: all Depends: ${python:Depends}, ${misc:Depends}, libavahi-compat-libdnssd1, - python-application (>= 2.8.0), - python-eventlib, - python-requests, - python-lxml, - python-sipsimple (>= 3.5.0), - python-twisted-core, - python-zope.interface + python3-application (>= 2.8.0), + python3-eventlib, + python3-requests, + python3-lxml, + python3-sipsimple, + python3-twisted, + python3-zope.interface Conflicts: sipsimple-cli Description: SIP SIMPLE Command Line Clients This package contains Command Line Clients for testing SIP SIMPLE client SDK from http://sipsimpleclient.org. They demonstrate the SDK capabilities and can be used as examples or for testing purposes. diff --git a/debian/rules b/debian/rules index 31fe765..20cd6ce 100755 --- a/debian/rules +++ b/debian/rules @@ -1,9 +1,9 @@ #!/usr/bin/make -f %: - dh $@ --with python2 --buildsystem=pybuild + dh $@ --with python3 --buildsystem=pybuild override_dh_clean: dh_clean rm -rf build dist MANIFEST diff --git a/setup.py b/setup.py index 5a7ba3a..2b95835 100755 --- a/setup.py +++ b/setup.py @@ -1,44 +1,44 @@ -#!/usr/bin/python2 +#!/usr/bin/python3 from distutils.core import setup import os import glob import sipclient setup( name='sipclients', version=sipclient.__version__, description='SIP SIMPLE client', long_description='Python command line clients using the SIP SIMPLE framework', url='http://sipsimpleclient.org', author='AG Projects', author_email='support@ag-projects.com', platforms=['Platform Independent'], classifiers=[ 'Development Status :: 5 - Production/Stable', 'Intended Audience :: Service Providers', 'License :: GNU Lesser General Public License (LGPL)', 'Operating System :: OS Independent', 'Programming Language :: Python' ], packages=['sipclient', 'sipclient.configuration'], data_files=[('share/sipclients/sounds', glob.glob(os.path.join('resources', 'sounds', '*.wav')))], scripts=[ 'sip-audio-session', 'sip-message', 'sip-publish-presence', 'sip-register', 'sip-session', 'sip-settings', 'sip-subscribe-mwi', 'sip-subscribe-presence', 'sip-subscribe-rls', 'sip-subscribe-winfo', 'sip-subscribe-xcap-diff' ] ) diff --git a/sip-audio-session b/sip-audio-session index 7c8c43a..6aa5fa3 100755 --- a/sip-audio-session +++ b/sip-audio-session @@ -1,1186 +1,1184 @@ -#!/usr/bin/env python2 +#!/usr/bin/env python3 import atexit import os import platform import select import shutil import signal import sys import termios import uuid from datetime import datetime from itertools import chain from optparse import OptionParser from threading import Thread from time import sleep from application import log from application.notification import NotificationCenter, NotificationData from application.process import process from application.python.queue import EventQueue from application.system import makedirs from twisted.internet import reactor from sipsimple.account import Account, AccountManager, BonjourAccount from sipsimple.audio import WavePlayer from sipsimple.application import SIPApplication from sipsimple.configuration import ConfigurationError from sipsimple.configuration.settings import SIPSimpleSettings from sipsimple.core import Engine, SIPCoreError, SIPURI, ToHeader from sipsimple.lookup import DNSLookup from sipsimple.session import Session from sipsimple.streams import MediaStreamRegistry from sipsimple.storage import FileStorage from sipclient.configuration import config_directory from sipclient.configuration.account import AccountExtension from sipclient.configuration.datatypes import ResourcePath from sipclient.configuration.settings import SIPSimpleSettingsExtension from sipclient.log import Logger from sipclient.system import IPAddressMonitor class BonjourNeighbour(object): def __init__(self, neighbour, uri, display_name, host): self.display_name = display_name self.host = host self.neighbour = neighbour self.uri = uri class InputThread(Thread): def __init__(self): Thread.__init__(self) self.setDaemon(True) self._old_terminal_settings = None def start(self): atexit.register(self._termios_restore) Thread.start(self) def run(self): notification_center = NotificationCenter() while True: chars = list(self._getchars()) while chars: char = chars.pop(0) if char == '\x1b': # escape if len(chars) >= 2 and chars[0] == '[' and chars[1] in ('A', 'B', 'C', 'D'): # one of the arrow keys char = char + chars.pop(0) + chars.pop(0) notification_center.post_notification('SIPApplicationGotInput', sender=self, data=NotificationData(input=char)) def stop(self): self._termios_restore() def _termios_restore(self): if self._old_terminal_settings is not None: termios.tcsetattr(sys.stdin.fileno(), termios.TCSADRAIN, self._old_terminal_settings) def _getchars(self): fd = sys.stdin.fileno() if os.isatty(fd): self._old_terminal_settings = termios.tcgetattr(fd) new = termios.tcgetattr(fd) new[3] = new[3] & ~termios.ICANON & ~termios.ECHO - new[6][termios.VMIN] = '\000' + new[6][termios.VMIN] = b'\000' try: termios.tcsetattr(fd, termios.TCSADRAIN, new) if select.select([fd], [], [], None)[0]: return sys.stdin.read(4192) finally: self._termios_restore() else: return os.read(fd, 4192) class RTPStatisticsThread(Thread): def __init__(self, application): Thread.__init__(self) self.setDaemon(True) self.application = application self.stopped = False def run(self): while not self.stopped: if self.application.active_session is not None and self.application.active_session.streams: audio_stream = self.application.active_session.streams[0] stats = audio_stream.statistics if stats is not None: self.application.output.put('%s RTP audio statistics: RTT=%d ms, packet loss=%.1f%%, jitter RX/TX=%d/%d ms\n' % (datetime.now().replace(microsecond=0), stats['rtt']['avg'] / 1000, 100.0 * stats['rx']['packets_lost'] / stats['rx']['packets'] if stats['rx']['packets'] else 0, stats['rx']['jitter']['avg'] / 1000, stats['tx']['jitter']['avg'] / 1000)) try: video_stream = self.application.active_session.streams[1] except IndexError: pass else: stats = video_stream.statistics if stats is not None: self.application.output.put('%s RTP video statistics: RTT=%d ms, packet loss=%.1f%%, jitter RX/TX=%d/%d ms\n' % (datetime.now().replace(microsecond=0), stats['rtt']['avg'] / 1000, 100.0 * stats['rx']['packets_lost'] / stats['rx']['packets'] if stats['rx']['packets'] else 0, stats['rx']['jitter']['avg'] / 1000, stats['tx']['jitter']['avg'] / 1000)) sleep(10) def stop(self): self.stopped = True class CancelThread(Thread): def __init__(self, application): Thread.__init__(self) self.setDaemon(True) self.application = application self.stopped = False def run(self): while not self.stopped: self.application.end_session_if_needed() sleep(1) def stop(self): self.stopped = True class SIPAudioApplication(SIPApplication): def __init__(self): self.account = None self.options = None self.target = None self.active_session = None self.answer_timers = {} self.hangup_timers = {} self.started_sessions = [] self.incoming_sessions = [] self.outgoing_session = None self.neighbours = {} self.registration_succeeded = False self.success = False self.input = None self.output = None self.ip_address_monitor = IPAddressMonitor() self.logger = None self.rtp_statistics = None self.alert_tone_generator = None self.voice_tone_generator = None self.wave_inbound_ringtone = None self.wave_outbound_ringtone = None self.tone_ringtone = None self.hold_tone = None self.ignore_local_hold = False self.ignore_local_unhold = False self.batch_mode = False self.stop_call_thread = None self.session_spool_dir = None def _write(self, message): - if isinstance(message, unicode): - message = message.encode(sys.getfilesystemencoding()) sys.stdout.write(message) sys.stdout.flush() def start(self, target, options): notification_center = NotificationCenter() if options.daemonize: process.daemonize() self.options = options self.target = target self.batch_mode = options.batch_mode self.enable_video = options.enable_video self.input = InputThread() if not self.batch_mode else None self.output = EventQueue(self._write) self.logger = Logger(sip_to_stdout=options.trace_sip, pjsip_to_stdout=options.trace_pjsip, notifications_to_stdout=options.trace_notifications) notification_center.add_observer(self, sender=self) notification_center.add_observer(self, sender=self.input) notification_center.add_observer(self, name='SIPSessionNewIncoming') notification_center.add_observer(self, name='RTPStreamDidChangeRTPParameters') notification_center.add_observer(self, name='RTPStreamICENegotiationDidSucceed') notification_center.add_observer(self, name='RTPStreamICENegotiationDidFail') if self.input: self.input.start() self.output.start() log.level.current = log.level.WARNING # get rid of twisted messages Account.register_extension(AccountExtension) BonjourAccount.register_extension(AccountExtension) SIPSimpleSettings.register_extension(SIPSimpleSettingsExtension) self.config_directory = options.config_directory or config_directory try: self.output.put("Using config directory: %s\n" % self.config_directory) SIPApplication.start(self, FileStorage(self.config_directory)) - except ConfigurationError, e: + except ConfigurationError as e: self.output.put("Failed to load sipclient's configuration: %s\n" % str(e)) self.output.put("If an old configuration file is in place, delete it or move it and recreate the configuration using the sip_settings script.\n") self.output.stop() if options.spool_dir: self.spool_dir = options.spool_dir else: self.spool_dir = "%s/spool/sesssions" % self.config_directory try: makedirs(self.spool_dir) except Exception as e: log.error('Failed to create spool directory at {directory}: {exception!s}'.format(directory=self.spool_dir, exception=e)) else: self.output.put("Using spool directory %s\n" % self.spool_dir) def print_help(self): message = 'Available control keys:\n' message += ' s: toggle SIP trace on the console\n' message += ' j: toggle PJSIP trace on the console\n' message += ' n: toggle notifications trace on the console\n' message += ' p: toggle printing RTP statistics on the console\n' message += ' h: hang-up the active session\n' message += ' r: toggle audio recording\n' message += ' m: mute the microphone\n' message += ' i: change audio input device\n' message += ' o: change audio output device\n' message += ' a: change audio alert device\n' message += ' SPACE: hold/unhold\n' message += ' Ctrl-d: quit the program\n' message += ' ?: display this help message\n' self.output.put('\n'+message+'\n') def _NH_SIPApplicationWillStart(self, notification): account_manager = AccountManager() notification_center = NotificationCenter() settings = SIPSimpleSettings() if 'armv7' in platform.platform() and settings.audio.echo_canceller.enabled: self.output.put("Disable echo canceller on ARM architecture\n") settings.audio.echo_canceller.enabled = False settings.save() for account in account_manager.iter_accounts(): if isinstance(account, Account): account.sip.register = False account.presence.enabled = False account.xcap.enabled = False account.message_summary.enabled = False if self.options.account is None: self.account = account_manager.default_account else: possible_accounts = [account for account in account_manager.iter_accounts() if self.options.account in account.id and account.enabled] if len(possible_accounts) > 1: self.output.put('More than one account exists which matches %s: %s\n' % (self.options.account, ', '.join(sorted(account.id for account in possible_accounts)))) self.output.stop() self.stop() self.end_cancel_thread() return elif len(possible_accounts) == 0: self.output.put('No enabled account that matches %s was found. Available and enabled accounts: %s\n' % (self.options.account, ', '.join(sorted(account.id for account in account_manager.get_accounts() if account.enabled)))) self.output.stop() self.stop() self.end_cancel_thread() return else: self.account = possible_accounts[0] notification_center.add_observer(self, sender=self.account) if isinstance(self.account, Account) and self.target is None: self.account.sip.register = True self.account.presence.enabled = False self.account.xcap.enabled = False self.account.message_summary.enabled = False self.output.put('Using account %s\n' % self.account.id) self.logger.start() if settings.logs.trace_sip and self.logger._siptrace_filename is not None: self.output.put('Logging SIP trace to file "%s"\n' % self.logger._siptrace_filename) if settings.logs.trace_pjsip and self.logger._pjsiptrace_filename is not None: self.output.put('Logging PJSIP trace to file "%s"\n' % self.logger._pjsiptrace_filename) if settings.logs.trace_notifications and self.logger._notifications_filename is not None: self.output.put('Logging notifications trace to file "%s"\n' % self.logger._notifications_filename) if self.options.disable_sound: settings.audio.input_device = None settings.audio.output_device = None settings.audio.alert_device = None if self.options.enable_default_devices: settings.audio.input_device = 'system_default' settings.audio.output_device = 'system_default' settings.audio.alert_device = 'system_default' def _NH_SIPApplicationDidStart(self, notification): engine = Engine() settings = SIPSimpleSettings() engine.trace_sip = self.logger.sip_to_stdout or settings.logs.trace_sip engine.log_level = settings.logs.pjsip_level if (self.logger.pjsip_to_stdout or settings.logs.trace_pjsip) else 0 self.ip_address_monitor.start() self.output.put('Available audio input devices: %s\n' % ', '.join(['None', 'system_default'] + sorted(engine.input_devices))) self.output.put('Available audio output devices: %s\n' % ', '.join(['None', 'system_default'] + sorted(engine.output_devices))) if engine.video_devices: self.output.put('Available cameras: %s\n' % ', '.join(sorted(engine.video_devices))) else: if self.enable_video: self.output.put('No camera present, video is disabled') self.enable_video = False if self.voice_audio_mixer.input_device == 'system_default': self.output.put('Using audio input device: %s (system default device)\n' % self.voice_audio_mixer.real_input_device) else: self.output.put('Using audio input device: %s\n' % self.voice_audio_mixer.input_device) if self.voice_audio_mixer.output_device == 'system_default': self.output.put('Using audio output device: %s (system default device)\n' % self.voice_audio_mixer.real_output_device) else: self.output.put('Using audio output device: %s\n' % self.voice_audio_mixer.output_device) if self.alert_audio_mixer.output_device == 'system_default': self.output.put('Using audio alert device: %s (system default device)\n' % self.alert_audio_mixer.real_output_device) else: self.output.put('Using audio alert device: %s\n' % self.alert_audio_mixer.output_device) if not self.batch_mode: self.print_help() inbound_ringtone = self.account.sounds.audio_inbound.sound_file if self.account.sounds.audio_inbound is not None else None outbound_ringtone = settings.sounds.audio_outbound if inbound_ringtone: self.wave_inbound_ringtone = WavePlayer(self.alert_audio_mixer, inbound_ringtone.path.normalized, volume=inbound_ringtone.volume, loop_count=0, pause_time=2) self.alert_audio_bridge.add(self.wave_inbound_ringtone) if outbound_ringtone: self.wave_outbound_ringtone = WavePlayer(self.voice_audio_mixer, outbound_ringtone.path.normalized, volume=outbound_ringtone.volume, loop_count=0, pause_time=2) self.voice_audio_bridge.add(self.wave_outbound_ringtone) self.tone_ringtone = WavePlayer(self.voice_audio_mixer, ResourcePath('sounds/ring_tone.wav').normalized, loop_count=0, pause_time=6) self.voice_audio_bridge.add(self.tone_ringtone) self.hold_tone = WavePlayer(self.voice_audio_mixer, ResourcePath('sounds/hold_tone.wav').normalized, loop_count=0, pause_time=30, volume=50) self.voice_audio_bridge.add(self.hold_tone) if self.target is not None: if isinstance(self.account, BonjourAccount) and '@' not in self.target: self.output.put('Bonjour mode requires a host in the destination address\n') self.stop() self.end_cancel_thread() return if '@' not in self.target: self.target = '%s@%s' % (self.target, self.account.id.domain) if not self.target.startswith('sip:') and not self.target.startswith('sips:'): self.target = 'sip:' + self.target try: self.target = SIPURI.parse(self.target) except SIPCoreError: self.output.put('Illegal SIP URI: %s\n' % self.target) self.stop() else: if '.' not in self.target.host and not isinstance(self.account, BonjourAccount): self.target.host = '%s.%s' % (self.target.host, self.account.id.domain) lookup = DNSLookup() notification_center = NotificationCenter() settings = SIPSimpleSettings() notification_center.add_observer(self, sender=lookup) if isinstance(self.account, Account) and self.account.sip.outbound_proxy is not None: uri = SIPURI(host=self.account.sip.outbound_proxy.host, port=self.account.sip.outbound_proxy.port, parameters={'transport': self.account.sip.outbound_proxy.transport}) elif isinstance(self.account, Account) and self.account.sip.always_use_my_proxy: uri = SIPURI(host=self.account.id.domain) else: uri = self.target self.session_spool_dir = self.spool_dir + "/" + (self.options.external_id or str(uuid.uuid1())) lookup.lookup_sip_proxy(uri, settings.sip.transport_list) try: makedirs(self.session_spool_dir) except Exception as e: log.error('Failed to create session spool directory at {directory}: {exception!s}'.format(directory=self.session_spool_dir, exception=e)) else: if self.stop_call_thread is None: self.stop_call_thread = CancelThread(self) self.stop_call_thread.start() self.output.put("To stop the call: touch %s/stop\n" % self.session_spool_dir) def _NH_SIPApplicationWillEnd(self, notification): if isinstance(self.account, Account): self.account.sip.register = False self.ip_address_monitor.stop() def _NH_SIPApplicationDidEnd(self, notification): if self.input: self.input.stop() self.output.stop() self.output.join() def _NH_SIPEngineDetectedNATType(self, notification): SIPApplication._NH_SIPEngineDetectedNATType(self, notification) if notification.data.succeeded: self.output.put('Detected NAT type: %s\n' % notification.data.nat_type) def _NH_SIPApplicationGotInput(self, notification): engine = Engine() notification_center = NotificationCenter() settings = SIPSimpleSettings() if notification.data.input == '\x04': if self.active_session is not None: self.output.put('Ending audio session...\n') self.active_session.end() elif self.outgoing_session is not None: self.output.put('Cancelling audio session...\n') self.outgoing_session.end() else: self.stop() self.end_cancel_thread() elif notification.data.input == '?': self.print_help() elif notification.data.input in ('y', 'n') and self.incoming_sessions: accepted_types = ['video', 'audio'] if self.enable_video else ['audio'] session = self.incoming_sessions.pop(0) if notification.data.input == 'y': session.accept([stream for stream in session.proposed_streams if stream.type in accepted_types]) else: session.reject() elif notification.data.input == 'm': self.voice_audio_mixer.muted = not self.voice_audio_mixer.muted self.output.put('The microphone is now %s\n' % ('muted' if self.voice_audio_mixer.muted else 'unmuted')) elif notification.data.input == 'i': - input_devices = [None, u'system_default'] + sorted(engine.input_devices) + input_devices = [None, 'system_default'] + sorted(engine.input_devices) if self.voice_audio_mixer.input_device in input_devices: old_input_device = self.voice_audio_mixer.input_device else: old_input_device = None tail_length = settings.audio.echo_canceller.tail_length if settings.audio.echo_canceller.enabled else 0 new_input_device = input_devices[(input_devices.index(old_input_device)+1) % len(input_devices)] try: self.voice_audio_mixer.set_sound_devices(new_input_device, self.voice_audio_mixer.output_device, tail_length) - except SIPCoreError, e: + except SIPCoreError as e: self.output.put('Failed to set input device to %s: %s\n' % (new_input_device, str(e))) else: - if new_input_device == u'system_default': + if new_input_device == 'system_default': self.output.put('Audio input device changed to %s (system default device)\n' % self.voice_audio_mixer.real_input_device) else: self.output.put('Audio input device changed to %s\n' % new_input_device) elif notification.data.input == 'o': - output_devices = [None, u'system_default'] + sorted(engine.output_devices) + output_devices = [None, 'system_default'] + sorted(engine.output_devices) if self.voice_audio_mixer.output_device in output_devices: old_output_device = self.voice_audio_mixer.output_device else: old_output_device = None tail_length = settings.audio.echo_canceller.tail_length if settings.audio.echo_canceller.enabled else 0 new_output_device = output_devices[(output_devices.index(old_output_device)+1) % len(output_devices)] try: self.voice_audio_mixer.set_sound_devices(self.voice_audio_mixer.input_device, new_output_device, tail_length) - except SIPCoreError, e: + except SIPCoreError as e: self.output.put('Failed to set output device to %s: %s\n' % (new_output_device, str(e))) else: - if new_output_device == u'system_default': + if new_output_device == 'system_default': self.output.put('Audio output device changed to %s (system default device)\n' % self.voice_audio_mixer.real_output_device) else: self.output.put('Audio output device changed to %s\n' % new_output_device) elif notification.data.input == 'a': - output_devices = [None, u'system_default'] + sorted(engine.output_devices) + output_devices = [None, 'system_default'] + sorted(engine.output_devices) if self.alert_audio_mixer.output_device in output_devices: old_output_device = self.alert_audio_mixer.output_device else: old_output_device = None tail_length = settings.audio.echo_canceller.tail_length if settings.audio.echo_canceller.enabled else 0 new_output_device = output_devices[(output_devices.index(old_output_device)+1) % len(output_devices)] try: self.alert_audio_mixer.set_sound_devices(self.alert_audio_mixer.input_device, new_output_device, tail_length) - except SIPCoreError, e: + except SIPCoreError as e: self.output.put('Failed to set alert device to %s: %s\n' % (new_output_device, str(e))) else: - if new_output_device == u'system_default': + if new_output_device == 'system_default': self.output.put('Audio alert device changed to %s (system default device)\n' % self.alert_audio_mixer.real_output_device) else: self.output.put('Audio alert device changed to %s\n' % new_output_device) elif notification.data.input == 'h': if self.active_session is not None: self.output.put('Ending audio session...\n') self.active_session.end() elif self.outgoing_session is not None: self.output.put('Cancelling audio session...\n') self.outgoing_session.end() elif notification.data.input == ' ': if self.active_session is not None: if self.active_session.on_hold: self.active_session.unhold() else: self.active_session.hold() elif notification.data.input in ('0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '*', '#', 'A', 'B', 'C', 'D'): if self.active_session is not None: try: audio_stream = self.active_session.streams[0] except IndexError: pass else: digit = notification.data.input filename = 'sounds/dtmf_%s_tone.wav' % {'*': 'star', '#': 'pound'}.get(digit, digit) wave_player = WavePlayer(self.voice_audio_mixer, ResourcePath(filename).normalized) notification_center.add_observer(self, sender=wave_player) audio_stream.send_dtmf(digit) if self.active_session.account.rtp.inband_dtmf: audio_stream.bridge.add(wave_player) self.voice_audio_bridge.add(wave_player) wave_player.start() elif notification.data.input in ('\x1b[A', '\x1b[D') and len(self.started_sessions) > 0: # UP and LEFT if self.active_session is None: self.active_session = self.started_sessions[0] self.active_session.unhold() self.ignore_local_unhold = True elif len(self.started_sessions) > 1: self.active_session.hold() self.active_session = self.started_sessions[self.started_sessions.index(self.active_session)-1] self.active_session.unhold() self.ignore_local_unhold = True else: return identity = str(self.active_session.remote_identity.uri) if self.active_session.remote_identity.display_name: identity = '"%s" <%s>' % (self.active_session.remote_identity.display_name, identity) self.output.put('Active audio session: "%s" (%d/%d)\n' % (identity, self.started_sessions.index(self.active_session)+1, len(self.started_sessions))) elif notification.data.input in ('\x1b[B', '\x1b[C') and len(self.started_sessions) > 0: # DOWN and RIGHT if self.active_session is None: self.active_session = self.started_sessions[0] self.active_session.unhold() self.ignore_local_unhold = True elif len(self.started_sessions) > 1: self.active_session.hold() self.active_session = self.started_sessions[(self.started_sessions.index(self.active_session)+1) % len(self.started_sessions)] self.active_session.unhold() self.ignore_local_unhold = True else: return identity = str(self.active_session.remote_identity.uri) if self.active_session.remote_identity.display_name: identity = '"%s" <%s>' % (self.active_session.remote_identity.display_name, identity) self.output.put('Active audio session: "%s" (%d/%d)\n' % (identity, self.started_sessions.index(self.active_session)+1, len(self.started_sessions))) elif notification.data.input == 'r': if self.active_session is None or not self.active_session.streams: return session = self.active_session audio_stream = self.active_session.streams[0] if audio_stream.recorder is not None: audio_stream.stop_recording() else: direction = session.direction remote = "%s@%s" % (session.remote_identity.uri.user, session.remote_identity.uri.host) filename = "%s-%s-%s.wav" % (datetime.now().strftime("%Y%m%d-%H%M%S"), remote, direction) path = os.path.join(settings.audio.directory.normalized, session.account.id) audio_stream.start_recording(os.path.join(path, filename)) elif notification.data.input == 'p': if self.rtp_statistics is None: self.rtp_statistics = RTPStatisticsThread(self) self.rtp_statistics.start() self.output.put('Output of RTP statistics on console is now activated\n') else: self.rtp_statistics.stop() self.rtp_statistics = None self.output.put('Output of RTP statistics on console is now dectivated\n') elif notification.data.input == 'j': self.logger.pjsip_to_stdout = not self.logger.pjsip_to_stdout engine.log_level = settings.logs.pjsip_level if (self.logger.pjsip_to_stdout or settings.logs.trace_pjsip) else 0 self.output.put('PJSIP tracing to console is now %s\n' % ('activated' if self.logger.pjsip_to_stdout else 'deactivated')) elif notification.data.input == 'n': self.logger.notifications_to_stdout = not self.logger.notifications_to_stdout self.output.put('Notification tracing to console is now %s.\n' % ('activated' if self.logger.notifications_to_stdout else 'deactivated')) elif notification.data.input == 's': self.logger.sip_to_stdout = not self.logger.sip_to_stdout engine.trace_sip = self.logger.sip_to_stdout or settings.logs.trace_sip self.output.put('SIP tracing to console is now %s\n' % ('activated' if self.logger.sip_to_stdout else 'deactivated')) def _NH_SIPEngineGotException(self, notification): self.output.put('An exception occured within the SIP core:\n%s\n' % notification.data.traceback) def _NH_SIPAccountRegistrationDidSucceed(self, notification): if self.registration_succeeded: return contact_header = notification.data.contact_header contact_header_list = notification.data.contact_header_list expires = notification.data.expires registrar = notification.data.registrar message = '%s Registered contact "%s" for sip:%s at %s:%d;transport=%s (expires in %d seconds).\n' % (datetime.now().replace(microsecond=0), contact_header.uri, self.account.id, registrar.address, registrar.port, registrar.transport, expires) if len(contact_header_list) > 1: message += 'Other registered contacts:\n%s\n' % '\n'.join([' %s (expires in %s seconds)' % (str(other_contact_header.uri), other_contact_header.expires) for other_contact_header in contact_header_list if other_contact_header.uri != notification.data.contact_header.uri]) self.output.put(message) self.registration_succeeded = True def _NH_SIPAccountRegistrationDidFail(self, notification): self.output.put('%s Failed to register contact for sip:%s: %s (retrying in %.2f seconds)\n' % (datetime.now().replace(microsecond=0), self.account.id, notification.data.error, notification.data.retry_after)) self.registration_succeeded = False def _NH_SIPAccountRegistrationDidEnd(self, notification): self.output.put('%s Registration ended.\n' % datetime.now().replace(microsecond=0)) def _NH_BonjourAccountRegistrationDidSucceed(self, notification): self.output.put('%s Registered Bonjour contact "%s"\n' % (datetime.now().replace(microsecond=0), notification.data.name)) def _NH_BonjourAccountRegistrationDidFail(self, notification): self.output.put('%s Failed to register Bonjour contact: %s\n' % (datetime.now().replace(microsecond=0), notification.data.reason)) def _NH_BonjourAccountRegistrationDidEnd(self, notification): self.output.put('%s Registration ended.\n' % datetime.now().replace(microsecond=0)) def _NH_BonjourAccountDidAddNeighbour(self, notification): neighbour, record = notification.data.neighbour, notification.data.record now = datetime.now().replace(microsecond=0) self.output.put('%s Discovered Bonjour neighbour: "%s (%s)" <%s>\n' % (now, record.name, record.host, record.uri)) self.neighbours[neighbour] = BonjourNeighbour(neighbour, record.uri, record.name, record.host) def _NH_BonjourAccountDidUpdateNeighbour(self, notification): neighbour, record = notification.data.neighbour, notification.data.record now = datetime.now().replace(microsecond=0) try: bonjour_neighbour = self.neighbours[neighbour] except KeyError: self.output.put('%s Discovered Bonjour neighbour: "%s (%s)" <%s>\n' % (now, record.name, record.host, record.uri)) self.neighbours[neighbour] = BonjourNeighbour(neighbour, record.uri, record.name, record.host) else: self.output.put('%s Updated Bonjour neighbour: "%s (%s)" <%s>\n' % (now, record.name, record.host, record.uri)) bonjour_neighbour.display_name = record.name bonjour_neighbour.host = record.host bonjour_neighbour.uri = record.uri def _NH_BonjourAccountDidRemoveNeighbour(self, notification): neighbour = notification.data.neighbour now = datetime.now().replace(microsecond=0) try: bonjour_neighbour = self.neighbours.pop(neighbour) except KeyError: pass else: self.output.put('%s Bonjour neighbour left: "%s (%s)" <%s>\n' % (now, bonjour_neighbour.display_name, bonjour_neighbour.host, bonjour_neighbour.uri)) def _NH_DNSLookupDidSucceed(self, notification): notification_center = NotificationCenter() result_text = ', '.join(('%s:%s (%s)' % (result.address, result.port, result.transport.upper()) for result in notification.data.result)) - self.output.put(u"\nDNS lookup for %s succeeded: %s\n" % (self.target.host, result_text)) + self.output.put("\nDNS lookup for %s succeeded: %s\n" % (self.target.host, result_text)) if self.end_session_if_needed(): self.stop() self.end_cancel_thread() self.outgoing_session = session = Session(self.account) notification_center.add_observer(self, sender=session) streams = [MediaStreamRegistry.AudioStream(), MediaStreamRegistry.VideoStream()] if self.enable_video else [MediaStreamRegistry.AudioStream()] session.connect(ToHeader(self.target), routes=notification.data.result, streams=streams) def _NH_DNSLookupDidFail(self, notification): self.output.put('DNS lookup failed: %s\n' % notification.data.error) self.stop() self.end_cancel_thread() def auto_answer_allowed(self, uri): if not self.options.auto_answer_uris: self.output.put('Auto answer allowed for %s\n' % uri) return True uri = uri.split(":")[1] auto_answer_uris = self.options.auto_answer_uris.split(",") if uri in auto_answer_uris: self.output.put('Auto answer allowed for %s\n' % uri) return True self.output.put('Auto answer denied for %s\n' % uri) return False def _NH_SIPSessionNewIncoming(self, notification): session = notification.sender for stream in notification.data.streams: if stream.type == 'audio': break else: session.reject(415) return self.session_spool_dir = self.spool_dir + "/" + str(uuid.uuid1()) try: makedirs(self.session_spool_dir) except Exception as e: log.error('Failed to create session spool directory at {directory}: {exception!s}'.format(directory=self.session_spool_dir, exception=e)) else: if self.stop_call_thread is None: self.stop_call_thread = CancelThread(self) self.stop_call_thread.start() self.output.put("To stop the call: touch %s/stop\n" % self.session_spool_dir) notification_center = NotificationCenter() notification_center.add_observer(self, sender=session) accepted_types = ['video', 'audio'] if self.enable_video else ['audio'] if self.options.auto_answer_interval is not None and self.auto_answer_allowed(str(session.remote_identity.uri)): if self.options.auto_answer_interval == 0: session.accept([stream for stream in session.proposed_streams if stream.type in accepted_types]) return else: def auto_answer(): self.incoming_sessions.remove(session) session.accept([stream for stream in session.proposed_streams if stream.type in accepted_types]) timer = reactor.callLater(self.options.auto_answer_interval, auto_answer) self.answer_timers[id(session)] = timer session.send_ring_indication() self.incoming_sessions.append(session) if len(self.incoming_sessions) == 1: self._print_new_session() if not self.started_sessions: if self.wave_inbound_ringtone: self.wave_inbound_ringtone.start() else: self.tone_ringtone.start() def _NH_SIPSessionNewOutgoing(self, notification): session = notification.sender local_identity = str(session.local_identity.uri) if session.local_identity.display_name: local_identity = '"%s" <%s>' % (session.local_identity.display_name, local_identity) remote_identity = str(session.remote_identity.uri) if session.remote_identity.display_name: remote_identity = '"%s" <%s>' % (session.remote_identity.display_name, remote_identity) self.output.put("Initiating SIP %s session from '%s' to '%s' via %s...\n" % ('video' if self.enable_video else 'audio', local_identity, remote_identity, session.route)) def _NH_SIPSessionGotRingIndication(self, notification): if self.wave_outbound_ringtone: self.wave_outbound_ringtone.start() def _NH_SIPSessionDidFail(self, notification): session = notification.sender if session.direction == 'incoming' and session in self.incoming_sessions: if self.wave_inbound_ringtone: self.wave_inbound_ringtone.stop() self.tone_ringtone.stop() elif session.direction == 'outgoing': if self.wave_outbound_ringtone: self.wave_outbound_ringtone.stop() if notification.data.failure_reason == 'Call completed elsewhere' or notification.data.code == 487: self.output.put('Session cancelled\n') if session is self.outgoing_session: self.stop() self.end_cancel_thread() if session in self.incoming_sessions: self.incoming_sessions.remove(session) elif notification.data.failure_reason == 'user request': self.output.put('Session rejected by user (%d %s)\n' % (notification.data.code, notification.data.reason)) if notification.sender is self.outgoing_session: self.stop() self.end_cancel_thread() else: self.output.put('Session failed: %s\n' % notification.data.failure_reason) if session is self.outgoing_session: self.stop() self.end_cancel_thread() if id(session) in self.answer_timers: timer = self.answer_timers[id(session)] if timer.active(): timer.cancel() del self.answer_timers[id(session)] if self.incoming_sessions: self._print_new_session() elif session.direction == 'incoming': if self.wave_inbound_ringtone: self.wave_inbound_ringtone.stop() self.tone_ringtone.stop() self.success = False def _NH_SIPSessionWillStart(self, notification): session = notification.sender if session.direction == 'incoming': if self.wave_inbound_ringtone: self.wave_inbound_ringtone.stop() if not self.incoming_sessions: self.tone_ringtone.stop() else: if self.wave_outbound_ringtone: self.wave_outbound_ringtone.stop() if id(session) in self.answer_timers: timer = self.answer_timers[id(session)] if timer.active(): timer.cancel() del self.answer_timers[id(session)] def _NH_SIPSessionDidStart(self, notification): notification_center = NotificationCenter() session = notification.sender self.output.put('Session started with %d streams' % len(notification.data.streams)) if session.remote_user_agent is not None: self.output.put('Remote SIP User Agent is "%s"\n' % session.remote_user_agent) audio_stream = notification.data.streams[0] self.output.put('Audio stream established using "%s" codec at %sHz\n' % (audio_stream.codec, audio_stream.sample_rate)) if audio_stream.ice_active: self.output.put('Audio RTP endpoints %s:%d (ICE type %s) <-> %s:%d (ICE type %s)\n' % (audio_stream.local_rtp_address, audio_stream.local_rtp_port, audio_stream.local_rtp_candidate.type.lower(), audio_stream.remote_rtp_address, audio_stream.remote_rtp_port, audio_stream.remote_rtp_candidate.type.lower())) else: self.output.put('Audio RTP endpoints %s:%d <-> %s:%d\n' % (audio_stream.local_rtp_address, audio_stream.local_rtp_port, audio_stream.remote_rtp_address, audio_stream.remote_rtp_port)) if audio_stream.encryption.active: self.output.put('RTP audio stream is encrypted using %s (%s)\n' % (audio_stream.encryption.type, audio_stream.encryption.cipher)) try: video_stream = notification.data.streams[1] except IndexError: video_stream = None else: self.output.put('Video stream established using "%s" codec at %sHz\n' % (video_stream.codec, video_stream.sample_rate)) if video_stream.ice_active: self.output.put('Video RTP endpoints %s:%d (ICE type %s) <-> %s:%d (ICE type %s)\n' % (video_stream.local_rtp_address, video_stream.local_rtp_port, video_stream.local_rtp_candidate.type.lower(), video_stream.remote_rtp_address, video_stream.remote_rtp_port, video_stream.remote_rtp_candidate.type.lower())) else: self.output.put('Video RTP endpoints %s:%d <-> %s:%d\n' % (video_stream.local_rtp_address, video_stream.local_rtp_port, video_stream.remote_rtp_address, video_stream.remote_rtp_port)) if video_stream.encryption.active: self.output.put('RTP video stream is encrypted using %s (%s)\n' % (video_stream.encryption.type, video_stream.encryption.cipher)) self.started_sessions.append(session) if self.active_session is not None: self.active_session.hold() self.active_session = session if len(self.started_sessions) > 1: message = 'Connected sessions:\n' for session in self.started_sessions: identity = str(session.remote_identity.uri) if session.remote_identity.display_name: identity = '"%s" <%s>' % (session.remote_identity.display_name, identity) message += ' Session %s (%d/%d) - %s\n' % (identity, self.started_sessions.index(session)+1, len(self.started_sessions), 'active' if session is self.active_session else 'on hold') message += 'Press arrow keys to switch the active session\n' self.output.put(message) if self.incoming_sessions: self.tone_ringtone.start() self._print_new_session() for stream in notification.data.streams: notification_center.add_observer(self, sender=stream) if self.options.auto_hangup_interval is not None: if self.options.auto_hangup_interval == 0: session.end() else: timer = reactor.callLater(self.options.auto_hangup_interval, session.end) self.hangup_timers[id(session)] = timer def _NH_SIPSessionWillEnd(self, notification): notification_center = NotificationCenter() session = notification.sender if id(session) in self.hangup_timers: timer = self.hangup_timers[id(session)] if timer.active(): timer.cancel() del self.hangup_timers[id(session)] hangup_tone = WavePlayer(self.voice_audio_mixer, ResourcePath('sounds/hangup_tone.wav').normalized) notification_center.add_observer(self, sender=hangup_tone) self.voice_audio_bridge.add(hangup_tone) hangup_tone.start() def end_session_if_needed(self): if not self.session_spool_dir: return False stop_call_file = self.session_spool_dir + "/stop" if stop_call_file and os.path.exists(stop_call_file): if self.active_session is not None: self.output.put('Ending audio session...\n') self.active_session.end() elif self.outgoing_session is not None: self.outgoing_session.end() elif self.incoming_sessions: session = self.incoming_sessions.pop(0) session.reject() return True return False def end_cancel_thread(self): if self.stop_call_thread is not None: self.stop_call_thread.stop() self.stop_call_thread = None if self.session_spool_dir: try: shutil.rmtree(self.session_spool_dir) except OSError: pass self.session_spool_dir = None def _NH_SIPSessionDidEnd(self, notification): self.end_cancel_thread() session = notification.sender if session is not self.active_session: identity = str(session.remote_identity.uri) if session.remote_identity.display_name: identity = '"%s" <%s>' % (session.remote_identity.display_name, identity) else: identity = '\b' if notification.data.end_reason == 'user request': self.output.put('Session %s ended by %s party\n' % (identity, notification.data.originator)) else: self.output.put('Session %s ended due to error: %s\n' % (identity, notification.data.end_reason)) if session.end_time and session.start_time: duration = session.end_time - session.start_time seconds = duration.seconds if duration.microseconds < 500000 else duration.seconds+1 minutes, seconds = seconds / 60, seconds % 60 hours, minutes = minutes / 60, minutes % 60 hours += duration.days*24 if not minutes and not hours: duration_text = '%d seconds' % seconds elif not hours: duration_text = '%02d:%02d' % (minutes, seconds) else: duration_text = '%02d:%02d:%02d' % (hours, minutes, seconds) self.output.put('Session duration was %s\n' % duration_text) self.started_sessions.remove(session) if session is self.active_session: if self.started_sessions: self.active_session = self.started_sessions[0] self.active_session.unhold() self.ignore_local_unhold = True identity = str(self.active_session.remote_identity.uri) if self.active_session.remote_identity.display_name: identity = '"%s" <%s>' % (self.active_session.remote_identity.display_name, identity) self.output.put('Active audio session: "%s" (%d/%d)\n' % (identity, self.started_sessions.index(self.active_session)+1, len(self.started_sessions))) else: self.active_session = None if session is self.outgoing_session: self.stop() self.end_cancel_thread() on_hold_streams = [stream for stream in chain(*(session.streams for session in self.started_sessions)) if stream.on_hold] if not on_hold_streams and self.hold_tone.is_active: self.hold_tone.stop() self.success = True def _NH_SIPSessionDidChangeHoldState(self, notification): session = notification.sender if notification.data.on_hold: if notification.data.originator == 'remote': if session is self.active_session: self.output.put('Remote party has put the audio session on hold\n') else: identity = str(session.remote_identity.uri) if session.remote_identity.display_name: identity = '"%s" <%s>' % (session.remote_identity.display_name, identity) self.output.put('%s has put the audio session on hold\n' % identity) elif not self.ignore_local_hold: if session is self.active_session: self.output.put('Session is put on hold\n') else: identity = str(session.remote_identity.uri) if session.remote_identity.display_name: identity = '"%s" <%s>' % (session.remote_identity.display_name, identity) self.output.put('Session %s is put on hold\n' % identity) else: self.ignore_local_hold = False else: if notification.data.originator == 'remote': if session is self.active_session: self.output.put('Remote party has taken the audio session out of hold\n') else: identity = str(session.remote_identity.uri) if session.remote_identity.display_name: identity = '"%s" <%s>' % (session.remote_identity.display_name, identity) self.output.put('%s has taken the audio session out of hold\n' % identity) elif not self.ignore_local_unhold: if session is self.active_session: self.output.put('Session is taken out of hold\n') else: identity = str(session.remote_identity.uri) if session.remote_identity.display_name: identity = '"%s" <%s>' % (session.remote_identity.display_name, identity) self.output.put('Session %s is taken out of hold\n' % identity) else: self.ignore_local_unhold = False def _NH_SIPSessionNewProposal(self, notification): if notification.data.originator == 'remote': session = notification.sender accepted_types = ['video', 'audio'] if self.enable_video else ['audio'] audio_streams = [stream for stream in notification.data.proposed_streams if stream.type in accepted_types] if audio_streams: session.accept_proposal(audio_streams) else: session.reject_proposal(488) def _NH_AudioStreamGotDTMF(self, notification): notification_center = NotificationCenter() digit = notification.data.digit filename = 'sounds/dtmf_%s_tone.wav' % {'*': 'star', '#': 'pound'}.get(digit, digit) wave_player = WavePlayer(self.voice_audio_mixer, ResourcePath(filename).normalized) notification_center.add_observer(self, sender=wave_player) self.voice_audio_bridge.add(wave_player) wave_player.start() def _NH_RTPStreamDidChangeHoldState(self, notification): if notification.data.on_hold: if not self.hold_tone.is_active: self.hold_tone.start() else: on_hold_streams = [stream for stream in chain(*(session.streams for session in self.started_sessions)) if stream is not notification.sender and stream.on_hold] if not on_hold_streams and self.hold_tone.is_active: self.hold_tone.stop() def _NH_RTPStreamDidChangeRTPParameters(self, notification): stream = notification.sender self.output.put('Audio RTP parameters changed:\n') self.output.put('Audio stream using "%s" codec at %sHz\n' % (stream.codec, stream.sample_rate)) self.output.put('Audio RTP endpoints %s:%d <-> %s:%d\n' % (stream.local_rtp_address, stream.local_rtp_port, stream.remote_rtp_address, stream.remote_rtp_port)) if stream.encryption.active: self.output.put('RTP audio stream is encrypted using %s (%s)\n' % (stream.encryption.type, stream.encryption.cipher)) def _NH_AudioStreamDidStartRecordingAudio(self, notification): self.output.put('Recording audio to %s\n' % notification.data.filename) def _NH_AudioStreamDidStopRecordingAudio(self, notification): self.output.put('Stopped recording audio to %s\n' % notification.data.filename) def _NH_WavePlayerDidEnd(self, notification): notification_center = NotificationCenter() notification_center.remove_observer(self, sender=notification.sender) def _NH_WavePlayerDidFail(self, notification): notification_center = NotificationCenter() notification_center.remove_observer(self, sender=notification.sender) self.output.put('Failed to play %s: %s' % (notification.sender.filename, notification.data.error)) def _NH_DefaultAudioDeviceDidChange(self, notification): SIPApplication._NH_DefaultAudioDeviceDidChange(self, notification) if notification.data.changed_input and self.voice_audio_mixer.input_device=='system_default': self.output.put('Switched default input device to: %s\n' % self.voice_audio_mixer.real_input_device) if notification.data.changed_output and self.voice_audio_mixer.output_device=='system_default': self.output.put('Switched default output device to: %s\n' % self.voice_audio_mixer.real_output_device) if notification.data.changed_output and self.alert_audio_mixer.output_device=='system_default': self.output.put('Switched alert device to: %s\n' % self.alert_audio_mixer.real_output_device) def _NH_AudioDevicesDidChange(self, notification): old_devices = set(notification.data.old_devices) new_devices = set(notification.data.new_devices) added_devices = new_devices - old_devices removed_devices = old_devices - new_devices changed_input_device = self.voice_audio_mixer.real_input_device in removed_devices changed_output_device = self.voice_audio_mixer.real_output_device in removed_devices changed_alert_device = self.alert_audio_mixer.real_output_device in removed_devices SIPApplication._NH_AudioDevicesDidChange(self, notification) if added_devices: self.output.put('Added audio device(s): %s\n' % ', '.join(sorted(added_devices))) if removed_devices: self.output.put('Removed audio device(s): %s\n' % ', '.join(sorted(removed_devices))) if changed_input_device: self.output.put('Audio input device has been switched to: %s\n' % self.voice_audio_mixer.real_input_device) if changed_output_device: self.output.put('Audio output device has been switched to: %s\n' % self.voice_audio_mixer.real_output_device) if changed_alert_device: self.output.put('Audio alert device has been switched to: %s\n' % self.alert_audio_mixer.real_output_device) self.output.put('Available audio input devices: %s\n' % ', '.join(['None', 'system_default'] + sorted(self.engine.input_devices))) self.output.put('Available audio output devices: %s\n' % ', '.join(['None', 'system_default'] + sorted(self.engine.output_devices))) def _NH_RTPStreamICENegotiationDidSucceed(self, notification): self.output.put("ICE negotiation succeeded in %s\n" % notification.data.duration) def _NH_RTPStreamICENegotiationDidFail(self, notification): self.output.put("ICE negotiation failed: %s\n" % notification.data.reason) def _print_new_session(self): session = self.incoming_sessions[0] identity = str(session.remote_identity.uri) if session.remote_identity.display_name: identity = '"%s" <%s>' % (session.remote_identity.display_name, identity) video_streams = [stream for stream in session.proposed_streams if stream.type in ['video']] media_type = 'video' if video_streams else 'audio' self.output.put("Incoming %s session from '%s', do you want to accept? (y/n)\n" % (media_type, identity)) def parse_handle_call_option(option, opt_str, value, parser, name): try: value = parser.rargs[0] except IndexError: value = 0 else: if value == '' or value[0] == '-': value = 0 else: try: value = int(value) except ValueError: value = 0 else: del parser.rargs[0] setattr(parser.values, name, value) if __name__ == '__main__': description = 'This script can sit idle waiting for an incoming audio session, or initiate an outgoing audio session to a SIP address. The program will close the session and quit when Ctrl+D is pressed.' usage = '%prog [options] [user@domain]' parser = OptionParser(usage=usage, description=description) parser.print_usage = parser.print_help parser.add_option('-a', '--account', type='string', dest='account', help='The account name to use for any outgoing traffic. If not supplied, the default account will be used.', metavar='NAME') parser.add_option('-c', '--config-directory', type='string', dest='config_directory', help='The configuration directory to use. This overrides the default location.') parser.add_option('-s', '--trace-sip', action='store_true', dest='trace_sip', default=False, help='Dump the raw contents of incoming and outgoing SIP messages.') parser.add_option('-j', '--trace-pjsip', action='store_true', dest='trace_pjsip', default=False, help='Print PJSIP logging output.') parser.add_option('-n', '--trace-notifications', action='store_true', dest='trace_notifications', default=False, help='Print all notifications (disabled by default).') parser.add_option('-S', '--disable-sound', action='store_true', dest='disable_sound', default=False, help='Disables initializing the sound card.') parser.set_default('auto_answer_interval', None) parser.add_option('--auto-answer', action='callback', callback=parse_handle_call_option, callback_args=('auto_answer_interval',), help='Interval after which to answer an incoming session (disabled by default). If the option is specified but the interval is not, it defaults to 0 (accept the session as soon as it starts ringing).', metavar='[INTERVAL]') parser.add_option('-u', '--auto-answer-uris', type='string', dest='auto_answer_uris', default="", help='Optional list of SIP URIs for which auto-answer is allowed') parser.add_option('-i', '--external-id', type='string', dest='external_id', help='id used for call control from external application') parser.add_option('-v', '--spool-dir', type='string', dest='spool_dir', default=None, help='Spool dir for call control from external applications, default is /var/spool/sipclients/sessions') parser.add_option('-t', '--enable-default-devices', action='store_true', dest='enable_default_devices', help='Use default audio devices') parser.add_option('-V', '--enable-video', action='store_true', dest='enable_video', default=False, help='Enable video if camera is available') parser.set_default('auto_hangup_interval', None) parser.add_option('--auto-hangup', action='callback', callback=parse_handle_call_option, callback_args=('auto_hangup_interval',), help='Interval after which to hang up an established session (disabled by default). If the option is specified but the interval is not, it defaults to 0 (hangup the session as soon as it connects).', metavar='[INTERVAL]') parser.add_option('-b', '--batch', action='store_true', dest='batch_mode', default=False, help='Run the program in batch mode: reading input from the console is disabled and the option --auto-answer is implied. This is particularly useful when running this script in a non-interactive environment.') parser.add_option('-d', '--daemonize', action='store_true', dest='daemonize', default=False, help='Enable running this program as a deamon.') options, args = parser.parse_args() target = args[0] if args and not options.auto_answer_uris else None application = SIPAudioApplication() application.start(target, options) signal.signal(signal.SIGINT, signal.SIG_DFL) application.output.join() sleep(0.1) sys.exit(0 if application.success else 1) diff --git a/sip-message b/sip-message index daeae57..e10fc81 100755 --- a/sip-message +++ b/sip-message @@ -1,356 +1,357 @@ -#!/usr/bin/env python2 +#!/usr/bin/env python3 import atexit import os import select import signal import sys import termios from datetime import datetime from optparse import OptionParser from threading import Thread from time import sleep from application import log from application.notification import NotificationCenter, NotificationData from application.python.queue import EventQueue from sipsimple.core import FromHeader, Message, RouteHeader, SIPCoreError, SIPURI, ToHeader from sipsimple.account import Account, AccountManager, BonjourAccount from sipsimple.application import SIPApplication from sipsimple.configuration import ConfigurationError from sipsimple.configuration.settings import SIPSimpleSettings from sipsimple.core import Engine from sipsimple.lookup import DNSLookup from sipsimple.storage import FileStorage from sipclient.configuration import config_directory from sipclient.configuration.account import AccountExtension from sipclient.configuration.settings import SIPSimpleSettingsExtension from sipclient.log import Logger from sipclient.system import IPAddressMonitor class InputThread(Thread): def __init__(self, read_message, batch_mode): Thread.__init__(self) self.setDaemon(True) self.read_message = read_message self.batch_mode = batch_mode self._old_terminal_settings = None def start(self): atexit.register(self._termios_restore) Thread.start(self) def run(self): notification_center = NotificationCenter() if self.read_message: lines = [] try: while True: - lines.append(raw_input()) + lines.append(input()) except EOFError: message = '\n'.join(lines) notification_center.post_notification('SIPApplicationGotInputMessage', sender=self, data=NotificationData(message=message)) if not self.batch_mode: while True: chars = list(self._getchars()) while chars: char = chars.pop(0) if char == '\x1b': # escape if len(chars) >= 2 and chars[0] == '[' and chars[1] in ('A', 'B', 'C', 'D'): # one of the arrow keys char = char + chars.pop(0) + chars.pop(0) notification_center.post_notification('SIPApplicationGotInput', sender=self, data=NotificationData(input=char)) def stop(self): self._termios_restore() def _termios_restore(self): if self._old_terminal_settings is not None: termios.tcsetattr(sys.stdin.fileno(), termios.TCSADRAIN, self._old_terminal_settings) def _getchars(self): fd = sys.stdin.fileno() if os.isatty(fd): self._old_terminal_settings = termios.tcgetattr(fd) new = termios.tcgetattr(fd) new[3] = new[3] & ~termios.ICANON & ~termios.ECHO - new[6][termios.VMIN] = '\000' + new[6][termios.VMIN] = b'\000' try: termios.tcsetattr(fd, termios.TCSADRAIN, new) if select.select([fd], [], [], None)[0]: return sys.stdin.read(4192) finally: self._termios_restore() else: return os.read(fd, 4192) class SIPMessageApplication(SIPApplication): def __init__(self): self.account = None self.options = None self.target = None self.routes = [] self.registration_succeeded = False self.input = None self.output = None self.ip_address_monitor = IPAddressMonitor() self.logger = None def _write(self, message): - if isinstance(message, unicode): - message = message.encode(sys.getfilesystemencoding()) + print('message %s %s' % (message, type(message))) + #if isinstance(message, str): + # message = message.encode(sys.getfilesystemencoding()) sys.stdout.write(message) sys.stdout.flush() def start(self, target, options): notification_center = NotificationCenter() self.options = options self.message = options.message self.target = target self.input = InputThread(read_message=self.target is not None and options.message is None, batch_mode=options.batch_mode) self.output = EventQueue(self._write) self.logger = Logger(sip_to_stdout=options.trace_sip, pjsip_to_stdout=options.trace_pjsip, notifications_to_stdout=options.trace_notifications) notification_center.add_observer(self, sender=self) notification_center.add_observer(self, sender=self.input) notification_center.add_observer(self, name='SIPEngineGotMessage') if self.input: self.input.start() self.output.start() log.level.current = log.level.WARNING # get rid of twisted messages Account.register_extension(AccountExtension) BonjourAccount.register_extension(AccountExtension) SIPSimpleSettings.register_extension(SIPSimpleSettingsExtension) try: SIPApplication.start(self, FileStorage(options.config_directory or config_directory)) - except ConfigurationError, e: + except ConfigurationError as e: self.output.put("Failed to load sipclient's configuration: %s\n" % str(e)) self.output.put("If an old configuration file is in place, delete it or move it and recreate the configuration using the sip_settings script.\n") self.output.stop() def _NH_SIPApplicationWillStart(self, notification): account_manager = AccountManager() notification_center = NotificationCenter() settings = SIPSimpleSettings() for account in account_manager.iter_accounts(): if isinstance(account, Account): account.sip.register = False account.presence.enabled = False account.xcap.enabled = False account.message_summary.enabled = False if self.options.account is None: self.account = account_manager.default_account else: possible_accounts = [account for account in account_manager.iter_accounts() if self.options.account in account.id and account.enabled] if len(possible_accounts) > 1: self.output.put('More than one account exists which matches %s: %s\n' % (self.options.account, ', '.join(sorted(account.id for account in possible_accounts)))) self.output.stop() self.stop() return elif len(possible_accounts) == 0: self.output.put('No enabled account that matches %s was found. Available and enabled accounts: %s\n' % (self.options.account, ', '.join(sorted(account.id for account in account_manager.get_accounts() if account.enabled)))) self.output.stop() self.stop() return else: self.account = possible_accounts[0] if isinstance(self.account, Account) and self.target is None: self.account.sip.register = True self.account.presence.enabled = False self.account.xcap.enabled = False self.account.message_summary.enabled = False notification_center.add_observer(self, sender=self.account) self.output.put('Using account %s\n' % self.account.id) self.logger.start() if settings.logs.trace_sip and self.logger._siptrace_filename is not None: self.output.put('Logging SIP trace to file "%s"\n' % self.logger._siptrace_filename) if settings.logs.trace_pjsip and self.logger._pjsiptrace_filename is not None: self.output.put('Logging PJSIP trace to file "%s"\n' % self.logger._pjsiptrace_filename) if settings.logs.trace_notifications and self.logger._notifications_filename is not None: self.output.put('Logging notifications trace to file "%s"\n' % self.logger._notifications_filename) def _NH_SIPApplicationDidStart(self, notification): notification_center = NotificationCenter() settings = SIPSimpleSettings() engine = Engine() engine.trace_sip = self.logger.sip_to_stdout or settings.logs.trace_sip engine.log_level = settings.logs.pjsip_level if (self.logger.pjsip_to_stdout or settings.logs.trace_pjsip) else 0 self.ip_address_monitor.start() if isinstance(self.account, BonjourAccount) and self.target is None: for transport in settings.sip.transport_list: try: self.output.put('Listening on: %s\n' % self.account.contact[transport]) except KeyError: pass if self.target is not None: if '@' not in self.target: self.target = '%s@%s' % (self.target, self.account.id.domain) if not self.target.startswith('sip:') and not self.target.startswith('sips:'): self.target = 'sip:' + self.target try: self.target = SIPURI.parse(self.target) except SIPCoreError: self.output.put('Illegal SIP URI: %s\n' % self.target) self.stop() if self.message is None: self.output.put('Press Ctrl+D on an empty line to end input and send the MESSAGE request.\n') else: settings = SIPSimpleSettings() lookup = DNSLookup() notification_center.add_observer(self, sender=lookup) if isinstance(self.account, Account) and self.account.sip.outbound_proxy is not None: uri = SIPURI(host=self.account.sip.outbound_proxy.host, port=self.account.sip.outbound_proxy.port, parameters={'transport': self.account.sip.outbound_proxy.transport}) elif isinstance(self.account, Account) and self.account.sip.always_use_my_proxy: uri = SIPURI(host=self.account.id.domain) else: uri = self.target lookup.lookup_sip_proxy(uri, settings.sip.transport_list) else: self.output.put('Press Ctrl+D to stop the program.\n') def _NH_SIPApplicationWillEnd(self, notification): self.ip_address_monitor.stop() def _NH_SIPApplicationDidEnd(self, notification): if self.input: self.input.stop() self.output.stop() self.output.join() def _NH_SIPApplicationGotInput(self, notification): if notification.data.input == '\x04': self.stop() def _NH_SIPApplicationGotInputMessage(self, notification): if not notification.data.message: self.stop() else: notification_center = NotificationCenter() settings = SIPSimpleSettings() self.message = notification.data.message lookup = DNSLookup() notification_center.add_observer(self, sender=lookup) if isinstance(self.account, Account) and self.account.sip.outbound_proxy is not None: uri = SIPURI(host=self.account.sip.outbound_proxy.host, port=self.account.sip.outbound_proxy.port, parameters={'transport': self.account.sip.outbound_proxy.transport}) elif isinstance(self.account, Account) and self.account.sip.always_use_my_proxy: uri = SIPURI(host=self.account.id.domain) else: uri = self.target lookup.lookup_sip_proxy(uri, settings.sip.transport_list) def _NH_SIPEngineGotException(self, notification): self.output.put('An exception occured within the SIP core:\n%s\n' % notification.data.traceback) def _NH_SIPAccountRegistrationDidSucceed(self, notification): if self.registration_succeeded: return contact_header = notification.data.contact_header contact_header_list = notification.data.contact_header_list expires = notification.data.expires registrar = notification.data.registrar message = '%s Registered contact "%s" for sip:%s at %s:%d;transport=%s (expires in %d seconds).\n' % (datetime.now().replace(microsecond=0), contact_header.uri, self.account.id, registrar.address, registrar.port, registrar.transport, expires) if len(contact_header_list) > 1: message += 'Other registered contacts:\n%s\n' % '\n'.join([' %s (expires in %s seconds)' % (str(other_contact_header.uri), other_contact_header.expires) for other_contact_header in contact_header_list if other_contact_header.uri != notification.data.contact_header.uri]) self.output.put(message) self.registration_succeeded = True def _NH_SIPAccountRegistrationDidFail(self, notification): self.output.put('%s Failed to register contact for sip:%s: %s (retrying in %.2f seconds)\n' % (datetime.now().replace(microsecond=0), self.account.id, notification.data.error, notification.data.retry_after)) self.registration_succeeded = False def _NH_SIPAccountRegistrationDidEnd(self, notification): self.output.put('%s Registration ended.\n' % datetime.now().replace(microsecond=0)) def _NH_DNSLookupDidSucceed(self, notification): self.routes = notification.data.result self._send_message() def _NH_DNSLookupDidFail(self, notification): self.output.put('DNS lookup failed: %s\n' % notification.data.error) self.stop() def _NH_SIPEngineGotMessage(self, notification): content_type = notification.data.content_type if content_type not in ('text/plain', 'text/html'): return from_header = FromHeader.new(notification.data.from_header) from_header.parameters = {} from_header.uri.parameters = {} identity = str(from_header.uri) if from_header.display_name: identity = '"%s" <%s>' % (from_header.display_name, identity) body = notification.data.body self.output.put("Got MESSAGE from '%s', Content-Type: %s\n%s\n" % (identity, content_type, body)) def _NH_SIPMessageDidSucceed(self, notification): self.output.put('MESSAGE was accepted by remote party\n') self.stop() def _NH_SIPMessageDidFail(self, notification): notification_center = NotificationCenter() notification_center.remove_observer(self, sender=notification.sender) self.output.put('Could not deliver MESSAGE: %d %s\n' % (notification.data.code, notification.data.reason)) self._send_message() def _send_message(self): notification_center = NotificationCenter() if self.routes: route = self.routes.pop(0) identity = str(self.account.uri) if self.account.display_name: identity = '"%s" <%s>' % (self.account.display_name, identity) self.output.put("Sending MESSAGE from '%s' to '%s' using proxy %s\n" % (identity, self.target, route)) self.output.put('Press Ctrl+D to stop the program.\n') message_request = Message(FromHeader(self.account.uri, self.account.display_name), ToHeader(self.target), RouteHeader(route.uri), 'text/plain', self.message, credentials=self.account.credentials) notification_center.add_observer(self, sender=message_request) message_request.send() else: self.output.put('No more routes to try. Aborting.\n') self.stop() if __name__ == '__main__': description = "This script will either sit idle waiting for an incoming MESSAGE request, or send a MESSAGE request to the specified SIP target. In outgoing mode the program will read the contents of the messages to be sent from standard input, Ctrl+D signalling EOF as usual. In listen mode the program will quit when Ctrl+D is pressed." usage = '%prog [options] [user@domain]' parser = OptionParser(usage=usage, description=description) parser.print_usage = parser.print_help parser.add_option('-a', '--account', type='string', dest='account', help='The account name to use for any outgoing traffic. If not supplied, the default account will be used.', metavar='NAME') parser.add_option('-c', '--config-directory', type='string', dest='config_directory', help='The configuration directory to use. This overrides the default location.') parser.add_option('-s', '--trace-sip', action='store_true', dest='trace_sip', default=False, help='Dump the raw contents of incoming and outgoing SIP messages.') parser.add_option('-j', '--trace-pjsip', action='store_true', dest='trace_pjsip', default=False, help='Print PJSIP logging output.') parser.add_option('-n', '--trace-notifications', action='store_true', dest='trace_notifications', default=False, help='Print all notifications (disabled by default).') parser.add_option('-b', '--batch', action='store_true', dest='batch_mode', default=False, help='Run the program in batch mode: reading control input from the console is disabled. This is particularly useful when running this script in a non-interactive environment.') parser.add_option('-m', '--message', type='string', dest='message', help='Contents of the message to send. This disables reading the message from standard input.') options, args = parser.parse_args() target = args[0] if args else None application = SIPMessageApplication() application.start(target, options) signal.signal(signal.SIGINT, signal.SIG_DFL) application.output.join() sleep(0.1) diff --git a/sip-publish-presence b/sip-publish-presence index 59e5022..b19452e 100755 --- a/sip-publish-presence +++ b/sip-publish-presence @@ -1,837 +1,837 @@ -#!/usr/bin/env python2 +#!/usr/bin/env python3 import hashlib import os import random import select import sys import termios import traceback import uuid from collections import deque from datetime import datetime from optparse import OptionParser from threading import RLock, Thread from time import time from application import log from application.notification import IObserver, NotificationCenter, NotificationData from application.python.queue import EventQueue from eventlib.twistedutil import join_reactor from twisted.internet import reactor from twisted.internet.error import ReactorNotRunning -from zope.interface import implements +from zope.interface import implementer from sipsimple.core import Engine, FromHeader, Publication, PublicationError, RouteHeader, SIPCoreError, SIPURI from sipsimple.account import Account, AccountManager, BonjourAccount from sipsimple.application import SIPApplication from sipsimple.configuration import ConfigurationError, ConfigurationManager from sipsimple.configuration.settings import SIPSimpleSettings from sipsimple.lookup import DNSLookup from sipsimple.payloads import BuilderError from sipsimple.payloads.pidf import Contact, Device, DeviceInfo, DMNote, DeviceTimestamp, Person, PIDFNote, PersonTimestamp, PIDF, Service, ServiceTimestamp, Status from sipsimple.payloads.rpid import Activities, ActivityRegistry, Mood, MoodRegistry, RPIDNote, TimeOffset from sipsimple.storage import FileStorage from sipsimple.threading import run_in_twisted_thread from sipclient.configuration import config_directory from sipclient.configuration.account import AccountExtension from sipclient.configuration.settings import SIPSimpleSettingsExtension from sipclient.log import Logger class KeyBinding(object): def __init__(self, description, handler): self.description = description self.handler = handler class Prompt(str): """Used to make a distinction between normal strings written to the console and prompts.""" class Menu(object): key_bindings = {} def __init__(self, interface): self.interface = interface def print_prompt(self): buf = ['Commands:'] - for key, binding in self.key_bindings.items(): + for key, binding in list(self.key_bindings.items()): buf.append(' %s: %s' % (key, binding.description)) self.interface.application.output.put('\n'+'\n'.join(buf)+'\n') def _exit(self): self.interface.exit_menu() def _exit_program(self): self.interface.application.stop() # Mood manipulation menu class MoodMenu(Menu): def _show_moods(self): person = self.interface.application.person buf = ['Moods:'] if person.mood is not None: for m in list(person.mood): buf.append(' %s' % str(m)) self.interface.application.output.put('\n'.join(buf)) def _add_mood(self): person = self.interface.application.person buf = ['Possible moods:'] values = list(MoodRegistry.names) values.sort() max_len = max(len(s) for s in values)+2 format = ' %%02d) %%-%ds' % max_len num_line = 80/(max_len+5) i = 0 text = '' for val in values: text += format % (i+1, val) i += 1 if i % num_line == 0: buf.append(text) text = '' buf.append(text) self.interface.application.output.put('\n'.join(buf)+'\n') m = self.interface.getstring('Select mood to add (any non-number will return)') try: m = int(m) - if m not in xrange(len(values)): + if m not in range(len(values)): raise ValueError except ValueError: self.interface.application.output.put('Invalid input') else: if person.mood is None: person.mood = Mood() person.mood.add(values[m-1]) person.timestamp = PersonTimestamp(datetime.now()) self.interface.application.publish() self.interface.application.output.put('Mood added') self.interface.show_top_level() def _del_mood(self): person = self.interface.application.person if person.mood is None: self.interface.application.output.put('There is no current mood set') self.print_prompt() return buf = ['Current moods:'] values = list(person.mood) values.sort() max_len = max(len(s) for s in values)+2 format = " %%02d) %%-%ds" % max_len num_line = 80/(max_len+5) i = 0 text = '' for val in values: text += format % (i+1, val) i += 1 if i % num_line == 0: buf.append(text) text = '' buf.append(text) self.interface.application.output.put('\n'.join(buf)+'\n') m = self.interface.getstring('Select mood to delete (any non-number will return)') try: m = int(m) except ValueError: self.interface.application.output.put('Invalid input') else: person.mood.remove(values[m-1]) person.timestamp = PersonTimestamp(datetime.now()) self.interface.application.publish() self.interface.application.output.put('Mood deleted') self.interface.show_top_level() def _clear_moods(self): person = self.interface.application.person if person.mood is None: self.interface.application.output.put('There is no current mood set') self.print_prompt() return person.mood = None person.timestamp = PersonTimestamp(datetime.now()) self.interface.application.publish() self.interface.application.output.put('Mood information cleared') self.interface.show_top_level() def _set_note(self): person = self.interface.application.person if person.mood is not None and person.mood.notes: - a_note = iter(person.mood.notes).next() + a_note = next(iter(person.mood.notes)) self.interface.application.output.put('Current note: %s' % a_note) note = self.interface.getstring('Set note (press return to delete)') if note == '': person.mood.notes.clear() self.interface.application.output.put('Note removed') else: if person.mood is None: person.mood = Mood() person.mood.notes.add(RPIDNote(note, lang='en')) self.interface.application.output.put('Note set') person.timestamp = PersonTimestamp(datetime.now()) self.interface.application.publish() self.interface.show_top_level() def _set_random(self): person = self.interface.application.person values = list(value for value in MoodRegistry.names if value != 'unknown') random.shuffle(values) if person.mood is None: person.mood = Mood() else: person.mood.clear() values = values[:3] for mood in values: person.mood.add(mood) person.timestamp = PersonTimestamp(datetime.now()) self.interface.application.publish() self.interface.application.output.put('You are now ' + ', '.join(values)) self.interface.show_top_level() key_bindings = {'s': KeyBinding(description='show current moods', handler=_show_moods), 'a': KeyBinding(description='add a mood', handler=_add_mood), 'd': KeyBinding(description='delete a mood', handler=_del_mood), 'c': KeyBinding(description='clear all mood data', handler=_clear_moods), 'n': KeyBinding(description='set mood note', handler=_set_note), 'r': KeyBinding(description='set random mood', handler=_set_random), 'x': KeyBinding(description='exit to upper level menu', handler=Menu._exit), 'q': KeyBinding(description='quit program', handler=Menu._exit_program)} # Activities manipulation menu class ActivitiesMenu(Menu): def _show_activity(self): person = self.interface.application.person buf = ["Activity:"] if person.activities is not None: for a in list(person.activities): buf.append(" %s" % str(a)) self.interface.application.output.put('\n'.join(buf)) def _set_activity(self): person = self.interface.application.person buf = ["Possible activities:"] values = list(ActivityRegistry.names) values.sort() max_len = max(len(s) for s in values)+2 format = " %%02d) %%-%ds" % max_len num_line = 80/(max_len+5) i = 0 text = '' for val in values: text += format % (i+1, val) i += 1 if i % num_line == 0: buf.append(text) text = '' self.interface.application.output.put('\n'.join(buf)+'\n') a = self.interface.getstring('Select activity to add (any non-number will return)') try: a = int(a) - if a-1 not in xrange(len(values)): + if a-1 not in range(len(values)): raise ValueError except ValueError: self.interface.application.output.put('Invalid input') else: if person.activities is None: person.activities = Activities() else: person.activities.clear() person.activities.add(values[a-1]) person.timestamp = PersonTimestamp(datetime.now()) self.interface.application.publish() self.interface.application.output.put('Activity set') self.interface.show_top_level() def _del_activity(self): person = self.interface.application.person if person.activities is None or len(person.activities.values) == 0: self.interface.application.output.put('There is no current activity set') return person.activities.clear() person.activities.add('unknown') person.timestamp = PersonTimestamp(datetime.now()) self.interface.application.publish() self.interface.application.output.put('Activity deleted') self.interface.show_top_level() def _clear_activity(self): person = self.interface.application.person if person.activities is None: self.interface.application.output.put('There is no current activity set') return person.activities = None person.timestamp = PersonTimestamp(datetime.now()) self.interface.application.publish() self.interface.application.output.put('Activities information cleared') self.interface.show_top_level() def _set_note(self): person = self.interface.application.person if person.activities is not None and person.activities.notes: - a_note = iter(person.activities.notes).next() + a_note = next(iter(person.activities.notes)) self.interface.application.output.put('Current note: %s' % a_note) note = self.interface.getstring('Set note (press return to delete)') if note == '': person.activities.notes.clear() self.interface.application.output.put('Note deleted') else: if person.activities is None: person.activities = Activities() person.activities.add('unknown') person.activities.notes.add(RPIDNote(note, lang='en')) self.interface.application.output.put('Note set') person.timestamp = PersonTimestamp(datetime.now()) self.interface.application.publish() self.interface.show_top_level() def _set_random(self): person = self.interface.application.person values = list(value for value in ActivityRegistry.names if value != 'unknown') activity = random.choice(values) if person.activities is None: person.activities = Activities() else: person.activities.clear() person.activities.add(activity) person.timestamp = PersonTimestamp(datetime.now()) self.interface.application.publish() self.interface.application.output.put('You are now %s' % activity) self.interface.show_top_level() key_bindings = {'s': KeyBinding(description='show current activity', handler=_show_activity), 'a': KeyBinding(description='set activity', handler=_set_activity), 'd': KeyBinding(description='delete activity', handler=_del_activity), 'c': KeyBinding(description='clear all acitivity data', handler=_clear_activity), 'n': KeyBinding(description='set activity note', handler=_set_note), 'r': KeyBinding(description='set random activity', handler=_set_random), 'x': KeyBinding(description='exit to upper level menu', handler=Menu._exit), 'q': KeyBinding(description='quit program', handler=Menu._exit_program)} # Extended status manipulation menu class ExtendedStatusMenu(Menu): def _show_extended_status(self): service = self.interface.application.service buf = ["Extended status:"] if service.status.extended is not None: buf.append(" %s" % str(service.status.extended)) self.interface.application.output.put('\n'.join(buf)) def _set_extended_status(self): service = self.interface.application.service buf = ["Possible statuses:"] values = ['available', 'away', 'extended-away', 'busy', 'offline'] max_len = max(len(s) for s in values)+2 format = " %%02d) %%-%ds" % max_len num_line = 80/(max_len+5) i = 0 text = '' for val in values: text += format % (i+1, val) i += 1 if i % num_line == 0: buf.append(text) text = '' buf.append(text) self.interface.application.output.put('\n'.join(buf)+'\n') a = self.interface.getstring('Select status (any non-number will return)') try: a = int(a) - if a-1 not in xrange(len(values)): + if a-1 not in range(len(values)): raise ValueError except ValueError: self.interface.application.output.put('Invalid input') else: status = values[a-1] if status == 'offline': service.status.basic = 'closed' else: service.status.basic = 'open' service.status.extended = status self.interface.application.publish() self.interface.application.output.put('Extended atatus set') self.interface.show_top_level() def _del_extended_status(self): self.interface.application.service.status.extended = None self.interface.application.publish() self.interface.application.output.put('Activity deleted') self.interface.show_top_level() key_bindings = {'s': KeyBinding(description='show current extended status', handler=_show_extended_status), 'e': KeyBinding(description='set extended status', handler=_set_extended_status), 'd': KeyBinding(description='delete extended status', handler=_del_extended_status), 'x': KeyBinding(description='exit to upper level menu', handler=Menu._exit), 'q': KeyBinding(description='quit program', handler=Menu._exit_program)} class TopLevelMenu(Menu): def _show_pidf(self): try: pidf = self.interface.application.pidf.toxml(pretty_print=True) - except BuilderError, e: - print "PIDF as currently defined is invalid: %s" % str(e) + except BuilderError as e: + print("PIDF as currently defined is invalid: %s" % str(e)) except: traceback.print_exc() else: self.interface.application.output.put(pidf) self.print_prompt() def _set_mood_info(self): mood_menu = MoodMenu(self.interface) self.interface.add_menu(mood_menu) def _set_activity_info(self): activities_menu = ActivitiesMenu(self.interface) self.interface.add_menu(activities_menu) def _toggle_basic(self): service = self.interface.application.service if service.status.basic == 'open': service.status.basic = 'closed' else: service.status.basic = 'open' service.timestamp = ServiceTimestamp(datetime.now()) self.interface.application.output.put("Your basic status is now '%s'" % service.status.basic) self.interface.application.publish() self.print_prompt() def _set_extended_status(self): extended_status_menu = ExtendedStatusMenu(self.interface) self.interface.add_menu(extended_status_menu) def _set_note(self): service = self.interface.application.service if service.notes: - a_note = iter(service.notes).next() + a_note = next(iter(service.notes)) self.interface.application.output.put('Current note: %s' % a_note) note = self.interface.getstring('Set note (press return to delete)') if note == '': if service.notes: service.notes.clear() self.interface.application.output.put('Note removed') else: service.notes.add(PIDFNote(note, lang='en')) self.interface.application.output.put('Note added') self.interface.application.publish() self.print_prompt() key_bindings = {'s': KeyBinding(description='show PIDF', handler=_show_pidf), 'm': KeyBinding(description='set mood information', handler=_set_mood_info), 'a': KeyBinding(description='set activities information', handler=_set_activity_info), 'b': KeyBinding(description='toggle basic status', handler=_toggle_basic), 'e': KeyBinding(description='set extended status', handler=_set_extended_status), 'n': KeyBinding(description='set note', handler=_set_note), 'q': KeyBinding(description='quit program', handler=Menu._exit_program)} class UserInterface(Thread): def __init__(self, application): Thread.__init__(self) self.application = application self.daemon = True self.menu_stack = deque([TopLevelMenu(self)]) self._old_terminal_settings = None def run(self): self.menu_stack[-1].print_prompt() notification_center = NotificationCenter() while True: for char in self._getchars(): menu = self.menu_stack[-1] if char == '\x04': self.application.stop() return elif char in menu.key_bindings: menu.key_bindings[char].handler(menu) else: notification_center.post_notification('SAInputWasReceived', sender=self, data=NotificationData(input=char)) def stop(self): self._termios_restore() def add_menu(self, menu): self.menu_stack.append(menu) menu.print_prompt() def show_top_level(self): main = self.menu_stack[0] self.menu_stack.clear() self.menu_stack.append(main) main.print_prompt() def exit_menu(self): if len(self.menu_stack) > 1: self.menu_stack.pop() self.menu_stack[-1].print_prompt() def getstring(self, prompt='selection'): self.application.output.put(Prompt(prompt)) return sys.stdin.readline().strip() def _termios_restore(self): if self._old_terminal_settings is not None: termios.tcsetattr(sys.stdin.fileno(), termios.TCSADRAIN, self._old_terminal_settings) def _getchars(self): fd = sys.stdin.fileno() if os.isatty(fd): self._old_terminal_settings = termios.tcgetattr(fd) new = termios.tcgetattr(fd) new[3] = new[3] & ~termios.ICANON & ~termios.ECHO - new[6][termios.VMIN] = '\000' + new[6][termios.VMIN] = b'\000' try: termios.tcsetattr(fd, termios.TCSADRAIN, new) if select.select([fd], [], [], None)[0]: return sys.stdin.read(4192) finally: self._termios_restore() else: return os.read(fd, 4192) +@implementer(IObserver) class PublicationApplication(object): - implements(IObserver) def __init__(self, account_name, trace_sip, trace_pjsip, trace_notifications): self.account_name = account_name self.interface = UserInterface(self) self.output = EventQueue(self._output_handler) self.logger = Logger(sip_to_stdout=trace_sip, pjsip_to_stdout=trace_pjsip, notifications_to_stdout=trace_notifications) self.lookup = DNSLookup() self.publication_lock = RLock() self.success = False self.account = None self.publication = None self.pidf = None self.service = None self.person = None self.device = None self.stopping = False self.publishing = False self._publication_routes = None self._publication_timeout = 0.0 self._publication_wait = 0.5 account_manager = AccountManager() engine = Engine() notification_center = NotificationCenter() notification_center.add_observer(self, sender=account_manager) notification_center.add_observer(self, sender=engine) notification_center.add_observer(self, sender=self.interface) notification_center.add_observer(self, sender=self.lookup) log.level.current = log.level.WARNING def run(self): account_manager = AccountManager() configuration = ConfigurationManager() engine = Engine() notification_center = NotificationCenter() # start output thread self.output.start() # startup configuration Account.register_extension(AccountExtension) BonjourAccount.register_extension(AccountExtension) SIPSimpleSettings.register_extension(SIPSimpleSettingsExtension) SIPApplication.storage = FileStorage(config_directory) try: configuration.start() - except ConfigurationError, e: + except ConfigurationError as e: raise RuntimeError("Failed to load sipclient's configuration: %s\nIf an old configuration file is in place, delete it or move it and recreate the configuration using the sip_settings script." % str(e)) account_manager.load() if self.account_name is None: self.account = account_manager.default_account else: possible_accounts = [account for account in account_manager.iter_accounts() if self.account_name in account.id and account.enabled] if len(possible_accounts) > 1: raise RuntimeError("More than one account exists which matches %s: %s" % (self.account_name, ", ".join(sorted(account.id for account in possible_accounts)))) if len(possible_accounts) == 0: raise RuntimeError("No enabled account that matches %s was found. Available and enabled accounts: %s" % (self.account_name, ", ".join(sorted(account.id for account in account_manager.get_accounts() if account.enabled)))) self.account = possible_accounts[0] if self.account is None: raise RuntimeError("Unknown account %s. Available enabled accounts: %s" % (self.account_name, ', '.join(sorted(account.id for account in account_manager.iter_accounts() if account.enabled)))) elif self.account == BonjourAccount(): raise RuntimeError("Cannot use bonjour account to publish presence information") elif not self.account.presence.enabled: raise RuntimeError("Presence is not enabled for account %s" % self.account.id) for account in account_manager.iter_accounts(): if account == self.account: account.sip.register = False else: account.enabled = False self.output.put('Using account %s' % self.account.id) settings = SIPSimpleSettings() # start logging self.logger.start() # start the engine engine.start( udp_port=settings.sip.udp_port if "udp" in settings.sip.transport_list else None, tcp_port=settings.sip.tcp_port if "tcp" in settings.sip.transport_list else None, tls_port=settings.sip.tls_port if "tls" in settings.sip.transport_list else None, tls_verify_server=self.account.tls.verify_server, tls_ca_file=os.path.expanduser(settings.tls.ca_list) if settings.tls.ca_list else None, tls_cert_file=os.path.expanduser(self.account.tls.certificate) if self.account.tls.certificate else None, tls_privkey_file=os.path.expanduser(self.account.tls.certificate) if self.account.tls.certificate else None, user_agent=settings.user_agent, sample_rate=settings.audio.sample_rate, rtp_port_range=(settings.rtp.port_range.start, settings.rtp.port_range.end), trace_sip=settings.logs.trace_sip or self.logger.sip_to_stdout, log_level=settings.logs.pjsip_level if (settings.logs.trace_pjsip or self.logger.pjsip_to_stdout) else 0 ) # initialize pidf self.pidf = PIDF(entity=self.account.id) # entity will be determined when account is selected # initialize top level elements device_id = settings.instance_id self.service = Service("ID-"+str(uuid.UUID(hex=settings.instance_id))) self.service.status = Status(basic='open') self.service.status.extended = 'available' contact = "sip:%s;gr=%s" % (self.account.id, device_id) self.service.contact = Contact(contact) self.service.contact.priority = 0 self.service.device_id = device_id self.service.device_info = DeviceInfo(device_id) self.service.timestamp = ServiceTimestamp(datetime.now()) self.pidf.add(self.service) - self.person = Person("ID-"+hashlib.md5(self.account.id).hexdigest()) + self.person = Person("ID-"+hashlib.md5(self.account.id.encode()).hexdigest()) self.person.time_offset = TimeOffset() self.person.timestamp = PersonTimestamp(datetime.now()) self.pidf.add(self.person) - self.device = Device(''.join(chr(random.randint(97, 122)) for i in xrange(8))) + self.device = Device(''.join(chr(random.randint(97, 122)) for i in range(8))) self.device.device_id = device_id self.device.notes.add(DMNote('Powered by %s' % settings.user_agent, lang='en')) self.device.timestamp = DeviceTimestamp(datetime.now()) self.pidf.add(self.device) # start the interface thread self.interface.start() # initialize publication object self.publication = Publication(FromHeader(self.account.uri, self.account.display_name), "presence", "application/pidf+xml", credentials=self.account.credentials, duration=self.account.sip.publish_interval) notification_center.add_observer(self, sender=self.publication) reactor.callLater(0, self.publish) # start twisted try: reactor.run() finally: self.interface.stop() # stop the output self.output.stop() self.output.join() self.logger.stop() return 0 if self.success else 1 def stop(self): self.stopping = True if self.publication is not None: self.service.status.basic = 'closed' self.service.status.extended = 'offline' self.publish() else: engine = Engine() engine.stop() def print_help(self): message = 'Available control keys:\n' message += ' t: toggle SIP trace on the console\n' message += ' j: toggle PJSIP trace on the console\n' message += ' n: toggle notifications trace on the console\n' message += ' Ctrl-d: quit the program\n' message += ' ?: display this help message\n' self.output.put('\n'+message) def publish(self): with self.publication_lock: if self.publishing: return settings = SIPSimpleSettings() self._publication_timeout = time() + 30 if self.account.sip.outbound_proxy is not None: uri = SIPURI(host=self.account.sip.outbound_proxy.host, port=self.account.sip.outbound_proxy.port, parameters={'transport': self.account.sip.outbound_proxy.transport}) else: uri = SIPURI(host=self.account.id.domain) self.lookup.lookup_sip_proxy(uri, settings.sip.transport_list) self.publishing = True def unpublish(self): try: self.publication.end(timeout=1) return except PublicationError: pass engine = Engine() engine.stop() def handle_notification(self, notification): handler = getattr(self, '_NH_%s' % notification.name, None) if handler is not None: handler(notification) def _NH_SIPPublicationDidSucceed(self, notification): with self.publication_lock: self._publication_routes = None self._publication_wait = 0.5 self.success = True self.publishing = False if self.stopping: self.unpublish() return def _NH_SIPPublicationDidFail(self, notification): with self.publication_lock: self.success = False self.output.put('Publishing failed: %d %s' % (notification.data.code, notification.data.reason)) if notification.data.code in (401, 403, 407): self.publishing = False self.stop() else: if self.stopping: self.unpublish() return if not self._publication_routes or time() > self._publication_timeout: self._publication_wait = min(self._publication_wait*2, 30) timeout = random.uniform(self._publication_wait, 2*self._publication_wait) reactor.callFromThread(reactor.callLater, timeout, self.publish) self.publishing = False else: route = self._publication_routes.popleft() self._do_publish(route) def _NH_SIPPublicationWillExpire(self, notification): # For now, just re-publish the whole document instead of sending a refresh self.publish() def _NH_SIPPublicationDidNotEnd(self, notification): self.success = False engine = Engine() engine.stop() def _NH_SIPPublicationDidEnd(self, notification): if notification.data.expired: self.output.put('Publication expired') else: self.output.put('Unpublished') if self.stopping: self.success = True engine = Engine() engine.stop() else: self.publish() def _NH_DNSLookupDidSucceed(self, notification): with self.publication_lock: self._publication_routes = deque(notification.data.result) route = self._publication_routes.popleft() self._do_publish(route) def _NH_DNSLookupDidFail(self, notification): with self.publication_lock: self.output.put('DNS lookup failed: %s' % notification.data.error) timeout = random.uniform(1.0, 2.0) reactor.callLater(timeout, self.publish) self.publishing = False def _NH_SAInputWasReceived(self, notification): engine = Engine() settings = SIPSimpleSettings() key = notification.data.input if key == 't': self.logger.sip_to_stdout = not self.logger.sip_to_stdout engine.trace_sip = self.logger.sip_to_stdout or settings.logs.trace_sip self.output.put('SIP tracing to console is now %s.' % ('activated' if self.logger.sip_to_stdout else 'deactivated')) elif key == 'j': self.logger.pjsip_to_stdout = not self.logger.pjsip_to_stdout engine.log_level = settings.logs.pjsip_level if (self.logger.pjsip_to_stdout or settings.logs.trace_pjsip) else 0 self.output.put('PJSIP tracing to console is now %s.' % ('activated' if self.logger.pjsip_to_stdout else 'deactivated')) elif key == 'n': self.logger.notifications_to_stdout = not self.logger.notifications_to_stdout self.output.put('Notification tracing to console is now %s.' % ('activated' if self.logger.notifications_to_stdout else 'deactivated')) elif key == '?': self.print_help() @run_in_twisted_thread def _NH_SIPEngineDidEnd(self, notification): self._stop_reactor() @run_in_twisted_thread def _NH_SIPEngineDidFail(self, notification): self.output.put('Engine failed.') self._stop_reactor() def _NH_SIPEngineGotException(self, notification): self.output.put('An exception occured within the SIP core:\n'+notification.data.traceback) def _stop_reactor(self): try: reactor.stop() except ReactorNotRunning: pass def _do_publish(self, route): try: route_header = RouteHeader(route.uri) self.publication.publish(self.pidf.toxml(), route_header, timeout=5) - except BuilderError, e: + except BuilderError as e: self.output.put("PIDF as currently defined is invalid: %s" % str(e)) self.publishing = False except: traceback.print_exc() self.publishing = False def _output_handler(self, event): if isinstance(event, Prompt): sys.stdout.write(event+'> ') sys.stdout.flush() else: sys.stdout.write(event+'\n') if __name__ == "__main__": description = "This script publishes the presence event package to a SIP Presence Agent for the given SIP account, the presence information can be changed using a menu-driven interface." usage = "%prog [options]" parser = OptionParser(usage=usage, description=description) parser.print_usage = parser.print_help parser.add_option("-a", "--account-name", type="string", dest="account_name", help="The name of the account to use.") parser.add_option("-s", "--trace-sip", action="store_true", dest="trace_sip", default=False, help="Dump the raw contents of incoming and outgoing SIP messages (disabled by default).") parser.add_option("-j", "--trace-pjsip", action="store_true", dest="trace_pjsip", default=False, help="Print PJSIP logging output (disabled by default).") parser.add_option("-n", "--trace-notifications", action="store_true", dest="trace_notifications", default=False, help="Print all notifications (disabled by default).") options, args = parser.parse_args() try: application = PublicationApplication(options.account_name, options.trace_sip, options.trace_pjsip, options.trace_notifications) return_code = application.run() - except RuntimeError, e: - print "Error: %s" % str(e) + except RuntimeError as e: + print("Error: %s" % str(e)) sys.exit(1) - except SIPCoreError, e: - print "Error: %s" % str(e) + except SIPCoreError as e: + print("Error: %s" % str(e)) sys.exit(1) else: sys.exit(return_code) diff --git a/sip-register b/sip-register index 6e98585..744f1e9 100755 --- a/sip-register +++ b/sip-register @@ -1,371 +1,369 @@ -#!/usr/bin/env python2 +#!/usr/bin/env python3 import atexit import os import select import signal import sys import termios from datetime import datetime from optparse import OptionParser from threading import Thread from time import sleep from application import log from application.notification import NotificationCenter, NotificationData from application.python.queue import EventQueue from sipsimple.account import Account, AccountManager, BonjourAccount from sipsimple.application import SIPApplication from sipsimple.configuration import ConfigurationError from sipsimple.configuration.settings import SIPSimpleSettings from sipsimple.core import Engine from sipsimple.storage import FileStorage from sipclient.configuration import config_directory from sipclient.configuration.account import AccountExtension from sipclient.configuration.settings import SIPSimpleSettingsExtension from sipclient.log import Logger from sipclient.system import IPAddressMonitor class BonjourNeighbour(object): def __init__(self, neighbour, uri, display_name, host): self.display_name = display_name self.host = host self.neighbour = neighbour self.uri = uri class InputThread(Thread): def __init__(self): Thread.__init__(self) self.daemon = True self._old_terminal_settings = None def start(self): atexit.register(self._termios_restore) Thread.start(self) def run(self): notification_center = NotificationCenter() while True: for char in self._getchars(): notification_center.post_notification('SIPApplicationGotInput', sender=self, data=NotificationData(input=char)) def stop(self): self._termios_restore() def _termios_restore(self): if self._old_terminal_settings is not None: termios.tcsetattr(sys.stdin.fileno(), termios.TCSADRAIN, self._old_terminal_settings) def _getchars(self): fd = sys.stdin.fileno() if os.isatty(fd): self._old_terminal_settings = termios.tcgetattr(fd) new = termios.tcgetattr(fd) new[3] = new[3] & ~termios.ICANON & ~termios.ECHO - new[6][termios.VMIN] = '\000' + new[6][termios.VMIN] = b'\000' try: termios.tcsetattr(fd, termios.TCSADRAIN, new) if select.select([fd], [], [], None)[0]: return sys.stdin.read(4192) finally: self._termios_restore() else: return os.read(fd, 4192) class RegistrationApplication(SIPApplication): def __init__(self): self.account = None self.options = None self.input = None self.output = None self.ip_address_monitor = IPAddressMonitor() self.logger = None self.max_registers = None self.neighbours = {} self.success = False def _write(self, message): - if isinstance(message, unicode): - message = message.encode(sys.getfilesystemencoding()) sys.stdout.write(message) sys.stdout.flush() def start(self, options): notification_center = NotificationCenter() self.options = options self.max_registers = options.max_registers if options.max_registers > 0 else None self.input = InputThread() if not options.batch_mode else None self.output = EventQueue(self._write) self.logger = Logger(sip_to_stdout=options.trace_sip, pjsip_to_stdout=options.trace_pjsip, notifications_to_stdout=options.trace_notifications) notification_center.add_observer(self, sender=self) notification_center.add_observer(self, sender=self.input) notification_center.add_observer(self, name='SIPSessionNewIncoming') notification_center.add_observer(self, name='DNSLookupDidSucceed') if self.input: self.input.start() self.output.start() log.level.current = log.level.WARNING # get rid of twisted messages Account.register_extension(AccountExtension) BonjourAccount.register_extension(AccountExtension) SIPSimpleSettings.register_extension(SIPSimpleSettingsExtension) try: SIPApplication.start(self, FileStorage(options.config_directory or config_directory)) - except ConfigurationError, e: + except ConfigurationError as e: self.output.put("Failed to load sipclient's configuration: %s\n" % str(e)) self.output.put("If an old configuration file is in place, delete it or move it and recreate the configuration using the sip_settings script.\n") self.output.stop() def print_help(self): message = 'Available control keys:\n' message += ' s: toggle SIP trace on the console\n' message += ' j: toggle PJSIP trace on the console\n' message += ' n: toggle notifications trace on the console\n' message += ' Ctrl-d: quit the program\n' message += ' ?: display this help message\n' self.output.put('\n'+message) def _NH_SIPApplicationWillStart(self, notification): account_manager = AccountManager() notification_center = NotificationCenter() settings = SIPSimpleSettings() if self.options.account is None: self.account = account_manager.default_account else: possible_accounts = [account for account in account_manager.iter_accounts() if self.options.account in account.id and account.enabled] if len(possible_accounts) > 1: self.output.put('More than one account exists which matches %s: %s\n' % (self.options.account, ', '.join(sorted(account.id for account in possible_accounts)))) self.output.stop() self.stop() return elif len(possible_accounts) == 0: self.output.put('No enabled account that matches %s was found. Available and enabled accounts: %s\n' % (self.options.account, ', '.join(sorted(account.id for account in account_manager.get_accounts() if account.enabled)))) self.output.stop() self.stop() return else: self.account = possible_accounts[0] for account in account_manager.iter_accounts(): if account is self.account: if isinstance(account, Account): account.sip.register = True account.message_summary.enabled = False account.presence.enabled = False account.xcap.enabled = False account.enabled = True else: account.enabled = False self.output.put('Using account %s\n' % self.account.id) notification_center.add_observer(self, sender=self.account) if self.account is BonjourAccount() and self.max_registers is not None: if self.max_registers == 1: self.max_registers = len([transport for transport in settings.sip.transport_list if (transport!='tls' or self.account.tls.certificate is not None)]) else: self.output.put('--max-registers option only accepts 0 or 1 if using Bonjour account\n') self.output.stop() self.stop() return # start logging self.logger.start() if settings.logs.trace_sip and self.logger._siptrace_filename is not None: self.output.put('Logging SIP trace to file "%s"\n' % self.logger._siptrace_filename) if settings.logs.trace_pjsip and self.logger._pjsiptrace_filename is not None: self.output.put('Logging PJSIP trace to file "%s"\n' % self.logger._pjsiptrace_filename) if settings.logs.trace_notifications and self.logger._notifications_filename is not None: self.output.put('Logging notifications trace to file "%s"\n' % self.logger._notifications_filename) def _NH_SIPApplicationDidStart(self, notification): engine = Engine() settings = SIPSimpleSettings() engine.trace_sip = self.logger.sip_to_stdout or settings.logs.trace_sip engine.log_level = settings.logs.pjsip_level if (self.logger.pjsip_to_stdout or settings.logs.trace_pjsip) else 0 self.ip_address_monitor.start() if self.account is not BonjourAccount() and self.max_registers != 1 and not self.options.batch_mode: self.print_help() elif self.account is BonjourAccount() and self.max_registers is None: self.print_help() def _NH_SIPApplicationWillEnd(self, notification): self.ip_address_monitor.stop() def _NH_SIPApplicationDidEnd(self, notification): if self.input: self.input.stop() self.output.stop() self.output.join() def _NH_DNSLookupDidSucceed(self, notification): notification_center = NotificationCenter() targets = {} for result in notification.data.result: try: ports = targets[result.address] except KeyError: targets[result.address] = set() port = '%s-%s' % (result.port, result.transport.upper()) targets[result.address].add(port) result_text = '' i = 1 - for t in targets.keys(): + for t in list(targets.keys()): result_text = result_text + ' ' + str(i) + ') ' + t + ': ' result_text = result_text + ' '.join(sorted(list(targets[t]))) i = i + 1 - self.output.put(u"\n%s DNS lookup for %s succeeded:%s\n" % (datetime.now().replace(microsecond=0), self.account.id.domain, result_text)) + self.output.put("\n%s DNS lookup for %s succeeded:%s\n" % (datetime.now().replace(microsecond=0), self.account.id.domain, result_text)) def _NH_SIPApplicationGotInput(self, notification): engine = Engine() settings = SIPSimpleSettings() key = notification.data.input if key == '\x04': self.stop() elif key == 's': self.logger.sip_to_stdout = not self.logger.sip_to_stdout engine.trace_sip = self.logger.sip_to_stdout or settings.logs.trace_sip self.output.put('SIP tracing to console is now %s.\n' % ('activated' if self.logger.sip_to_stdout else 'deactivated')) elif key == 'j': self.logger.pjsip_to_stdout = not self.logger.pjsip_to_stdout engine.log_level = settings.logs.pjsip_level if (self.logger.pjsip_to_stdout or settings.logs.trace_pjsip) else 0 self.output.put('PJSIP tracing to console is now %s.\n' % ('activated' if self.logger.pjsip_to_stdout else 'deactivated')) elif key == 'n': self.logger.notifications_to_stdout = not self.logger.notifications_to_stdout self.output.put('Notification tracing to console is now %s.\n' % ('activated' if self.logger.notifications_to_stdout else 'deactivated')) elif key == '?': self.print_help() def _NH_SIPAccountRegistrationDidSucceed(self, notification): contact_header = notification.data.contact_header contact_header_list = notification.data.contact_header_list expires = notification.data.expires registrar = notification.data.registrar if not self.success: message = '%s Registered contact "%s" at %s:%d;transport=%s for %d seconds\n' % (datetime.now().replace(microsecond=0), contact_header.uri, registrar.address, registrar.port, registrar.transport, expires) if notification.sender.contact.public_gruu: message += '%s Public GRUU: "%s"\n' % (datetime.now().replace(microsecond=0), notification.sender.contact.public_gruu) if notification.sender.contact.temporary_gruu: message += '%s Temporary GRUU: "%s"\n' % (datetime.now().replace(microsecond=0), notification.sender.contact.temporary_gruu) if len(contact_header_list) > 1: message += 'Other registered contacts:\n%s\n' % '\n'.join([' %s (expires in %s seconds)' % (str(other_contact_header.uri), other_contact_header.expires) for other_contact_header in contact_header_list if other_contact_header.uri != notification.data.contact_header.uri]) self.output.put(message) self.success = True else: self.output.put('%s Refreshed contact "%s" at %s:%d;transport=%s for %d seconds\n' % (datetime.now().replace(microsecond=0), contact_header.uri, registrar.address, registrar.port, registrar.transport, expires)) if self.max_registers is not None: self.max_registers -= 1 if self.max_registers == 0: self.stop() def _NH_SIPAccountRegistrationGotAnswer(self, notification): if notification.data.code >= 300: registrar = notification.data.registrar code = notification.data.code reason = notification.data.reason self.output.put('%s Registration failed at %s:%d;transport=%s: %d %s\n' % (datetime.now().replace(microsecond=0), registrar.address, registrar.port, registrar.transport, code, reason)) def _NH_SIPAccountRegistrationDidFail(self, notification): self.output.put('%s Failed to register contact for sip:%s: %s (retrying in %.2f seconds)\n' % (datetime.now().replace(microsecond=0), self.account.id, notification.data.error, notification.data.retry_after)) self.success = False if self.max_registers is not None: self.max_registers -= 1 if self.max_registers == 0: self.stop() def _NH_SIPAccountRegistrationDidEnd(self, notification): self.output.put('%s Registration ended.\n' % datetime.now().replace(microsecond=0)) def _NH_BonjourAccountRegistrationDidSucceed(self, notification): self.output.put('%s Registered Bonjour contact "%s"\n' % (datetime.now().replace(microsecond=0), notification.data.name)) if self.max_registers is not None: self.max_registers -= 1 if self.max_registers == 0: self.stop() def _NH_BonjourAccountRegistrationDidFail(self, notification): self.output.put('%s Failed to register Bonjour contact: %s\n' % (datetime.now().replace(microsecond=0), notification.data.reason)) if self.max_registers is not None: self.max_registers -= 1 if self.max_registers == 0: self.stop() def _NH_BonjourAccountRegistrationDidEnd(self, notification): self.output.put('%s Registration ended.\n' % datetime.now().replace(microsecond=0)) def _NH_BonjourAccountDidAddNeighbour(self, notification): neighbour, record = notification.data.neighbour, notification.data.record now = datetime.now().replace(microsecond=0) self.output.put('%s Discovered Bonjour neighbour: "%s (%s)" <%s>\n' % (now, record.name, record.host, record.uri)) self.neighbours[neighbour] = BonjourNeighbour(neighbour, record.uri, record.name, record.host) def _NH_BonjourAccountDidUpdateNeighbour(self, notification): neighbour, record = notification.data.neighbour, notification.data.record now = datetime.now().replace(microsecond=0) try: bonjour_neighbour = self.neighbours[neighbour] except KeyError: self.output.put('%s Discovered Bonjour neighbour: "%s (%s)" <%s>\n' % (now, record.name, record.host, record.uri)) self.neighbours[neighbour] = BonjourNeighbour(neighbour, record.uri, record.name, record.host) else: self.output.put('%s Updated Bonjour neighbour: "%s (%s)" <%s>\n' % (now, record.name, record.host, record.uri)) bonjour_neighbour.display_name = record.name bonjour_neighbour.host = record.host bonjour_neighbour.uri = record.uri def _NH_BonjourAccountDidRemoveNeighbour(self, notification): neighbour = notification.data.neighbour now = datetime.now().replace(microsecond=0) try: bonjour_neighbour = self.neighbours.pop(neighbour) except KeyError: pass else: self.output.put('%s Bonjour neighbour left: "%s (%s)" <%s>\n' % (now, bonjour_neighbour.display_name, bonjour_neighbour.host, bonjour_neighbour.uri)) def _NH_SIPEngineGotException(self, notification): self.output.put('%s An exception occured within the SIP core:\n%s\n' % (datetime.now().replace(microsecond=0), notification.data.traceback)) if __name__ == "__main__": description = 'This script registers the contact address of the given SIP account to the SIP registrar and refresh it while the program is running. When Ctrl+D is pressed it will unregister.' usage = '%prog [options]' parser = OptionParser(usage=usage, description=description) parser.print_usage = parser.print_help parser.add_option('-a', '--account', type='string', dest='account', help='The name of the account to use. If not supplied, the default account will be used.', metavar='NAME') parser.add_option('-c', '--config-directory', type='string', dest='config_directory', help='The configuration directory to use. This overrides the default location.') parser.add_option('-s', '--trace-sip', action='store_true', dest='trace_sip', default=False, help='Dump the raw contents of incoming and outgoing SIP messages (disabled by default).') parser.add_option('-j', '--trace-pjsip', action='store_true', dest='trace_pjsip', default=False, help='Print PJSIP logging output (disabled by default).') parser.add_option('-n', '--trace-notifications', action='store_true', dest='trace_notifications', default=False, help='Print all notifications (disabled by default).') parser.add_option('-r', '--max-registers', type='int', dest='max_registers', default=1, help='Max number of REGISTERs sent (default 1, set to 0 for infinite).') parser.add_option('-b', '--batch', action='store_true', dest='batch_mode', default=False, help='Run the program in batch mode: reading input from the console is disabled. This is particularly useful when running this script in a non-interactive environment.') options, args = parser.parse_args() application = RegistrationApplication() application.start(options) signal.signal(signal.SIGINT, signal.SIG_DFL) application.output.join() sleep(0.1) sys.exit(0 if application.success else 1) diff --git a/sip-session b/sip-session index c7f027b..dc16ec4 100755 --- a/sip-session +++ b/sip-session @@ -1,2840 +1,2840 @@ -#!/usr/bin/env python2 +#!/usr/bin/env python3 import os import re import signal import sys -import urllib +import urllib.request, urllib.parse, urllib.error import random import requests from collections import defaultdict from datetime import datetime, timedelta from dateutil.tz import tzlocal from itertools import chain from lxml import html from optparse import OptionParser from otr import OTRState, SMPStatus from threading import Event, Thread from time import sleep from application import log from application.notification import IObserver, NotificationCenter from application.python.queue import EventQueue from application.python import Null from eventlib import api from twisted.internet import reactor -from zope.interface import implements +from zope.interface import implementer from otr import OTRSession, OTRTransport, OTRState, SMPStatus from otr.exceptions import IgnoreMessage, UnencryptedMessage, EncryptedMessageError, OTRError from sipsimple.core import Engine, FromHeader, Message, RouteHeader, SIPCoreError, SIPURI, ToHeader from sipsimple.account import Account, AccountManager, BonjourAccount from sipsimple.application import SIPApplication from sipsimple.audio import WavePlayer from sipsimple.configuration import ConfigurationError from sipsimple.configuration.settings import SIPSimpleSettings from sipsimple.lookup import DNSLookup from sipsimple.payloads.iscomposing import IsComposingMessage from sipsimple.session import IllegalStateError, Session from sipsimple.streams import MediaStreamRegistry from sipsimple.streams.msrp.filetransfer import FileSelector from sipsimple.streams.msrp.chat import CPIMPayload, SimplePayload, CPIMParserError, ChatIdentity, OTREncryption from sipsimple.storage import FileStorage from sipsimple.threading.green import run_in_green_thread from sipsimple.util import ISOTimestamp from sipclient.configuration import config_directory from sipclient.configuration.account import AccountExtension from sipclient.configuration.datatypes import ResourcePath from sipclient.configuration.settings import SIPSimpleSettingsExtension from sipclient.log import Logger from sipclient.system import IPAddressMonitor from sipclient.ui import Prompt, Question, RichText, UI # This is a helper function for sending formatted notice messages def show_notice(text, bold=True): ui = UI() if isinstance(text, list): ui.writelines([RichText(line, bold=bold) if not isinstance(line, RichText) else line for line in text]) elif isinstance(text, RichText): ui.write(text) else: ui.write(RichText(text, bold=bold)) # Utility classes # class BonjourNeighbour(object): def __init__(self, neighbour, uri, display_name, host): self.display_name = display_name self.host = host self.neighbour = neighbour self.uri = uri class RTPStatisticsThread(Thread): def __init__(self): Thread.__init__(self) self.setDaemon(True) self.stopped = False def run(self): application = SIPSessionApplication() while not self.stopped: if application.active_session is not None and application.active_session.streams: audio_stream = next((stream for stream in application.active_session.streams if stream.type == 'audio'), None) if audio_stream is not None: stats = audio_stream.statistics if stats is not None: reactor.callFromThread(show_notice, '%s RTP statistics: RTT=%d ms, packet loss=%.1f%%, jitter RX/TX=%d/%d ms' % (datetime.now().replace(microsecond=0), stats['rtt']['avg'] / 1000, 100.0 * stats['rx']['packets_lost'] / stats['rx']['packets'] if stats['rx']['packets'] else 0, stats['rx']['jitter']['avg'] / 1000, stats['tx']['jitter']['avg'] / 1000)) sleep(10) def stop(self): self.stopped = True class QueuedMessage(object): def __init__(self, msg_id, content, content_type='text/plain'): self.id = msg_id self.content = content self.timestamp = None self.content_type = content_type self.encrypted = False class OTRInternalMessage(QueuedMessage): def __init__(self, content): super(OTRInternalMessage, self).__init__('OTR', content, 'text/plain') +@implementer(IObserver) class MessageSession(object): - implements(IObserver) def __init__(self, account, target, use_cpim=False): self.account = account self.target = target self.routes = None self.use_cpim = use_cpim self.msg_id = 0 self.started = False self.msg_map = {} self.route = None self.ended = False self.encryption = OTREncryption(self) self.message_queue = EventQueue(self._send_message) target = self.target if '@' not in target: target = '%s@%s' % (target, self.account.id.domain) if not target.startswith('sip:') and not target.startswith('sips:'): target = 'sip:' + target self.remote_uri = str(target).split(":")[1] try: self.target_uri = SIPURI.parse(target) except SIPCoreError: show_notice('Illegal SIP URI: %s' % target) self.target_uri = None self.notification_center = NotificationCenter() show_notice('Message session started with %s' % self.target) def end(self): show_notice('Ending message session to %s' % self.target) if self.encryption.active: self.encryption.stop() self.notification_center = None self.message_queue = None self.encryption = None self.ended = True def start(self): if self.ended: return lookup = DNSLookup() self.notification_center.add_observer(self, sender=lookup) settings = SIPSimpleSettings() if isinstance(self.account, Account) and self.account.sip.outbound_proxy is not None: uri = SIPURI(host=self.account.sip.outbound_proxy.host, port=self.account.sip.outbound_proxy.port, parameters={'transport': self.account.sip.outbound_proxy.transport}) elif isinstance(self.account, Account) and self.account.sip.always_use_my_proxy: uri = SIPURI(host=self.account.id.domain) else: uri = self.target_uri lookup.lookup_sip_proxy(uri, settings.sip.transport_list) def handle_notification(self, notification): if self.ended: return handler = getattr(self, '_NH_%s' % notification.name, Null) handler(notification) def inject_otr_message(self, data): show_notice('Requesting %s of OTR encryption...' % 'stop' if self.encryption.active else 'start') messageObject = OTRInternalMessage(data) self.message_queue.put(messageObject) def _NH_DNSLookupDidSucceed(self, notification): self.notification_center.remove_observer(self, sender=notification.sender) self.routes = notification.data.result if not self.started: self.message_queue.start() if not self.encryption.active: self.encryption.start() show_notice('%s Session to %s will start via %s' % (datetime.now().replace(microsecond=0), self.remote_uri, self.routes[0])) self.started = True def _NH_DNSLookupDidFail(self, notification): self.notification_center.remove_observer(self, sender=notification.sender) show_notice('%s Session to %s failed: DNS lookup failed' % (datetime.now().replace(microsecond=0), self.remote_uri)) self.message_queue.stop() def send_message(self, text): if not self.routes: self.start() self.msg_id = self.msg_id + 1 messageObject = QueuedMessage(self.msg_id , text) self.message_queue.put(messageObject) return self.msg_id def _send_message(self, message): if not self.routes: return if not self.route: self.route = self.routes.pop(0) identity = str(self.account.uri) if self.account.display_name: identity = '"%s" <%s>' % (self.account.display_name, identity) - if isinstance(message.content, unicode): + if isinstance(message.content, str): charset = 'utf8' message.content = message.content.encode(charset) else: charset = None what_type = None if not isinstance(message, OTRInternalMessage): try: message.content = self.encryption.otr_session.handle_output(message.content, message.content_type) except OTRError as e: if 'has ended the private conversation' in str(e): show_notice('Encryption has been disabled by remote party, please resend the message again') self.encryption.stop() else: show_notice('Failed to encrypt outgoing message: %s' % str(e)) return else: what_type = 'OTR' if message.timestamp is None: message.timestamp = ISOTimestamp.now() if self.use_cpim: content_type = 'message/cpim' payload = CPIMPayload(message.content, 'text/plain', charset='utf-8', sender=ChatIdentity(self.account.uri, self.account.display_name), recipients=[ChatIdentity(self.remote_uri, None)]) else: payload = SimplePayload(message.content, message.content_type, charset) content, content_type = payload.encode() message_request = Message(FromHeader(self.account.uri, self.account.display_name), ToHeader(self.target_uri), RouteHeader(self.route.uri), content_type, content, credentials=self.account.credentials) self.notification_center.add_observer(self, sender=message_request) if message.id != 'OTR': if '?OTR:' in content: show_notice('Encrypted message %s sent to %s' % (message.id, self.route.uri)) message.encrypted = True else: show_notice('Message %s sent to %s' % (message.id, self.route.uri)) self.msg_map[message_request] = message message_request.send(15) def handle_incoming(self, from_header, content_type, data): self.msg_id = self.msg_id + 1 if content_type == 'message/cpim': try: payload = CPIMPayload.decode(content) except CPIMParserError as e: show_notice('CPIM parse error: %s' % str(e)) return None else: if payload.sender: identity = '%s@%s' % (payload.sender.uri.user, payload.sender.uri.host) if from_header.uri == cpim_message.sender.uri: if payload.sender.display_name: identity = '"%s" <%s>' % (payload.sender.display_name, identity) elif from_header.display_name: identity = '"%s" <%s>' % (from_header.display_name, identity) else: identity = from_header else: payload = SimplePayload.decode(data, content_type) from_header = FromHeader.new(from_header) identity = '%s@%s' % (str(from_header.uri.user), str(from_header.uri.host)) content = payload.content content_type = payload.content_type try: content = self.encryption.otr_session.handle_input(content, content_type) except IgnoreMessage: return None except UnencryptedMessage: encrypted = False encryption_active = True except EncryptedMessageError as e: show_notice('OTP encrypted message error: %s' % str(e)) return None except OTRError as e: show_notice('OTP error: %s' % str(e)) return None else: encrypted = encryption_active = self.encryption.active if payload.charset is not None: content = content.decode(payload.charset) elif payload.content_type.startswith('text/'): content.decode('utf8') return (content_type, content, identity, self.msg_id, encrypted) def _NH_SIPMessageDidSucceed(self, notification): self.notification_center.remove_observer(self, sender=notification.sender) try: message = self.msg_map[notification.sender] except KeyError: message = None else: del(self.msg_map[notification.sender]) if not message: return if message.id in (None, 'OTR'): return if notification.data.code == 202: show_notice('%s Message %s to %s will be delivered later by the server' % (datetime.now().replace(microsecond=0), message.id, self.remote_uri)) elif notification.data.code == 200: if message.encrypted: show_notice('%s Encrypted message %s received by %s' % (datetime.now().replace(microsecond=0), message.id, self.remote_uri)) else: show_notice('%s Message %s received by %s' % (datetime.now().replace(microsecond=0), message.id, self.remote_uri)) else: show_notice('%s Message %s received by %s (code %d)' % (datetime.now().replace(microsecond=0), message.id, self.remote_uri, notification.data.code )) def _NH_SIPMessageDidFail(self, notification): self.notification_center.remove_observer(self, sender=notification.sender) try: message = self.msg_map[notification.sender] except KeyError: message = None else: del(self.msg_map[notification.sender]) if not message: return if message.id in (None, 'OTR'): return server = notification.data.headers.get('Server', Null).body client = notification.data.headers.get('Client', Null).body if notification.data.code == 408 and self.routes: pass # self.route = self.routes.pop(0) # TO DO retry if message.encrypted: show_notice('%s Encrypted message %s to %s failed on %s: %s (%d)' % (datetime.now().replace(microsecond=0), message.id, self.remote_uri, server or client, notification.data.reason, notification.data.code)) else: show_notice('%s Message %s to %s failed on %s: %s (%d)' % (datetime.now().replace(microsecond=0), message.id, self.remote_uri, server or client, notification.data.reason, notification.data.code)) OTRTransport.register(MessageSession) +@implementer(IObserver) class OutgoingCallInitializer(object): - implements(IObserver) def __init__(self, account, target, audio=False, chat=False): self.account = account self.target = target self.streams = [] if audio: self.streams.append(MediaStreamRegistry.AudioStream()) if chat: self.streams.append(MediaStreamRegistry.ChatStream()) self.wave_ringtone = None def start(self): if isinstance(self.account, BonjourAccount) and '@' not in self.target: show_notice('Bonjour mode requires a host in the destination address') return if '@' not in self.target: self.target = '%s@%s' % (self.target, self.account.id.domain) if not self.target.startswith('sip:') and not self.target.startswith('sips:'): self.target = 'sip:' + self.target try: self.target = SIPURI.parse(self.target) except SIPCoreError: show_notice('Illegal SIP URI: %s' % self.target) else: if '.' not in self.target.host and not isinstance(self.account, BonjourAccount): self.target.host = '%s.%s' % (self.target.host, self.account.id.domain) lookup = DNSLookup() notification_center = NotificationCenter() notification_center.add_observer(self, sender=lookup) settings = SIPSimpleSettings() if isinstance(self.account, Account) and self.account.sip.outbound_proxy is not None: uri = SIPURI(host=self.account.sip.outbound_proxy.host, port=self.account.sip.outbound_proxy.port, parameters={'transport': self.account.sip.outbound_proxy.transport}) elif isinstance(self.account, Account) and self.account.sip.always_use_my_proxy: uri = SIPURI(host=self.account.id.domain) else: uri = self.target show_notice('DNS lookup for %s' % uri) lookup.lookup_sip_proxy(uri, settings.sip.transport_list) def handle_notification(self, notification): handler = getattr(self, '_NH_%s' % notification.name, Null) handler(notification) def _NH_DNSLookupDidSucceed(self, notification): notification_center = NotificationCenter() notification_center.remove_observer(self, sender=notification.sender) session = Session(self.account) notification_center.add_observer(self, sender=session) session.connect(ToHeader(self.target), routes=notification.data.result, streams=self.streams) application = SIPSessionApplication() application.outgoing_session = session def _NH_DNSLookupDidFail(self, notification): show_notice('Call to %s failed: DNS lookup error: %s' % (self.target, notification.data.error)) notification_center = NotificationCenter() notification_center.remove_observer(self, sender=notification.sender) def _NH_SIPSessionNewOutgoing(self, notification): session = notification.sender local_identity = str(session.local_identity.uri) if session.local_identity.display_name: local_identity = '"%s" <%s>' % (session.local_identity.display_name, local_identity) remote_identity = str(session.remote_identity.uri) if session.remote_identity.display_name: remote_identity = '"%s" <%s>' % (session.remote_identity.display_name, remote_identity) show_notice("Initiating SIP session from '%s' to '%s' via %s..." % (local_identity, remote_identity, session.route)) self.message_session_to = None def _NH_SIPSessionGotRingIndication(self, notification): settings = SIPSimpleSettings() ui = UI() ringtone = settings.sounds.audio_outbound if ringtone and self.wave_ringtone is None: self.wave_ringtone = WavePlayer(SIPApplication.voice_audio_mixer, ringtone.path.normalized, volume=ringtone.volume, loop_count=0, pause_time=2) SIPApplication.voice_audio_bridge.add(self.wave_ringtone) self.wave_ringtone.start() ui.status = 'Ringing...' def _NH_SIPSessionWillStart(self, notification): ui = UI() if self.wave_ringtone: self.wave_ringtone.stop() SIPApplication.voice_audio_bridge.remove(self.wave_ringtone) self.wave_ringtone = None ui.status = 'Connecting...' def _NH_SIPSessionDidStart(self, notification): notification_center = NotificationCenter() ui = UI() session = notification.sender notification_center.remove_observer(self, sender=session) ui.status = 'Connected' reactor.callLater(2, setattr, ui, 'status', None) application = SIPSessionApplication() application.outgoing_session = None for stream in notification.data.streams: if stream.type == 'audio': show_notice('Audio session established using "%s" codec at %sHz' % (stream.codec, stream.sample_rate)) if stream.ice_active: show_notice('Audio RTP endpoints %s:%d (ICE type %s) <-> %s:%d (ICE type %s)' % (stream.local_rtp_address, stream.local_rtp_port, stream.local_rtp_candidate.type.lower(), stream.remote_rtp_address, stream.remote_rtp_port, stream.remote_rtp_candidate.type.lower())) else: show_notice('Audio RTP endpoints %s:%d <-> %s:%d' % (stream.local_rtp_address, stream.local_rtp_port, stream.remote_rtp_address, stream.remote_rtp_port)) if session.remote_user_agent is not None: show_notice('Remote SIP User Agent is "%s"' % session.remote_user_agent) def _NH_SIPSessionDidFail(self, notification): notification_center = NotificationCenter() session = notification.sender notification_center.remove_observer(self, sender=session) ui = UI() ui.status = None application = SIPSessionApplication() application.outgoing_session = None if self.wave_ringtone: self.wave_ringtone.stop() SIPApplication.voice_audio_bridge.remove(self.wave_ringtone) self.wave_ringtone = None if notification.data.failure_reason == 'user request' and notification.data.code == 487: show_notice('SIP session cancelled') elif notification.data.failure_reason == 'user request': show_notice('SIP session rejected by user (%d %s)' % (notification.data.code, notification.data.reason)) else: show_notice('SIP session failed: %s' % notification.data.failure_reason) +@implementer(IObserver) class IncomingCallInitializer(object): - implements(IObserver) sessions = 0 tone_ringtone = None def __init__(self, session, auto_answer_interval=None): self.session = session self.auto_answer_interval = auto_answer_interval self.question = None def start(self): IncomingCallInitializer.sessions += 1 notification_center = NotificationCenter() notification_center.add_observer(self, sender=self.session) # start auto-answer self.answer_timer = None if self.auto_answer_interval == 0: self.session.accept(self.session.proposed_streams) return elif self.auto_answer_interval > 0: self.answer_timer = reactor.callFromThread(reactor.callLater, self.auto_answer_interval, self.session.accept, self.session.proposed_streams) # start ringing application = SIPSessionApplication() self.wave_ringtone = None if application.active_session is None: if IncomingCallInitializer.sessions == 1: ringtone = self.session.account.sounds.audio_inbound.sound_file if self.session.account.sounds.audio_inbound is not None else None if ringtone: self.wave_ringtone = WavePlayer(SIPApplication.alert_audio_mixer, ringtone.path.normalized, volume=ringtone.volume, loop_count=0, pause_time=2) SIPApplication.alert_audio_bridge.add(self.wave_ringtone) self.wave_ringtone.start() elif IncomingCallInitializer.tone_ringtone is None: IncomingCallInitializer.tone_ringtone = WavePlayer(SIPApplication.voice_audio_mixer, ResourcePath('sounds/ring_tone.wav').normalized, loop_count=0, pause_time=6) SIPApplication.voice_audio_bridge.add(IncomingCallInitializer.tone_ringtone) IncomingCallInitializer.tone_ringtone.start() self.session.send_ring_indication() # ask question identity = str(self.session.remote_identity.uri) if self.session.remote_identity.display_name: identity = '"%s" <%s>' % (self.session.remote_identity.display_name, identity) streams = '/'.join(stream.type for stream in self.session.proposed_streams) self.question = Question("Incoming %s from '%s', do you want to accept? (a)ccept/(r)eject/(b)usy" % (streams, identity), 'arbi', bold=True) notification_center.add_observer(self, sender=self.question) ui = UI() ui.add_question(self.question) def handle_notification(self, notification): handler = getattr(self, '_NH_%s' % notification.name, Null) handler(notification) def _NH_UIQuestionGotAnswer(self, notification): notification_center = NotificationCenter() ui = UI() notification_center.remove_observer(self, sender=notification.sender) answer = notification.data.answer self.question = None if answer == 'a': self.session.accept(self.session.proposed_streams) ui.status = 'Accepting...' elif answer == 'r': self.session.reject() ui.status = 'Rejecting...' elif answer == 'b': self.session.reject(486) ui.status = 'Sending Busy Here...' if self.wave_ringtone: self.wave_ringtone.stop() self.wave_ringtone = None if IncomingCallInitializer.sessions > 1: if IncomingCallInitializer.tone_ringtone is None: IncomingCallInitializer.tone_ringtone = WavePlayer(SIPApplication.voice_audio_mixer, ResourcePath('sounds/ring_tone.wav').normalized, loop_count=0, pause_time=6) SIPApplication.voice_audio_bridge.add(IncomingCallInitializer.tone_ringtone) IncomingCallInitializer.tone_ringtone.start() elif IncomingCallInitializer.tone_ringtone: IncomingCallInitializer.tone_ringtone.stop() IncomingCallInitializer.tone_ringtone = None if self.answer_timer is not None and self.answer_timer.active(): self.answer_timer.cancel() def _NH_SIPSessionWillStart(self, notification): ui = UI() if self.question is not None: notification_center = NotificationCenter() notification_center.remove_observer(self, sender=self.question) ui.remove_question(self.question) self.question = None ui.status = 'Connecting...' def _NH_SIPSessionDidStart(self, notification): notification_center = NotificationCenter() session = notification.sender notification_center.remove_observer(self, sender=session) IncomingCallInitializer.sessions -= 1 ui = UI() ui.status = 'Connected' reactor.callLater(2, setattr, ui, 'status', None) identity = str(session.remote_identity.uri) if session.remote_identity.display_name: identity = '"%s" <%s>' % (session.remote_identity.display_name, identity) show_notice("SIP session with '%s' established" % identity) for stream in notification.data.streams: if stream.type == 'audio': show_notice('Audio stream using "%s" codec at %sHz' % (stream.codec, stream.sample_rate)) if stream.ice_active: show_notice('Audio RTP endpoints %s:%d (ICE type %s) <-> %s:%d (ICE type %s)' % (stream.local_rtp_address, stream.local_rtp_port, stream.local_rtp_candidate_type, stream.remote_rtp_address, stream.remote_rtp_port, stream.remote_rtp_candidate_type)) else: show_notice('Audio RTP endpoints %s:%d <-> %s:%d' % (stream.local_rtp_address, stream.local_rtp_port, stream.remote_rtp_address, stream.remote_rtp_port)) if stream.encryption.active: show_notice('RTP audio stream is encrypted using %s (%s)\n' % (stream.encryption.type, stream.encryption.cipher)) if session.remote_user_agent is not None: show_notice('Remote SIP User Agent is "%s"' % session.remote_user_agent) def _NH_SIPSessionDidFail(self, notification): notification_center = NotificationCenter() ui = UI() session = notification.sender notification_center.remove_observer(self, sender=session) ui.status = None if self.question is not None: notification_center.remove_observer(self, sender=self.question) ui.remove_question(self.question) self.question = None IncomingCallInitializer.sessions -= 1 if self.wave_ringtone: self.wave_ringtone.stop() self.wave_ringtone = None if IncomingCallInitializer.sessions == 0 and IncomingCallInitializer.tone_ringtone is not None: IncomingCallInitializer.tone_ringtone.stop() IncomingCallInitializer.tone_ringtone = None if notification.data.failure_reason == 'user request' and notification.data.code == 487: show_notice('SIP session cancelled by user') if notification.data.failure_reason == 'Call completed elsewhere' and notification.data.code == 487: show_notice('SIP session cancelled, call was answered elsewhere') elif notification.data.failure_reason == 'user request': show_notice('SIP session rejected (%d %s)' % (notification.data.code, notification.data.reason)) else: show_notice('SIP session failed: %s' % notification.data.failure_reason) +@implementer(IObserver) class OutgoingProposalHandler(object): - implements(IObserver) def __init__(self, session, audio=False, chat=False): self.session = session self.stream = None if audio: self.stream = MediaStreamRegistry.AudioStream() if chat: self.stream = MediaStreamRegistry.ChatStream() if not self.stream: raise ValueError("Need to specify exactly one stream") def start(self): notification_center = NotificationCenter() notification_center.add_observer(self, sender=self.session) try: self.session.add_stream(self.stream) except IllegalStateError: notification_center.remove_observer(self, sender=self.session) raise remote_identity = str(self.session.remote_identity.uri) if self.session.remote_identity.display_name: remote_identity = '"%s" <%s>' % (self.session.remote_identity.display_name, remote_identity) show_notice("Proposing %s to '%s'..." % (self.stream.type, remote_identity)) def handle_notification(self, notification): handler = getattr(self, '_NH_%s' % notification.name, Null) handler(notification) def _NH_SIPSessionProposalAccepted(self, notification): notification_center = NotificationCenter() notification_center.remove_observer(self, sender=self.session) application = SIPSessionApplication() application.sessions_with_proposals.remove(notification.sender) show_notice('Proposal accepted') def _NH_SIPSessionProposalRejected(self, notification): notification_center = NotificationCenter() notification_center.remove_observer(self, sender=self.session) application = SIPSessionApplication() application.sessions_with_proposals.remove(notification.sender) ui = UI() ui.status = None if notification.data.code == 487: show_notice('Proposal cancelled (%d %s)' % (notification.data.code, notification.data.reason)) else: show_notice('Proposal rejected (%d %s)' % (notification.data.code, notification.data.reason)) def _NH_SIPSessionDidEnd(self, notification): notification_center = NotificationCenter() notification_center.remove_observer(self, sender=self.session) +@implementer(IObserver) class IncomingProposalHandler(object): - implements(IObserver) sessions = 0 tone_ringtone = None def __init__(self, session): self.session = session self.question = None def start(self): IncomingProposalHandler.sessions += 1 notification_center = NotificationCenter() notification_center.add_observer(self, sender=self.session) # start ringing if IncomingProposalHandler.tone_ringtone is None: IncomingProposalHandler.tone_ringtone = WavePlayer(SIPApplication.voice_audio_mixer, ResourcePath('sounds/ring_tone.wav').normalized, loop_count=0, pause_time=6) SIPApplication.voice_audio_bridge.add(IncomingProposalHandler.tone_ringtone) IncomingProposalHandler.tone_ringtone.start() self.session.send_ring_indication() # ask question identity = str(self.session.remote_identity.uri) if self.session.remote_identity.display_name: identity = '"%s" <%s>' % (self.session.remote_identity.display_name, identity) streams = ', '.join(stream.type for stream in self.session.proposed_streams) self.question = Question("'%s' wants to add %s, do you want to accept? (a)ccept/(r)eject" % (identity, streams), 'ar', bold=True) notification_center.add_observer(self, sender=self.question) ui = UI() ui.add_question(self.question) def handle_notification(self, notification): handler = getattr(self, '_NH_%s' % notification.name, Null) handler(notification) def _NH_UIQuestionGotAnswer(self, notification): notification_center = NotificationCenter() ui = UI() notification_center.remove_observer(self, sender=notification.sender) answer = notification.data.answer self.question = None if answer == 'a': self.session.accept_proposal(self.session.proposed_streams) ui.status = 'Accepting proposal...' elif answer == 'r': self.session.reject_proposal() ui.status = 'Rejecting proposal...' if IncomingProposalHandler.sessions == 1 and IncomingProposalHandler.tone_ringtone: IncomingProposalHandler.tone_ringtone.stop() IncomingProposalHandler.tone_ringtone = None def _NH_SIPSessionProposalAccepted(self, notification): notification_center = NotificationCenter() session = notification.sender notification_center.remove_observer(self, sender=session) application = SIPSessionApplication() application.sessions_with_proposals.remove(notification.sender) IncomingProposalHandler.sessions -= 1 ui = UI() ui.status = None show_notice('Proposal accepted') def _NH_SIPSessionProposalRejected(self, notification): notification_center = NotificationCenter() session = notification.sender notification_center.remove_observer(self, sender=session) application = SIPSessionApplication() application.sessions_with_proposals.remove(notification.sender) IncomingProposalHandler.sessions -= 1 ui = UI() ui.status = None if notification.data.code == 487: show_notice('Proposal cancelled (%d %s)' % (notification.data.code, notification.data.reason)) else: show_notice('Proposal rejected (%d %s)' % (notification.data.code, notification.data.reason)) if IncomingProposalHandler.tone_ringtone: IncomingProposalHandler.tone_ringtone.stop() IncomingProposalHandler.tone_ringtone = None if self.question is not None: notification_center.remove_observer(self, sender=self.question) ui.remove_question(self.question) self.question = None def _NH_SIPSessionHadProposalFailure(self, notification): notification_center = NotificationCenter() session = notification.sender notification_center.remove_observer(self, sender=session) IncomingProposalHandler.sessions -= 1 ui = UI() ui.status = None show_notice('Proposal failed (%s)' % notification.data.failure_reason) def _NH_SIPSessionDidEnd(self, notification): notification_center = NotificationCenter() ui = UI() session = notification.sender notification_center.remove_observer(self, sender=session) ui.status = None if self.question is not None: notification_center.remove_observer(self, sender=self.question) ui.remove_question(self.question) self.question = None IncomingProposalHandler.sessions -= 1 if IncomingProposalHandler.sessions == 0 and IncomingProposalHandler.tone_ringtone is not None: IncomingProposalHandler.tone_ringtone.stop() IncomingProposalHandler.tone_ringtone = None +@implementer(IObserver) class OutgoingTransferHandler(object): - implements(IObserver) def __init__(self, account, target, filepath): self.account = account self.target = target self.filepath = filepath.decode(sys.getfilesystemencoding()) self.file_selector = None self.hash_compute_proc = None self.session = None self.stream = None self.handler = None self.wave_ringtone = None @run_in_green_thread def start(self): if isinstance(self.account, BonjourAccount) and '@' not in self.target: show_notice('Bonjour mode requires a host in the destination address') return if '@' not in self.target: self.target = '%s@%s' % (self.target, self.account.id.domain) if not self.target.startswith('sip:') and not self.target.startswith('sips:'): self.target = 'sip:' + self.target try: self.target = SIPURI.parse(self.target) except SIPCoreError: show_notice('Illegal SIP URI: %s' % self.target) return show_notice('Preparing transfer...') self.file_selector = FileSelector.for_file(self.filepath) if '.' not in self.target.host and not isinstance(self.account, BonjourAccount): self.target.host = '%s.%s' % (self.target.host, self.account.id.domain) lookup = DNSLookup() notification_center = NotificationCenter() notification_center.add_observer(self, sender=lookup) settings = SIPSimpleSettings() if isinstance(self.account, Account) and self.account.sip.outbound_proxy is not None: uri = SIPURI(host=self.account.sip.outbound_proxy.host, port=self.account.sip.outbound_proxy.port, parameters={'transport': self.account.sip.outbound_proxy.transport}) elif isinstance(self.account, Account) and self.account.sip.always_use_my_proxy: uri = SIPURI(host=self.account.id.domain) else: uri = self.target lookup.lookup_sip_proxy(uri, settings.sip.transport_list) def _terminate(self, failure_reason=None): notification_center = NotificationCenter() notification_center.remove_observer(self, sender=self.session) notification_center.remove_observer(self, sender=self.stream) notification_center.remove_observer(self, sender=self.handler) ui = UI() ui.status = None if self.wave_ringtone: self.wave_ringtone.stop() self.wave_ringtone = None if failure_reason is None: show_notice('File transfer of %s finished' % os.path.basename(self.filepath)) else: show_notice('File transfer of %s failed: %s' % (os.path.basename(self.filepath), failure_reason)) self.session = None self.stream = None self.handler = None def handle_notification(self, notification): handler = getattr(self, '_NH_%s' % notification.name, Null) handler(notification) def _NH_DNSLookupDidSucceed(self, notification): notification.center.remove_observer(self, sender=notification.sender) self.session = Session(self.account) self.stream = MediaStreamRegistry.FileTransferStream(self.file_selector, 'sendonly') self.handler = self.stream.handler notification.center.add_observer(self, sender=self.session) notification.center.add_observer(self, sender=self.stream) notification.center.add_observer(self, sender=self.handler) self.session.connect(ToHeader(self.target), routes=notification.data.result, streams=[self.stream]) def _NH_DNSLookupDidFail(self, notification): notification.center.remove_observer(self, sender=notification.sender) show_notice('File transfer to %s failed: DNS lookup error: %s' % (self.target, notification.data.error)) def _NH_SIPSessionNewOutgoing(self, notification): session = notification.sender local_identity = str(session.local_identity.uri) if session.local_identity.display_name: local_identity = '"%s" <%s>' % (session.local_identity.display_name, local_identity) remote_identity = str(session.remote_identity.uri) if session.remote_identity.display_name: remote_identity = '"%s" <%s>' % (session.remote_identity.display_name, remote_identity) show_notice("Initiating file transfer from '%s' to '%s' via %s..." % (local_identity, remote_identity, session.route)) def _NH_SIPSessionGotRingIndication(self, notification): settings = SIPSimpleSettings() ui = UI() ringtone = settings.sounds.audio_outbound if ringtone and self.wave_ringtone is None: self.wave_ringtone = WavePlayer(SIPApplication.voice_audio_mixer, ringtone.path.normalized, volume=ringtone.volume, loop_count=0, pause_time=2) SIPApplication.voice_audio_bridge.add(self.wave_ringtone) self.wave_ringtone.start() ui.status = 'Ringing...' def _NH_SIPSessionWillStart(self, notification): ui = UI() if self.wave_ringtone: self.wave_ringtone.stop() ui.status = 'Connecting...' def _NH_SIPSessionDidStart(self, notification): session = notification.sender ui = UI() ui.status = 'File transfer connected' identity = str(session.remote_identity.uri) if session.remote_identity.display_name: identity = '"%s" <%s>' % (session.remote_identity.display_name, identity) show_notice("File transfer for %s to '%s' started" % (os.path.basename(self.filepath), identity)) def _NH_MediaStreamDidNotInitialize(self, notification): self._terminate(failure_reason=notification.data.reason) def _NH_FileTransferHandlerDidEnd(self, notification): self.session.end() self._terminate(failure_reason=notification.data.reason) +@implementer(IObserver) class IncomingTransferHandler(object): - implements(IObserver) sessions = 0 tone_ringtone = None def __init__(self, session, auto_answer_interval=None): self.session = session self.stream = None self.handler = None self.auto_answer_interval = auto_answer_interval self.file = None self.filename = None self.finished = False self.hash = None self.question = None self.wave_ringtone = None def start(self): self.stream = self.session.proposed_streams[0] self.handler = self.stream.handler self.file_selector = self.stream.file_selector self.filename = self.file_selector.name IncomingTransferHandler.sessions += 1 notification_center = NotificationCenter() notification_center.add_observer(self, sender=self.session) notification_center.add_observer(self, sender=self.stream) notification_center.add_observer(self, sender=self.handler) # start auto-answer self.answer_timer = None if self.auto_answer_interval == 0: self.session.accept(self.session.proposed_streams) return elif self.auto_answer_interval > 0: self.answer_timer = reactor.callFromThread(reactor.callLater, self.auto_answer_interval, self.session.accept, self.session.proposed_streams) # start ringing application = SIPSessionApplication() if application.active_session is None: if IncomingTransferHandler.sessions == 1: ringtone = self.session.account.sounds.audio_inbound.sound_file if self.session.account.sounds.audio_inbound is not None else None if ringtone: self.wave_ringtone = WavePlayer(SIPApplication.alert_audio_mixer, ringtone.path.normalized, volume=ringtone.volume, loop_count=0, pause_time=2) SIPApplication.alert_audio_bridge.add(self.wave_ringtone) self.wave_ringtone.start() elif IncomingTransferHandler.tone_ringtone is None: IncomingTransferHandler.tone_ringtone = WavePlayer(SIPApplication.voice_audio_mixer, ResourcePath('sounds/ring_tone.wav').normalized, loop_count=0, pause_time=6) SIPApplication.voice_audio_bridge.add(IncomingTransferHandler.tone_ringtone) IncomingTransferHandler.tone_ringtone.start() self.session.send_ring_indication() # ask question identity = str(self.session.remote_identity.uri) if self.session.remote_identity.display_name: identity = '"%s" <%s>' % (self.session.remote_identity.display_name, identity) self.question = Question("Incoming file transfer for %s from '%s', do you want to accept? (a)ccept/(r)eject" % (os.path.basename(self.filename), identity), 'ari', bold=True) notification_center.add_observer(self, sender=self.question) ui = UI() ui.add_question(self.question) def _terminate(self, failure_reason=None): notification_center = NotificationCenter() notification_center.remove_observer(self, sender=self.session) notification_center.remove_observer(self, sender=self.stream) notification_center.remove_observer(self, sender=self.handler) ui = UI() ui.status = None if self.question is not None: notification_center.remove_observer(self, sender=self.question) ui.remove_question(self.question) self.question = None if self.wave_ringtone: self.wave_ringtone.stop() if failure_reason is None: show_notice('File transfer of %s finished' % os.path.basename(self.filename)) else: show_notice('File transfer of %s failed: %s' % (os.path.basename(self.filename), failure_reason)) self.session = None self.stream = None self.handler = None def handle_notification(self, notification): handler = getattr(self, '_NH_%s' % notification.name, Null) handler(notification) def _NH_UIQuestionGotAnswer(self, notification): notification_center = NotificationCenter() ui = UI() notification_center.remove_observer(self, sender=notification.sender) answer = notification.data.answer self.question = None if answer == 'a': self.session.accept(self.session.proposed_streams) ui.status = 'Accepting...' elif answer == 'r': self.session.reject() ui.status = 'Rejecting...' if IncomingTransferHandler.sessions == 1: if self.wave_ringtone: self.wave_ringtone.stop() self.wave_ringtone = None if IncomingTransferHandler.tone_ringtone: IncomingTransferHandler.tone_ringtone.stop() IncomingTransferHandler.tone_ringtone = None if self.answer_timer is not None and self.answer_timer.active(): self.answer_timer.cancel() def _NH_SIPSessionWillStart(self, notification): ui = UI() if self.question is not None: notification_center = NotificationCenter() notification_center.remove_observer(self, sender=self.question) ui.remove_question(self.question) self.question = None ui.status = 'Connecting...' notification_center = NotificationCenter() notification_center.add_observer(self, sender=notification.sender.proposed_streams[0]) def _NH_SIPSessionDidStart(self, notification): session = notification.sender IncomingCallInitializer.sessions -= 1 ui = UI() ui.status = 'File transfer connected' identity = str(session.remote_identity.uri) if session.remote_identity.display_name: identity = '"%s" <%s>' % (session.remote_identity.display_name, identity) show_notice("File transfer for %s with '%s' started" % (os.path.basename(self.filename), identity)) if IncomingTransferHandler.sessions == 1: if self.wave_ringtone: self.wave_ringtone.stop() self.wave_ringtone = None if IncomingTransferHandler.tone_ringtone: IncomingTransferHandler.tone_ringtone.stop() IncomingTransferHandler.tone_ringtone = None def _NH_SIPSessionDidFail(self, notification): IncomingTransferHandler.sessions -= 1 if self.wave_ringtone: self.wave_ringtone.stop() self.wave_ringtone = None if IncomingTransferHandler.sessions == 0 and IncomingTransferHandler.tone_ringtone is not None: IncomingTransferHandler.tone_ringtone.stop() IncomingTransferHandler.tone_ringtone = None def _NH_MediaStreamDidNotInitialize(self, notification): self._terminate(failure_reason=notification.data.reason) def _NH_FileTransferHandlerDidInitialize(self, notification): self.filename = self.stream.file_selector.name def _NH_FileTransferHandlerProgress(self, notification): ui = UI() ui.status = '%s: %s%%' % (os.path.basename(self.filename), notification.data.transferred_bytes*100//notification.data.total_bytes) def _NH_FileTransferHandlerDidEnd(self, notification): reactor.callFromThread(reactor.callLater, 0, self.session.end) self._terminate(failure_reason=notification.data.reason) class SIPSessionApplication(SIPApplication): # public methods # def __init__(self): self.account = None self.options = None self.target = None self.active_session = None self.message_session_to = None self.outgoing_session = None self.connected_sessions = [] self.sessions_with_proposals = set() self.hangup_timers = {} self.neighbours = {} self.registration_succeeded = {} self.stopped_event = Event() self.received_message_ids = set() self.message_sessions = set() self.ip_address_monitor = IPAddressMonitor() self.logger = None self.rtp_statistics = None self.hold_tone = None self.ignore_local_hold = False self.ignore_local_unhold = False self.smp_verification_question = 'What is the ZRTP authentication string?' self.smp_verifified_using_zrtp = False self.smp_verification_delay = 0 self.smp_verification_tries = 5 self.smp_secret = None def start(self, target, options): notification_center = NotificationCenter() ui = UI() history_file = os.path.join(config_directory, 'input.history') ui.input.add_history(history_file) self.options = options self.target = target self.logger = Logger(sip_to_stdout=options.trace_sip, msrp_to_stdout=options.trace_msrp, pjsip_to_stdout=options.trace_pjsip, notifications_to_stdout=options.trace_notifications) notification_center.add_observer(self, sender=self) notification_center.add_observer(self, sender=ui) notification_center.add_observer(self, name='SIPSessionNewIncoming') notification_center.add_observer(self, name='SIPSessionNewOutgoing') notification_center.add_observer(self, name='RTPStreamDidChangeRTPParameters') notification_center.add_observer(self, name='RTPStreamICENegotiationDidSucceed') notification_center.add_observer(self, name='RTPStreamICENegotiationDidFail') notification_center.add_observer(self, name='SIPEngineGotMessage') log.level.current = log.level.WARNING # get rid of twisted messages control_bindings={'s': 'trace sip', 'm': 'trace msrp', 'j': 'trace pjsip', 'n': 'trace notifications', 'h': 'hangup', 'r': 'record', 'i': 'input', 'o': 'output', 'a': 'alert', 'u': 'mute', ' ': 'hold', 'q': 'quit', '/': 'help', '?': 'help', '0': 'dtmf 0', '1': 'dtmf 1', '2': 'dtmf 2', '3': 'dtmf 3', '4': 'dtmf 4', '5': 'dtmf 5', '6': 'dtmf 6', '7': 'dtmf 7', '8': 'dtmf 8', '9': 'dtmf 9', '*': 'dtmf *', '#': 'dtmf #', 'A': 'dtmf A', 'B': 'dtmf B', 'C': 'dtmf C', 'D': 'dtmf D'} ui.start(control_bindings=control_bindings, display_text=False) Account.register_extension(AccountExtension) BonjourAccount.register_extension(AccountExtension) SIPSimpleSettings.register_extension(SIPSimpleSettingsExtension) try: SIPApplication.start(self, FileStorage(options.config_directory or config_directory)) - except ConfigurationError, e: + except ConfigurationError as e: show_notice("Failed to load sipclient's configuration: %s\n" % str(e), bold=False) show_notice("If an old configuration file is in place, delete it or move it and recreate the configuration using the sip_settings script.", bold=False) ui.stop() self.stopped_event.set() # notification handlers # def _NH_SIPApplicationWillStart(self, notification): account_manager = AccountManager() notification_center = NotificationCenter() settings = SIPSimpleSettings() ui = UI() settings.logs.trace_sip = self.options.trace_sip settings.logs.trace_msrp = self.options.trace_msrp settings.logs.trace_pjsip = self.options.trace_pjsip settings.logs.trace_notifications = self.options.trace_notifications settings.save() for account in account_manager.iter_accounts(): if isinstance(account, Account): account.sip.register = False account.presence.enabled = False account.xcap.enabled = False account.message_summary.enabled = False notification_center.add_observer(self, sender=account) if self.options.account is None: self.account = account_manager.default_account else: possible_accounts = [account for account in account_manager.iter_accounts() if self.options.account in account.id and account.enabled] if len(possible_accounts) > 1: show_notice('More than one account exists which matches %s: %s' % (self.options.account, ', '.join(sorted(account.id for account in possible_accounts))), bold=False) self.stop() return elif len(possible_accounts) == 0: show_notice('No enabled account which matches %s was found. Available and enabled accounts: %s' % (self.options.account, ', '.join(sorted(account.id for account in account_manager.get_accounts() if account.enabled))), bold=False) self.stop() return else: self.account = possible_accounts[0] if isinstance(self.account, Account): self.account.sip.register = True show_notice('Using account %s' % self.account.id, bold=False) ui.prompt = Prompt(self.account.id, foreground='default') self.logger.start() if settings.logs.trace_sip and self.logger._siptrace_filename is not None: show_notice('Logging SIP trace to file "%s"' % self.logger._siptrace_filename, bold=False) if settings.logs.trace_msrp and self.logger._msrptrace_filename is not None: show_notice('Logging MSRP trace to file "%s"' % self.logger._msrptrace_filename, bold=False) if settings.logs.trace_pjsip and self.logger._pjsiptrace_filename is not None: show_notice('Logging PJSIP trace to file "%s"' % self.logger._pjsiptrace_filename, bold=False) if settings.logs.trace_notifications and self.logger._notifications_filename is not None: show_notice('Logging notifications trace to file "%s"' % self.logger._notifications_filename, bold=False) if self.options.disable_sound: settings.audio.input_device = None settings.audio.output_device = None settings.audio.alert_device = None def _NH_SIPApplicationDidStart(self, notification): settings = SIPSimpleSettings() self.ip_address_monitor.start() # set the file transfer directory if it's not set if settings.file_transfer.directory is None: settings.file_transfer.directory = 'file_transfers' # display a list of available devices self._CH_devices() show_notice('Type /help to see a list of available commands.', bold=False) if self.target is not None: call_initializer = OutgoingCallInitializer(self.account, self.target, audio=True) call_initializer.start() def _NH_SIPApplicationWillEnd(self, notification): show_notice('Application will end') self.ip_address_monitor.stop() def _NH_SIPApplicationDidEnd(self, notification): ui = UI() ui.stop() self.stopped_event.set() def _NH_SIPEngineDetectedNATType(self, notification): SIPApplication._NH_SIPEngineDetectedNATType(self, notification) if notification.data.succeeded: show_notice('Detected NAT type: %s' % notification.data.nat_type) def _NH_UIInputGotCommand(self, notification): handler = getattr(self, '_CH_%s' % notification.data.command, None) if handler is not None: try: handler(*notification.data.args) except TypeError: show_notice('Illegal use of command /%s. Type /help for a list of available commands.' % notification.data.command) else: show_notice('Unknown command /%s. Type /help for a list of available commands.' % notification.data.command) def _NH_UIInputGotText(self, notification): msrp_chat = None message_text = notification.data.text local_identity = '%s@%s' % (self.account.id.username, self.account.id.domain) encrypted = False if self.active_session is not None: msrp_chat = next((stream for stream in self.active_session.streams if stream.type == 'chat'), None) if msrp_chat: msrp_chat.send_message(message_text) if msrp_chat.local_identity.display_name: local_identity = msrp_chat.local_identity.display_name else: local_identity = str(msrp_chat.local_identity.uri) else: # compose SMS message pass elif self.message_session_to: # compose SMS message message_session = self.message_session(self.message_session_to) msg_id = message_session.send_message(message_text) message_text = message_text + ' (%s)' % msg_id if message_session.encryption.active: local_identity = '%s encrypted text sent to %s' % (local_identity, self.message_session_to) else: local_identity = '%s clear text sent to %s' % (local_identity, self.message_session_to) else: show_notice('No active chat or message session') return ui = UI() ui.write(RichText('%s> ' % local_identity, foreground='darkred') + message_text) def message_session(self, recipient): try: - message_session = (session for session in self.message_sessions if session.target == recipient and self.account == session.account).next() + message_session = next((session for session in self.message_sessions if session.target == recipient and self.account == session.account)) except StopIteration: message_session = MessageSession(self.account, recipient) self.message_sessions.add(message_session) return message_session def _NH_SIPEngineGotException(self, notification): lines = ['An exception occured within the SIP core:'] lines.extend(notification.data.traceback.split('\n')) show_notice(lines) def _NH_SIPAccountRegistrationDidSucceed(self, notification): account = notification.sender try: s = self.registration_succeeded[account.id] except KeyError: pass else: if s and notification.data.expires == 0: show_notice('%s Registration ended for %s' % (datetime.now().replace(microsecond=0), notification.sender.id)) return contact_header = notification.data.contact_header contact_header_list = notification.data.contact_header_list expires = notification.data.expires registrar = notification.data.registrar now = datetime.now().replace(microsecond=0) lines = ['%s Registered contact "%s" of %s at %s:%d;transport=%s for %d seconds' % (now, contact_header.uri, account.id, registrar.address, registrar.port, registrar.transport, expires)] if len(contact_header_list) > 1: lines.append('%s Other registered contacts of %s:' % (now, account.id)) lines.extend('%s %s of %s for %s seconds' % (now, str(other_contact_header.uri), account.id, other_contact_header.expires) for other_contact_header in contact_header_list if other_contact_header.uri != notification.data.contact_header.uri) if account.contact.public_gruu is not None: lines.append('%s Public GRUU: %s' % (now, account.contact.public_gruu)) if account.contact.temporary_gruu is not None: lines.append('%s Temporary GRUU: %s' % (now, account.contact.temporary_gruu)) show_notice(lines) self.registration_succeeded[account.id] = True def _NH_SIPAccountRegistrationDidFail(self, notification): account = notification.sender if self.active_session is None: show_notice('%s Failed to register contact for %s: %s (retrying in %.2f seconds)' % (datetime.now().replace(microsecond=0), account.id, notification.data.error, notification.data.retry_after)) self.registration_succeeded[notification.sender.id] = False def _NH_SIPAccountRegistrationDidEnd(self, notification): account = notification.sender show_notice('%s Registration ended for %s' % (datetime.now().replace(microsecond=0), account.id)) def _NH_BonjourAccountRegistrationDidSucceed(self, notification): pass #show_notice('%s Registered Bonjour contact "%s"' % (datetime.now().replace(microsecond=0), notification.data.name)) def _NH_BonjourAccountRegistrationDidFail(self, notification): show_notice('%s Failed to register Bonjour contact: %s' % (datetime.now().replace(microsecond=0), notification.data.reason)) def _NH_BonjourAccountRegistrationDidEnd(self, notification): pass #show_notice('%s Bonjour registration ended for %s' % (datetime.now().replace(microsecond=0), notification.sender.id)) def _NH_BonjourAccountDidAddNeighbour(self, notification): neighbour, record = notification.data.neighbour, notification.data.record now = datetime.now().replace(microsecond=0) #show_notice('%s Discovered Bonjour neighbour: "%s (%s)" <%s>' % (now, record.name, record.host, record.uri)) self.neighbours[neighbour] = BonjourNeighbour(neighbour, record.uri, record.name, record.host) def _NH_BonjourAccountDidUpdateNeighbour(self, notification): neighbour, record = notification.data.neighbour, notification.data.record now = datetime.now().replace(microsecond=0) try: bonjour_neighbour = self.neighbours[neighbour] except KeyError: #show_notice('%s Discovered Bonjour neighbour: "%s (%s)" <%s>' % (now, record.name, record.host, record.uri)) self.neighbours[neighbour] = BonjourNeighbour(neighbour, record.uri, record.name, record.host) else: #show_notice('%s Updated Bonjour neighbour: "%s (%s)" <%s>' % (now, record.name, record.host, record.uri)) bonjour_neighbour.display_name = record.name bonjour_neighbour.host = record.host bonjour_neighbour.uri = record.uri def _NH_BonjourAccountDidRemoveNeighbour(self, notification): neighbour = notification.data.neighbour now = datetime.now().replace(microsecond=0) try: bonjour_neighbour = self.neighbours.pop(neighbour) except KeyError: pass else: pass #show_notice('%s Bonjour neighbour left: "%s (%s)" <%s>' % (now, bonjour_neighbour.display_name, bonjour_neighbour.host, bonjour_neighbour.uri)) def _NH_SIPEngineGotMessage(self, notification): data = notification.data call_id = data.headers.get('Call-ID', Null).body try: self.received_message_ids.remove(call_id) except KeyError: self.received_message_ids.add(call_id) else: # drop duplicate message received return content_type = data.content_type if content_type not in ('text/plain', 'text/html', 'message/cpim', 'application/im-iscomposing+xml'): return account = AccountManager().find_account(data.request_uri) is_composing = False from_header = FromHeader.new(data.from_header) identity = '%s@%s' % (str(from_header.uri.user), str(from_header.uri.host)) message_session = self.message_session(identity) parsed_payload = message_session.handle_incoming(data.from_header, content_type, data.body) if parsed_payload is None: return (content_type, content, identity, msg_id, encrypted) = parsed_payload if content_type == 'application/im-iscomposing+xml': # body must not be utf-8 decoded msg = IsComposingMessage.parse(content) state = msg.state.value refresh = msg.refresh.value if msg.refresh is not None else None content_type = msg.content_type.value if msg.content_type is not None else None last_active = msg.last_active.value if msg.last_active is not None else None sender_identity = data.from_header #show_notice('Is composing state=%s, refresh=%s, last_active=%s' % (state, refresh, last_active)) if state == 'active': if refresh is None: refresh = 120 if last_active is not None and (last_active - ISOTimestamp.now() > timedelta(seconds=refresh)): # message is old, discard it return elif state == 'idle': return is_composing = True if content_type != 'application/im-iscomposing+xml': if self.message_session_to != identity: show_notice('Message session started with %s' % identity) self.message_session_to = identity if from_header.display_name: identity = '"%s" <%s>' % (from_header.display_name, identity) if is_composing: show_notice("%s %s is composing a message" % (datetime.now().replace(microsecond=0), identity)) else: if encrypted: show_notice("%s %s wrote: %s (%s encrypted)" % (datetime.now().replace(microsecond=0), identity, content, msg_id)) else: if content.startswith('?OTR:'): show_notice("%s %s wrote a garbled message, probably encryption session must be reset using /otr command" % (datetime.now().replace(microsecond=0), identity)) else: show_notice("%s %s wrote: %s (%s)" % (datetime.now().replace(microsecond=0), identity, content, msg_id)) def _NH_SIPSessionNewIncoming(self, notification): session = notification.sender transfer_streams = [stream for stream in session.proposed_streams if stream.type == 'file-transfer' and stream.direction == 'recvonly'] # only allow sessions with 0 or 1 file transfers if transfer_streams: if len(transfer_streams) > 1: session.reject(488) return transfer_handler = IncomingTransferHandler(session, self.options.auto_answer_interval) transfer_handler.start() else: notification_center = NotificationCenter() notification_center.add_observer(self, sender=session) call_initializer = IncomingCallInitializer(session, self.options.auto_answer_interval) call_initializer.start() def _NH_SIPSessionNewOutgoing(self, notification): session = notification.sender transfer_streams = [stream for stream in session.proposed_streams if stream.type == 'file-transfer'] if not transfer_streams: notification_center = NotificationCenter() notification_center.add_observer(self, sender=session) def _NH_SIPSessionDidFail(self, notification): notification_center = NotificationCenter() notification_center.remove_observer(self, sender=notification.sender) def _NH_SIPSessionWillStart(self, notification): notification_center = NotificationCenter() for stream in notification.sender.proposed_streams: notification_center.add_observer(self, sender=stream) def _NH_SIPSessionDidStart(self, notification): session = notification.sender self.connected_sessions.append(session) if self.active_session is not None: self.active_session.hold() self.active_session = session self.message_session_to = None self._update_prompt() if len(self.connected_sessions) > 1: # this displays the conencted sessions self._CH_sessions() if self.options.auto_hangup_interval is not None: if self.options.auto_hangup_interval == 0: session.end() else: timer = reactor.callLater(self.options.auto_hangup_interval, session.end) self.hangup_timers[id(session)] = timer def _NH_SIPSessionWillEnd(self, notification): notification_center = NotificationCenter() session = notification.sender if id(session) in self.hangup_timers: timer = self.hangup_timers[id(session)] if timer.active(): timer.cancel() del self.hangup_timers[id(session)] hangup_tone = WavePlayer(self.voice_audio_mixer, ResourcePath('sounds/hangup_tone.wav').normalized) notification_center.add_observer(self, sender=hangup_tone) self.voice_audio_bridge.add(hangup_tone) hangup_tone.start() def _NH_SIPSessionDidEnd(self, notification): notification_center = NotificationCenter() session = notification.sender notification_center.remove_observer(self, sender=session) for stream in session.streams or session.proposed_streams or []: notification_center.remove_observer(self, sender=stream) ui = UI() ui.status = None identity = str(session.remote_identity.uri) if session.remote_identity.display_name: identity = '"%s" <%s>' % (session.remote_identity.display_name, identity) if notification.data.end_reason == 'user request': show_notice('SIP session with %s ended by %s party' % (identity, notification.data.originator)) else: show_notice('SIP session with %s ended due to error: %s' % (identity, notification.data.end_reason)) if session.start_time: duration = session.end_time - session.start_time seconds = duration.seconds if duration.microseconds < 500000 else duration.seconds+1 minutes, seconds = seconds / 60, seconds % 60 hours, minutes = minutes / 60, minutes % 60 hours += duration.days*24 if not minutes and not hours: duration_text = '%d seconds' % seconds elif not hours: duration_text = '%02d:%02d' % (minutes, seconds) else: duration_text = '%02d:%02d:%02d' % (hours, minutes, seconds) else: duration_text = '0s' show_notice('Session duration was %s' % duration_text) if session in self.connected_sessions: self.connected_sessions.remove(session) if session is self.active_session: if self.connected_sessions: self.active_session = self.connected_sessions[0] self.message_session_to = None self.active_session.unhold() self.ignore_local_unhold = True identity = str(self.active_session.remote_identity.uri) if self.active_session.remote_identity.display_name: identity = '"%s" <%s>' % (self.active_session.remote_identity.display_name, identity) show_notice('Active SIP session: "%s" (%d/%d)' % (identity, self.connected_sessions.index(self.active_session)+1, len(self.connected_sessions))) else: self.active_session = None self._update_prompt() if session is self.outgoing_session: self.outgoing_session = None on_hold_streams = [stream for stream in chain(*(session.streams for session in self.connected_sessions)) if stream.on_hold] if not on_hold_streams and self.hold_tone is not None: self.hold_tone.stop() self.hold_tone = None def _NH_SIPSessionDidChangeHoldState(self, notification): session = notification.sender if notification.data.on_hold: if notification.data.originator == 'remote': if session is self.active_session: show_notice('Remote party has put the session on hold') else: identity = str(session.remote_identity.uri) if session.remote_identity.display_name: identity = '"%s" <%s>' % (session.remote_identity.display_name, identity) show_notice('%s has put the session on hold' % identity) elif not self.ignore_local_hold: if session is self.active_session: show_notice('Session was put on hold') else: identity = str(session.remote_identity.uri) if session.remote_identity.display_name: identity = '"%s" <%s>' % (session.remote_identity.display_name, identity) show_notice('Session with %s was put on hold' % identity) else: self.ignore_local_hold = False if self.hold_tone is None: self.hold_tone = WavePlayer(self.voice_audio_mixer, ResourcePath('sounds/hold_tone.wav').normalized, loop_count=0, pause_time=30, volume=50) self.voice_audio_bridge.add(self.hold_tone) self.hold_tone.start() else: if notification.data.originator == 'remote': if session is self.active_session: show_notice('Remote party has taken the session out of hold') else: identity = str(session.remote_identity.uri) if session.remote_identity.display_name: identity = '"%s" <%s>' % (session.remote_identity.display_name, identity) show_notice('%s has taken the session out of hold' % identity) elif not self.ignore_local_unhold: if session is self.active_session: show_notice('Session was taken out of hold') else: identity = str(session.remote_identity.uri) if session.remote_identity.display_name: identity = '"%s" <%s>' % (session.remote_identity.display_name, identity) show_notice('Session with %s was taken out of hold' % identity) else: self.ignore_local_unhold = False on_hold_streams = [stream for stream in chain(*(session.streams for session in self.connected_sessions)) if stream.on_hold] if not on_hold_streams and self.hold_tone is not None: self.hold_tone.stop() self.hold_tone = None def _NH_SIPSessionNewProposal(self, notification): self.sessions_with_proposals.add(notification.sender) if notification.data.originator == 'remote': proposal_handler = IncomingProposalHandler(notification.sender) proposal_handler.start() def _NH_SIPSessionDidRenegotiateStreams(self, notification): notification_center = NotificationCenter() for stream in notification.data.added_streams: notification_center.add_observer(self, sender=stream) for stream in notification.data.removed_streams: notification_center.remove_observer(self, sender=stream) session = notification.sender added_streams = ', '.join(stream.type for stream in notification.data.added_streams) removed_streams = ', '.join(stream.type for stream in notification.data.removed_streams) action = 'added' if added_streams else 'removed' message = '%s party %s %s' % (notification.data.originator.capitalize(), action, added_streams or removed_streams) if session is not self.active_session: identity = str(session.remote_identity.uri) if session.remote_identity.display_name: identity = '"%s" <%s>' % (session.remote_identity.display_name, identity) message = '%s in session with %s' % (message, identity) show_notice(message) self._update_prompt() def _NH_AudioStreamGotDTMF(self, notification): notification_center = NotificationCenter() digit = notification.data.digit filename = 'sounds/dtmf_%s_tone.wav' % {'*': 'star', '#': 'pound'}.get(digit, digit) wave_player = WavePlayer(self.voice_audio_mixer, ResourcePath(filename).normalized) notification_center.add_observer(self, sender=wave_player) self.voice_audio_bridge.add(wave_player) wave_player.start() show_notice('Got DMTF %s' % notification.data.digit) def _NH_RTPStreamZRTPVerifiedStateChanged(self, notification): self._update_prompt() def _NH_RTPStreamZRTPPeerNameChanged(self, notification): self._update_prompt() def _NH_RTPStreamDidEnableEncryption(self, notification): stream = notification.sender show_notice("%s encryption activated using %s (%s)" % (stream.type.title(), stream.encryption.type, stream.encryption.cipher)) if stream.encryption.type == 'ZRTP': peer_name = stream.encryption.zrtp.peer_name if stream.encryption.zrtp.peer_name else None show_notice("ZRTP secret is %s" % stream.encryption.zrtp.sas) show_notice("ZRTP peer name is %s, use /zrtp_name command to change it" % (peer_name or 'not set')) show_notice("ZRTP peer is %s, use /zrtp_verified command to toggle it" % ('verified' if stream.encryption.zrtp.verified else 'not verified')) self._update_prompt() def _NH_RTPStreamDidChangeRTPParameters(self, notification): stream = notification.sender show_notice('Audio RTP parameters changed:') show_notice('Audio stream using "%s" codec at %sHz' % (stream.codec, stream.sample_rate)) show_notice('Audio RTP endpoints %s:%d <-> %s:%d' % (stream.local_rtp_address, stream.local_rtp_port, stream.remote_rtp_address, stream.remote_rtp_port)) if stream.encryption.active: show_notice('RTP audio stream is encrypted using %s (%s)\n' % (stream.encryption.type, stream.encryption.cipher)) def _NH_AudioStreamDidStartRecordingAudio(self, notification): show_notice('Recording audio to %s' % notification.data.filename) def _NH_AudioStreamDidStopRecordingAudio(self, notification): show_notice('Stopped recording audio to %s' % notification.data.filename) def _NH_ChatStreamSMPVerificationDidStart(self, notification): show_notice('OTR verification started', bold=False) data = notification.data session = self.active_session stream = notification.sender if data.originator == 'remote': if data.question == self.smp_verification_question: try: audio_stream = next(stream for stream in session.streams if stream.type=='audio' and stream.encryption.type=='ZRTP' and stream.encryption.active and stream.encryption.zrtp.verified) except StopIteration: stream.encryption.smp_abort() else: show_notice("OTR verification requested by remote and replied automatically using ZRTP SAS", bold=False) stream.encryption.smp_answer(audio_stream.encryption.zrtp.sas) self.smp_verifified_using_zrtp = True else: show_notice("OTR verification requested by remote, reply using /otr_answer") show_notice('OTR verification question: %s' % data.question) self._update_prompt() else: try: audio_stream = next(stream for stream in session.streams if stream.type=='audio' and stream.encryption.type=='ZRTP' and stream.encryption.active and stream.encryption.zrtp.verified) except StopIteration: pass else: stream.encryption.smp_verify(audio_stream.encryption.zrtp.sas, question=self.smp_verification_question) show_notice("OTR verification performed automatically using ZRTP SAS...", bold=False) self._update_prompt() def _NH_ChatStreamSMPVerificationDidNotStart(self, notification): pass def _NH_ChatStreamSMPVerificationDidEnd(self, notification): data = notification.data stream = notification.sender if data.status is SMPStatus.Success: show_notice("OTR verification %s" % ('succeeded' if data.same_secrets else 'failed: the secret is wrong'), bold=False) stream.encryption.verified = data.same_secrets self._update_prompt() elif data.status is SMPStatus.Interrupted: show_notice("OTR verification aborted: %s" % data.reason, bold=False) elif data.status is SMPStatus.ProtocolError: show_notice("OTR verification error: %s" % data.reason, bold=False) if data.reason == 'startup collision': self.smp_verification_tries -= 1 self.smp_verification_delay *= 2 if self.smp_verification_tries > 0: # TODO verify later pass def _NH_ChatStreamOTRError(self, notification): show_notice("Chat encryption error: %s", notification.data.error, bold=False) def _NH_ChatStreamOTRVerifiedStateChanged(self, notification): self._update_prompt() def _NH_ChatStreamOTREncryptionStateChanged(self, notification): data = notification.data stream = notification.sender if data.new_state is OTRState.Encrypted: local_fingerprint = stream.encryption.key_fingerprint.encode('hex').upper() remote_fingerprint = stream.encryption.peer_fingerprint.encode('hex').upper() show_notice("Chat encryption activated using OTR protocol", bold=False) show_notice("OTR local fingerprint %s" % local_fingerprint, bold=False) show_notice("OTR remote fingerprint %s" % remote_fingerprint, bold=False) if stream.encryption.verified: show_notice("OTR remote fingerprint has been verified", bold=False) else: show_notice("OTR remote fingerprint has not yet been verified", bold=False) elif data.new_state is OTRState.Finished: show_notice("Chat encryption deactivated", bold=False) elif data.new_state is OTRState.Plaintext: show_notice("Chat encryption deactivated", bold=False) self._update_prompt() def _NH_ChatStreamGotMessage(self, notification): if not notification.data.message.content_type.startswith("text/"): return remote_identity = notification.data.message.sender.display_name or notification.data.message.sender.uri doc = html.fromstring(notification.data.message.content) if doc.body.text is not None: doc.body.text = doc.body.text.lstrip('\n') for br in doc.xpath('.//br'): br.tail = '\n' + (br.tail or '') head = RichText('%s> ' % remote_identity, foreground='blue') ui = UI() ui.writelines([head + line for line in doc.body.text_content().splitlines()]) def _NH_DefaultAudioDeviceDidChange(self, notification): SIPApplication._NH_DefaultAudioDeviceDidChange(self, notification) if notification.data.changed_input and self.voice_audio_mixer.input_device=='system_default': show_notice('Switched default input device to: %s' % self.voice_audio_mixer.real_input_device) if notification.data.changed_output and self.voice_audio_mixer.output_device=='system_default': show_notice('Switched default output device to: %s' % self.voice_audio_mixer.real_output_device) if notification.data.changed_output and self.alert_audio_mixer.output_device=='system_default': show_notice('Switched alert device to: %s' % self.alert_audio_mixer.real_output_device) def _NH_AudioDevicesDidChange(self, notification): old_devices = set(notification.data.old_devices) new_devices = set(notification.data.new_devices) added_devices = new_devices - old_devices removed_devices = old_devices - new_devices changed_input_device = self.voice_audio_mixer.real_input_device in removed_devices changed_output_device = self.voice_audio_mixer.real_output_device in removed_devices changed_alert_device = self.alert_audio_mixer.real_output_device in removed_devices SIPApplication._NH_AudioDevicesDidChange(self, notification) if added_devices: show_notice('Added audio device(s): %s' % ', '.join(sorted(added_devices))) if removed_devices: show_notice('Removed audio device(s): %s' % ', '.join(sorted(removed_devices))) if changed_input_device: show_notice('Input device has been switched to: %s' % self.voice_audio_mixer.real_input_device) if changed_output_device: show_notice('Output device has been switched to: %s' % self.voice_audio_mixer.real_output_device) if changed_alert_device: show_notice('Alert device has been switched to: %s' % self.alert_audio_mixer.real_output_device) def _NH_WavePlayerDidEnd(self, notification): notification_center = NotificationCenter() notification_center.remove_observer(self, sender=notification.sender) def _NH_RTPStreamICENegotiationDidSucceed(self, notification): show_notice(" ") show_notice("ICE negotiation succeeded in %s" % notification.data.duration) if self.rtp_statistics: show_notice(" ") show_notice("Local ICE candidates:") for candidate in notification.data.local_candidates: show_notice(str(candidate)) show_notice(" ") show_notice("Remote ICE candidates:") for candidate in notification.data.remote_candidates: show_notice(str(candidate)) show_notice(" ") show_notice("ICE connectivity valid pairs:") for check in notification.data.valid_pairs: show_notice(str(check)) show_notice(" ") def _NH_RTPStreamICENegotiationDidFail(self, notification): show_notice("\n") show_notice("ICE negotiation failed: %s" % notification.data.reason) # command handlers # def _CH_call(self, target): if self.outgoing_session is not None: show_notice('Please cancel any outgoing sessions before makeing any new ones') return call_initializer = OutgoingCallInitializer(self.account, target, audio=True, chat=True) call_initializer.start() def _CH_audio(self, target, chat_option=None): if chat_option and chat_option != '+chat': raise TypeError() if self.outgoing_session is not None: show_notice('Please cancel any outgoing sessions before makeing any new ones') return call_initializer = OutgoingCallInitializer(self.account, target, audio=True, chat=chat_option=='+chat') call_initializer.start() def _CH_message(self, target=None): if not target: show_notice('Usage: /message user@domain') try: SIPURI.parse('sip:%s' % target) if self.message_session_to != target: message_session = self.message_session(target) self.message_session_to = target except SIPCoreError: show_notice('Invalid SIP URI %s' % target) def _CH_chat(self, target, audio_option=None): if audio_option and audio_option != '+audio': raise TypeError() if self.outgoing_session is not None: show_notice('Please cancel any outgoing sessions before makeing any new ones') return call_initializer = OutgoingCallInitializer(self.account, target, audio=audio_option=='+audio', chat=True) call_initializer.start() def _CH_send(self, target, filepath): if '~' in filepath: filepath = os.path.expanduser(filepath) filepath = os.path.abspath(filepath) transfer_handler = OutgoingTransferHandler(self.account, target, filepath) transfer_handler.start() def _CH_next(self): if len(self.connected_sessions) > 1: self.active_session.hold() self.active_session = self.connected_sessions[(self.connected_sessions.index(self.active_session)+1) % len(self.connected_sessions)] self.message_session_to = None self.active_session.unhold() self.ignore_local_unhold = True identity = str(self.active_session.remote_identity.uri) if self.active_session.remote_identity.display_name: identity = '"%s" <%s>' % (self.active_session.remote_identity.display_name, identity) show_notice('Active SIP session: "%s" (%d/%d)' % (identity, self.connected_sessions.index(self.active_session)+1, len(self.connected_sessions))) self._update_prompt() def _CH_prev(self): if len(self.connected_sessions) > 1: self.active_session.hold() self.active_session = self.connected_sessions[self.connected_sessions.index(self.active_session)-1] self.message_session_to = None self.active_session.unhold() self.ignore_local_unhold = True identity = str(self.active_session.remote_identity.uri) if self.active_session.remote_identity.display_name: identity = '"%s" <%s>' % (self.active_session.remote_identity.display_name, identity) show_notice('Active SIP session: "%s" (%d/%d)' % (identity, self.connected_sessions.index(self.active_session)+1, len(self.connected_sessions))) self._update_prompt() def _CH_sessions(self): if self.connected_sessions: lines = ['Connected sessions:'] for session in self.connected_sessions: identity = str(session.remote_identity.uri) if session.remote_identity.display_name: identity = '"%s" <%s>' % (session.remote_identity.display_name, identity) lines.append(' SIP session with %s (%d/%d) - %s' % (identity, self.connected_sessions.index(session)+1, len(self.connected_sessions), 'active' if session is self.active_session else 'on hold')) if len(self.connected_sessions) > 1: lines.append('Use the /next and /prev commands to switch the active session') show_notice(lines) else: show_notice('There are no connected sessions') def _CH_neighbours(self): if not isinstance(self.account, BonjourAccount): show_notice('This command is only available if using the Bonjour account') return lines = ['Bonjour neighbours:'] - for key, neighbour in self.neighbours.iteritems(): + for key, neighbour in self.neighbours.items(): lines.append(' "%s (%s)" <%s>' % (neighbour.display_name, neighbour.host, neighbour.uri)) show_notice(lines) def _CH_trace(self, *types): if not types: lines = [] lines.append('SIP tracing to console is now %s' % ('active' if self.logger.sip_to_stdout else 'inactive')) lines.append('MSRP tracing to console is now %s' % ('active' if self.logger.msrp_to_stdout else 'inactive')) lines.append('PJSIP tracing to console is now %s' % ('active' if self.logger.pjsip_to_stdout else 'inactive')) lines.append('Notification tracing to console is now %s' % ('active' if self.logger.notifications_to_stdout else 'inactive')) show_notice(lines) return add_types = [type[1:] for type in types if type[0] == '+'] remove_types = [type[1:] for type in types if type[0] == '-'] toggle_types = [type for type in types if type[0] not in ('+', '-')] settings = SIPSimpleSettings() if 'sip' in add_types or ('sip' in toggle_types and not self.logger.sip_to_stdout): self.logger.sip_to_stdout = True settings.logs.trace_sip = True show_notice('SIP tracing to console is now activated') elif 'sip' in remove_types or ('sip' in toggle_types and self.logger.sip_to_stdout): self.logger.sip_to_stdout = False settings.logs.trace_sip = False show_notice('SIP tracing to console is now deactivated') if 'msrp' in add_types or ('msrp' in toggle_types and not self.logger.msrp_to_stdout): self.logger.msrp_to_stdout = True settings.logs.trace_msrp = True show_notice('MSRP tracing to console is now activated') elif 'msrp' in remove_types or ('msrp' in toggle_types and self.logger.msrp_to_stdout): self.logger.msrp_to_stdout = False settings.logs.trace_msrp = False show_notice('MSRP tracing to console is now deactivated') if 'pjsip' in add_types or ('pjsip' in toggle_types and not self.logger.pjsip_to_stdout): self.logger.pjsip_to_stdout = True settings.logs.trace_pjsip = True show_notice('PJSIP tracing to console is now activated') elif 'pjsip' in remove_types or ('pjsip' in toggle_types and self.logger.pjsip_to_stdout): self.logger.pjsip_to_stdout = False settings.logs.trace_pjsip = False show_notice('PJSIP tracing to console is now deactivated') if 'notifications' in add_types or ('notifications' in toggle_types and not self.logger.notifications_to_stdout): self.logger.notifications_to_stdout = True settings.logs.trace_notifications = True show_notice('Notification tracing to console is now activated') elif 'notifications' in remove_types or ('notifications' in toggle_types and self.logger.notifications_to_stdout): self.logger.notifications_to_stdout = False settings.logs.trace_notifications = False show_notice('Notification tracing to console is now deactivated') settings.save() def _CH_rtp(self, state='toggle'): if state == 'toggle': new_state = self.rtp_statistics is None elif state == 'on': new_state = True elif state == 'off': new_state = False else: raise TypeError() if (self.rtp_statistics and new_state) or (not self.rtp_statistics and not new_state): return if new_state: self.rtp_statistics = RTPStatisticsThread() self.rtp_statistics.start() show_notice('Output of RTP statistics and ICE negotiation results on console is now activated') else: self.rtp_statistics.stop() self.rtp_statistics = None show_notice('Output of RTP statistics and ICE negotiation results on console is now dectivated') def _CH_mute(self, state='toggle'): if state == 'toggle': self.voice_audio_mixer.muted = not self.voice_audio_mixer.muted elif state == 'on': self.voice_audio_mixer.muted = True elif state == 'off': self.voice_audio_mixer.muted = False show_notice('The microphone is now %s' % ('muted' if self.voice_audio_mixer.muted else 'unmuted')) def _CH_input(self, device=None): engine = Engine() settings = SIPSimpleSettings() - input_devices = [None, u'system_default'] + sorted(engine.input_devices) + input_devices = [None, 'system_default'] + sorted(engine.input_devices) if device is None: if self.voice_audio_mixer.input_device in input_devices: old_input_device = self.voice_audio_mixer.input_device else: old_input_device = None tail_length = settings.audio.echo_canceller.tail_length if settings.audio.echo_canceller.enabled else 0 new_input_device = input_devices[(input_devices.index(old_input_device)+1) % len(input_devices)] try: self.voice_audio_mixer.set_sound_devices(new_input_device, self.voice_audio_mixer.output_device, tail_length) - except SIPCoreError, e: + except SIPCoreError as e: show_notice('Failed to set input device to %s: %s' % (new_input_device, str(e))) else: - if new_input_device == u'system_default': + if new_input_device == 'system_default': show_notice('Input device changed to %s (system default device)' % self.voice_audio_mixer.real_input_device) else: show_notice('Input device changed to %s' % new_input_device) else: device = device.decode(sys.getfilesystemencoding()) - if device == u'None': + if device == 'None': device = None elif device not in input_devices: show_notice('Unknown input device %s. Type /devices to see a list of available devices' % device) return tail_length = settings.audio.echo_canceller.tail_length if settings.audio.echo_canceller.enabled else 0 try: self.voice_audio_mixer.set_sound_devices(device, self.voice_audio_mixer.output_device, self.voice_audio_mixer.tail_length) - except SIPCoreError, e: + except SIPCoreError as e: show_notice('Failed to set input device to %s: %s' % (device, str(e))) else: - if device == u'system_default': + if device == 'system_default': show_notice('Input device changed to %s (system default device)' % self.voice_audio_mixer.real_input_device) else: show_notice('Input device changed to %s' % device) def _CH_output(self, device=None): engine = Engine() settings = SIPSimpleSettings() - output_devices = [None, u'system_default'] + sorted(engine.output_devices) + output_devices = [None, 'system_default'] + sorted(engine.output_devices) if device is None: if self.voice_audio_mixer.output_device in output_devices: old_output_device = self.voice_audio_mixer.output_device else: old_output_device = None tail_length = settings.audio.echo_canceller.tail_length if settings.audio.echo_canceller.enabled else 0 new_output_device = output_devices[(output_devices.index(old_output_device)+1) % len(output_devices)] try: self.voice_audio_mixer.set_sound_devices(self.voice_audio_mixer.input_device, new_output_device, tail_length) - except SIPCoreError, e: + except SIPCoreError as e: show_notice('Failed to set output device to %s: %s' % (new_output_device, str(e))) else: - if new_output_device == u'system_default': + if new_output_device == 'system_default': show_notice('Output device changed to %s (system default device)' % self.voice_audio_mixer.real_output_device) else: show_notice('Output device changed to %s' % new_output_device) else: device = device.decode(sys.getfilesystemencoding()) - if device == u'None': + if device == 'None': device = None elif device not in output_devices: show_notice('Unknown output device %s. Type /devices to see a list of available devices' % device) return tail_length = settings.audio.echo_canceller.tail_length if settings.audio.echo_canceller.enabled else 0 try: self.voice_audio_mixer.set_sound_devices(self.voice_audio_mixer.input_device, device, tail_length) - except SIPCoreError, e: + except SIPCoreError as e: show_notice('Failed to set output device to %s: %s' % (device, str(e))) else: - if device == u'system_default': + if device == 'system_default': show_notice('Output device changed to %s (system default device)' % self.voice_audio_mixer.real_output_device) else: show_notice('Output device changed to %s' % device) def _CH_alert(self, device=None): engine = Engine() settings = SIPSimpleSettings() - output_devices = [None, u'system_default'] + sorted(engine.output_devices) + output_devices = [None, 'system_default'] + sorted(engine.output_devices) if device is None: if self.alert_audio_mixer.output_device in output_devices: old_output_device = self.alert_audio_mixer.output_device else: old_output_device = None tail_length = settings.audio.echo_canceller.tail_length if settings.audio.echo_canceller.enabled else 0 new_output_device = output_devices[(output_devices.index(old_output_device)+1) % len(output_devices)] try: self.alert_audio_mixer.set_sound_devices(self.alert_audio_mixer.input_device, new_output_device, tail_length) - except SIPCoreError, e: + except SIPCoreError as e: old_output_device = new_output_device show_notice('Failed to set alert device to %s: %s' % (new_output_device, str(e))) else: - if new_output_device == u'system_default': + if new_output_device == 'system_default': show_notice('Alert device changed to %s (system default device)' % self.alert_audio_mixer.real_output_device) else: show_notice('Alert device changed to %s' % new_output_device) else: device = device.decode(sys.getfilesystemencoding()) - if device == u'None': + if device == 'None': device = None elif device not in output_devices: show_notice('Unknown output device %s. Type /devices to see a list of available devices' % device) return tail_length = settings.audio.echo_canceller.tail_length if settings.audio.echo_canceller.enabled else 0 try: self.alert_audio_mixer.set_sound_devices(self.alert_audio_mixer.input_device, device, tail_length) - except SIPCoreError, e: + except SIPCoreError as e: show_notice('Failed to set alert device to %s: %s' % (device, str(e))) else: - if device == u'system_default': + if device == 'system_default': show_notice('Alert device changed to %s (system default device)' % self.alert_audio_mixer.real_output_device) else: show_notice('Alert device changed to %s' % device) def _CH_devices(self): engine = Engine() show_notice('Available audio input devices: %s' % ', '.join(['None', 'system_default'] + sorted(engine.input_devices)), bold=False) show_notice('Available audio output devices: %s' % ', '.join(['None', 'system_default'] + sorted(engine.output_devices)), bold=False) if self.voice_audio_mixer.input_device == 'system_default': show_notice('Using audio input device: %s (system default device)' % self.voice_audio_mixer.real_input_device, bold=False) else: show_notice('Using audio input device: %s' % self.voice_audio_mixer.input_device, bold=False) if self.voice_audio_mixer.output_device == 'system_default': show_notice('Using audio output device: %s (system default device)' % self.voice_audio_mixer.real_output_device, bold=False) else: show_notice('Using audio output device: %s' % self.voice_audio_mixer.output_device, bold=False) if self.alert_audio_mixer.output_device == 'system_default': show_notice('Using audio alert device: %s (system default device)' % self.alert_audio_mixer.real_output_device, bold=False) else: show_notice('Using audio alert device: %s' % self.alert_audio_mixer.output_device, bold=False) def _CH_help(self): self._print_help() def _CH_quit(self): self.stop() def _CH_eof(self): ui = UI() if self.active_session is not None: if self.active_session in self.sessions_with_proposals: ui.status = 'Cancelling proposal...' self.active_session.cancel_proposal() else: ui.status = 'Ending SIP session...' self.active_session.end() elif self.outgoing_session is not None: ui.status = 'Cancelling SIP session...' self.outgoing_session.end() else: self.stop() def _CH_hangup(self): if self.active_session is not None: show_notice('Ending SIP session...') self.active_session.end() elif self.outgoing_session is not None: show_notice('Cancelling SIP session...') self.outgoing_session.end() else: try: - message_session = (session for session in self.message_sessions if session.target == self.message_session_to and self.account == session.account).next() + message_session = next((session for session in self.message_sessions if session.target == self.message_session_to and self.account == session.account)) except StopIteration: pass else: message_session.end() try: self.message_sessions.remove(message_session) except KeyError: show_notice('Message session not found') pass @run_in_green_thread def _CH_dtmf(self, tones): if self.active_session is not None: audio_stream = next((stream for stream in self.active_session.streams if stream.type == 'audio'), None) if audio_stream is not None: notification_center = NotificationCenter() for digit in tones: filename = 'sounds/dtmf_%s_tone.wav' % {'*': 'star', '#': 'pound'}.get(digit, digit) wave_player = WavePlayer(self.voice_audio_mixer, ResourcePath(filename).normalized) notification_center.add_observer(self, sender=wave_player) audio_stream.send_dtmf(digit) if self.active_session.account.rtp.inband_dtmf: audio_stream.bridge.add(wave_player) self.voice_audio_bridge.add(wave_player) wave_player.start() api.sleep(0.3) def _CH_record(self, state='toggle'): if self.active_session is None: return audio_stream = next((stream for stream in self.active_session.streams if stream.type == 'audio'), None) if audio_stream is not None: if state == 'toggle': new_state = audio_stream.recorder is None elif state == 'on': new_state = True elif state == 'off': new_state = False else: show_notice('Illegal argument to /record. Type /help for a list of available commands.') return if new_state: settings = SIPSimpleSettings() direction = self.active_session.direction remote = "%s@%s" % (self.active_session.remote_identity.uri.user, self.active_session.remote_identity.uri.host) filename = "%s-%s-%s.wav" % (datetime.now().strftime("%Y%m%d-%H%M%S"), remote, direction) path = os.path.join(settings.audio.directory.normalized, self.active_session.account.id) audio_stream.start_recording(os.path.join(path, filename)) else: audio_stream.stop_recording() def _CH_account(self, *args): account_manager = AccountManager() if not args or args[0] == 'list': show_notice('Accounts available: %s' % ", ".join(account.id for account in account_manager.iter_accounts() if account != self.account and account.enabled)) elif args and args[0] == 'register': if not self.account.sip.register: self.registration_succeeded[self.account.id] = False self.account.sip.register = not self.account.sip.register self.account.save() elif args and args[0] == 'enroll': settings = SIPSimpleSettings() try: username = args[1].strip() password = args[2].strip() email = args[3].strip() display_name = " ".join(args[4:]) if not "@" in email: show_notice('Email address is not valid, must be user@domain') return if not display_name: show_notice('Please specify a display name') return sip_address = '%s@%s' % (username, settings.enrollment.default_domain) if account_manager.has_account(sip_address): show_notice('Account %s is already defined' % sip_address) return except IndexError: show_notice('To enroll an account you must provide: username password email display_name') return else: data = self.enroll(username, password, email, display_name) if data['error']: show_notice(data['error']) else: show_notice('Account %s created' % data['sip_address']) elif args: new_account = args[0] possible_accounts = [account for account in account_manager.iter_accounts() if new_account in account.id and account.enabled] if len(possible_accounts) > 1: show_notice('More than one account exists which matches %s: %s' % (new_account, ', '.join(sorted(account.id for account in possible_accounts))), bold=False) elif len(possible_accounts) == 0: show_notice('No enabled account which matches %s was found. Available and enabled accounts: %s' % (self.options.account, ', '.join(sorted(possible_accounts))), bold=False) elif possible_accounts[0] == self.account: show_notice('Same account selected') elif possible_accounts[0] != self.account: self.switch_account(possible_accounts[0]) def switch_account(self, account): if isinstance(self.account, Account): self.account.sip.register = False self.account.save() self.registration_succeeded[self.account.id] = False self.account = account show_notice('Switching account to %s' % self.account.id) notification_center = NotificationCenter() notification_center.add_observer(self, sender=account) if isinstance(self.account, Account): self.account.sip.register = True self.account.save() self._update_prompt() def enroll(self, username, password, email, display_name): settings = SIPSimpleSettings() tzname = datetime.now(tzlocal()).tzname() or "" post_data = {'password' : password.encode("utf8"), 'username' : username.encode("utf8"), 'email' : email.encode("utf8"), 'display_name' : display_name.encode("utf8"), 'tzinfo' : tzname } return_data = {} try: r = requests.post(settings.enrollment.url, timeout=5, data=post_data) settings = SIPSimpleSettings() show_notice("Enrollment request sent to %s" % settings.enrollment.url) if r.status_code == 200: body = r.json() if not body["success"]: return_data['error'] = "Enrollment failed: %s" % body["error_message"] else: data = defaultdict(lambda: None, body) sip_address = data['sip_address'] outbound_proxy = data['outbound_proxy'] try: account = Account(sip_address) except ValueError as e: return_data['error'] = "Local account create error: %s" % str(e) else: account.auth.password = password account.sip.always_use_my_proxy = True account.sip.register = True account.rtp.srtp_encryption = 'opportunistic' account.nat_traversal.msrp_relay = data['msrp_relay'] if data['outbound_proxy']: account.sip.outbound_proxy = data['outbound_proxy'] if data['xcap_root']: account.xcap.xcap_root = data['xcap_root'] account.xcap.enabled = True account.enabled = True account.save() account_manager = AccountManager() account_manager.default_account = account self.switch_account(account) return_data = data else: return_data['error'] = "Enrollment server error code %s" % r.status_code except Exception as e: return_data['error'] = "Enrollment exception: %s" % str(e) return return_data def _CH_otr(self): if self.active_session is None: message_session = self.message_session(self.message_session_to) if not message_session: return if message_session.encryption.active: show_notice("Message encryption will stop") message_session.encryption.stop() else: show_notice("Message encryption will start") message_session.encryption.start() return chat_stream = next((stream for stream in self.active_session.streams if stream.type == 'chat'), None) if chat_stream is None: return if chat_stream.encryption.active: show_notice("Chat encryption will stop") chat_stream.encryption.stop() else: show_notice("Chat encryption will start") chat_stream.encryption.start() def _CH_otr_secret(self, *args): secret = " ".join(args) if args else None if secret is None and self.smp_secret: show_notice("OTR secret answer is: %s" % self.smp_secret) return self.smp_secret = secret show_notice("OTR secret answer is now set to: %s" % secret) def _CH_otr_answer(self, *args): answer = " ".join(args) if args else None if self.active_session is None: message_session = self.message_session(self.message_session_to) if not message_session: show_notice("OTR functions can be used only during active chat sessions") return message_session.encryption.smp_answer(answer) chat_stream = next((stream for stream in self.active_session.streams if stream.type == 'chat'), None) if chat_stream is None: show_notice("OTR functions can be used only during active chat sessions") return if answer: chat_stream.encryption.smp_answer(answer) def _CH_otr_question(self, *args): question = " ".join(args) if args else None if self.active_session is None: message_session = self.message_session(self.message_session_to) if not message_session: show_notice("OTR functions can be used only during active chat sessions") return if question: show_notice("OTR SMP verification question will be asked: %s" % question) message_session.encryption.verified = False message_session.encryption.smp_verify(self.smp_secret, question=question) return chat_stream = next((stream for stream in self.active_session.streams if stream.type == 'chat'), None) audio_stream = next((stream for stream in self.active_session.streams if stream.type == 'audio'), None) if chat_stream is None: show_notice("OTR functions can be used only during active chat sessions") return if self.smp_secret is None: show_notice("First set the SMP secret answer using /otr_secret command") return if question: show_notice("OTR SMP verification question will be asked: %s" % question) chat_stream.encryption.verified = False chat_stream.encryption.smp_verify(self.smp_secret, question=question) def _CH_zrtp_verified(self): if self.active_session is None: show_notice("ZRTP functions can be used only during active audio sessions") return audio_stream = next((stream for stream in self.active_session.streams if stream.type == 'audio'), None) if audio_stream is None: show_notice("ZRTP functions can be used only during active audio sessions") return audio_stream.encryption.zrtp.verified = not audio_stream.encryption.zrtp.verified def _CH_zrtp_name(self, *args): if self.active_session is None: show_notice("ZRTP functions can be used only during active audio sessions") return audio_stream = next((stream for stream in self.active_session.streams if stream.type == 'audio'), None) if audio_stream is None: show_notice("ZRTP functions can be used only during active audio sessions") return peer_name = " ".join(args) if args else None audio_stream.encryption.zrtp.peer_name = peer_name def _CH_hold(self, state='toggle'): if self.active_session is not None: if state == 'toggle': new_state = not self.active_session.on_hold elif state == 'on': new_state = True elif state == 'off': new_state = False else: show_notice('Illegal argument to /hold. Type /help for a list of available commands.') return if new_state: self.active_session.hold() else: self.active_session.unhold() def _CH_add(self, stream_name): if self.active_session is None: show_notice('There is no active session') return if stream_name in (stream.type for stream in self.active_session.streams): show_notice('The active session already has a %s stream' % stream_name) return proposal_handler = OutgoingProposalHandler(self.active_session, **{stream_name: True}) try: proposal_handler.start() except IllegalStateError: show_notice('Cannot add a stream while another transaction is in progress') def _CH_remove(self, stream_name): if self.active_session is None: show_notice('There is no active session') return try: - stream = (stream for stream in self.active_session.streams if stream.type==stream_name).next() + stream = next((stream for stream in self.active_session.streams if stream.type==stream_name)) except StopIteration: show_notice('The current active session does not have any %s streams' % stream_name) else: try: self.active_session.remove_stream(stream) except IllegalStateError: show_notice('Cannot remove a stream while another transaction is in progress') def _CH_add_participant(self, uri): if self.active_session is None: show_notice('There is no active session') return if re.match('^(sip:|sips:)', uri) is None: uri = 'sip:%s' % uri try: uri = SIPURI.parse(uri) except SIPCoreError: show_notice('Invalid SIP URI') else: self.active_session.conference.add_participant(uri) def _CH_remove_participant(self, uri): if self.active_session is None: show_notice('There is no active session') return if re.match('^(sip:|sips:)', uri) is None: uri = 'sip:%s' % uri try: uri = SIPURI.parse(uri) except SIPCoreError: show_notice('Invalid SIP URI') else: self.active_session.conference.remove_participant(uri) def _CH_transfer(self, uri): if self.active_session is None: show_notice('There is no active session') return if re.match('^(sip:|sips:)', uri) is None: uri = 'sip:%s' % uri try: uri = SIPURI.parse(uri) except SIPCoreError: show_notice('Invalid SIP URI') else: self.active_session.transfer(uri) def _CH_nickname(self, nickname): if self.active_session is not None: try: - chat_stream = (stream for stream in self.active_session.streams if stream.type=='chat').next() + chat_stream = next((stream for stream in self.active_session.streams if stream.type=='chat')) except StopIteration: return try: chat_stream.set_local_nickname(nickname) - except Exception, e: + except Exception as e: show_notice('Error setting nickname: %s' % e) # private methods # def _print_help(self): lines = [] lines.append('General commands:') lines.append(' /account [list|register|user@domain|enroll user password email display_name]: account management') lines.append(' /call {user[@domain]}: call the specified user using audio and chat') lines.append(' /audio {user[@domain]} [+chat]: call the specified user using audio and possibly chat') lines.append(' /chat {user[@domain]} [+audio]: call the specified user using chat and possibly audio') lines.append(' /message {user[@domain]}: start a message session') lines.append(' /send {user[@domain]} {file}: initiate a file transfer with the specified user') lines.append(' /next: select the next connected session') lines.append(' /prev: select the previous connected session') lines.append(' /sessions: show the list of connected sessions') if isinstance(self.account, BonjourAccount): lines.append(' /neighbours: show the list of bonjour neighbours') lines.append(' /trace [[+|-]sip] [[+|-]msrp] [[+|-]pjsip] [[+|-]notifications]: toggle/set tracing on the console (ctrl-x s | ctrl-x m | ctrl-x j | ctrl-x n)') lines.append(' /rtp [on|off]: toggle/set printing RTP statistics and ICE negotiation results on the console (ctrl-x p)') lines.append(' /mute [on|off]: mute the microphone (ctrl-x u)') lines.append(' /input [device]: change audio input device (ctrl-x i)') lines.append(' /output [device]: change audio output device (ctrl-x o)') lines.append(' /alert [device]: change audio alert device (ctrl-x a)') lines.append(' /quit: quit the program (ctrl-x q)') lines.append(' /help: display this help message (ctrl-x ?)') lines.append('In call commands:') lines.append(' /hangup: hang-up the active session (ctrl-x h)') lines.append(' /dtmf {0-9|*|#|A-D}...: send DTMF tones (ctrl-x 0-9|*|#|A-D)') lines.append(' /record [on|off]: toggle/set audio recording (ctrl-x r)') lines.append(' /hold [on|off]: hold/unhold (ctrl-x SPACE)') lines.append(' /zrtp_verified: toggle verified flag for ZRTP peer (both parties must do it)') lines.append(' /zrtp_name name: set name for ZRTP peer') lines.append(' /otr: toggle OTR encryption for the chat stream') lines.append(' /otr_answer answer: Answer OTR verification question using SMP protocol') lines.append(' /otr_secret [secret]: show or set OTR secret') lines.append(' /otr_question question: Ask OTR verification question using SMP protocol') lines.append(' /add {chat|audio}: add a stream to the current session') lines.append(' /remove {chat|audio}: remove a stream from the current session') lines.append(' /add_participant {user@domain}: add the specified user to the conference') lines.append(' /remove_participant {user@domain}: remove the specified user from the conference') lines.append(' /transfer {user@domain}: transfer (using blind transfer) callee to the specified destination') lines.append(' /nickname nick: set the user nickname whithin a conference') show_notice(lines, bold=False) def _update_prompt(self): ui = UI() session = self.active_session if session is None: ui.prompt = Prompt('%s@%s' % (self.account.id.username, self.account.id.domain), foreground='default') else: info = [] identity = '%s@%s' % (session.remote_identity.uri.user, session.remote_identity.uri.host) if session.remote_identity.display_name: identity = '%s (%s)' % (session.remote_identity.display_name, identity) streams = '/'.join(stream.type.capitalize() for stream in session.streams) chat_stream = next((stream for stream in session.streams if stream.type == 'chat'), None) audio_stream = next((stream for stream in session.streams if stream.type == 'audio'), None) if chat_stream and chat_stream.encryption.active: info.append('Chat encrypted verified' if chat_stream.encryption.verified else 'Chat encrypted NOT verified') if audio_stream and audio_stream.encryption.active: if audio_stream.encryption.zrtp: if audio_stream.encryption.zrtp.verified: if audio_stream.encryption.zrtp.peer_name: info.append('Audio encrypted verified with %s' % audio_stream.encryption.zrtp.peer_name) else: info.append('Audio encrypted verified') else: info.append('Audio encrypted NOT verified') else: info.append('Audio encrypted NOT verified') if not info: info = ['not encrypted'] ui.prompt = Prompt('%s to %s (%s)' % (streams, identity, ", ".join(info)), foreground='darkred') def parse_handle_call_option(option, opt_str, value, parser, name): try: value = parser.rargs[0] except IndexError: value = 0 else: if value == '' or value[0] == '-': value = 0 else: try: value = int(value) except ValueError: value = 0 else: del parser.rargs[0] setattr(parser.values, name, value) if __name__ == '__main__': description = '%prog is a command-line client for handling multiple audio, chat and file-transfer sessions' usage = '%prog [options] [user@domain]' parser = OptionParser(usage=usage, description=description) parser.print_usage = parser.print_help parser.add_option('-a', '--account', type='string', dest='account', help='The account name to use for any outgoing traffic. If not supplied, the default account will be used.', metavar='NAME') parser.add_option('-c', '--config-directory', type='string', dest='config_directory', help='The configuration directory to use. This overrides the default location.') parser.add_option('-s', '--trace-sip', action='store_true', dest='trace_sip', default=False, help='Dump the raw contents of incoming and outgoing SIP messages.') parser.add_option('-m', '--trace-msrp', action='store_true', dest='trace_msrp', default=False, help='Dump msrp logging information and the raw contents of incoming and outgoing MSRP messages.') parser.add_option('-j', '--trace-pjsip', action='store_true', dest='trace_pjsip', default=False, help='Print PJSIP logging output.') parser.add_option('-n', '--trace-notifications', action='store_true', dest='trace_notifications', default=False, help='Print all notifications (disabled by default).') parser.add_option('-S', '--disable-sound', action='store_true', dest='disable_sound', default=False, help='Disables initializing the sound card.') parser.set_default('auto_answer_interval', None) parser.add_option('--auto-answer', action='callback', callback=parse_handle_call_option, callback_args=('auto_answer_interval',), help='Interval after which to answer an incoming session (disabled by default). If the option is specified but the interval is not, it defaults to 0 (accept the session as soon as it starts ringing).', metavar='[INTERVAL]') parser.set_default('auto_hangup_interval', None) parser.add_option('--auto-hangup', action='callback', callback=parse_handle_call_option, callback_args=('auto_hangup_interval',), help='Interval after which to hang up an established session (disabled by default). If the option is specified but the interval is not, it defaults to 0 (hangup the session as soon as it connects).', metavar='[INTERVAL]') options, args = parser.parse_args() target = args[0] if args else None application = SIPSessionApplication() application.start(target, options) signal.signal(signal.SIGINT, signal.SIG_DFL) application.stopped_event.wait() sleep(0.1) diff --git a/sip-settings b/sip-settings index 66853cc..22e3599 100755 --- a/sip-settings +++ b/sip-settings @@ -1,380 +1,374 @@ -#!/usr/bin/env python2 +#!/usr/bin/env python3 import fcntl import re import struct import sys import termios from collections import deque from optparse import OptionParser from sipsimple.account import Account, BonjourAccount, AccountManager from sipsimple.application import SIPApplication from sipsimple.configuration import ConfigurationError, ConfigurationManager, DefaultValue, Setting, SettingsGroupMeta from sipsimple.configuration.datatypes import List, STUNServerAddress from sipsimple.configuration.settings import SIPSimpleSettings from sipsimple.storage import FileStorage from sipsimple.threading import ThreadManager from sipclient.configuration import config_directory from sipclient.configuration.account import AccountExtension from sipclient.configuration.settings import SIPSimpleSettingsExtension def format_child(obj, attrname, maxchars): linebuf = attrname if isinstance(getattr(type(obj), attrname, None), Setting): attr = getattr(obj, attrname) - if isinstance(attr, unicode): - string = attr.encode(sys.getfilesystemencoding()) - else: - string = str(attr) + string = str(attr) if maxchars is not None: maxchars -= len(attrname)+4 if len(string) > maxchars: string = string[:maxchars-3]+'...' linebuf += ' = ' + string return linebuf def display_object(obj, name): # get terminal width if sys.stdout.isatty(): width = struct.unpack('HHHH', fcntl.ioctl(sys.stdout.fileno(), termios.TIOCGWINSZ, struct.pack('HHHH', 0, 0, 0, 0)))[1] else: width = None children = deque([child for child in dir(type(obj)) if isinstance(getattr(type(obj), child, None), Setting)] + \ [child for child in dir(type(obj)) if isinstance(getattr(type(obj), child, None), SettingsGroupMeta)]) # display first line linebuf = ' '*(len(name)+3) + '+' if children: linebuf += '-- ' + format_child(obj, children.popleft(), width-(len(name)+7) if width is not None else None) - print linebuf + print(linebuf) # display second line linebuf = name + ' --|' if children: linebuf += '-- ' + format_child(obj, children.popleft(), width-(len(name)+7) if width is not None else None) - print linebuf + print(linebuf) # display the rest of the lines if children: while children: child = children.popleft() linebuf = ' '*(len(name)+3) + ('|' if children else '+') + '-- ' + format_child(obj, child, width-(len(name)+7) if width is not None else None) - print linebuf + print(linebuf) else: linebuf = ' '*(len(name)+3) + '+' - print linebuf + print(linebuf) - print + print() [display_object(getattr(obj, child), child) for child in dir(type(obj)) if isinstance(getattr(type(obj), child, None), SettingsGroupMeta)] class SettingsParser(object): @classmethod def parse_default(cls, type, value): if issubclass(type, List): values = re.split(r'\s*,\s*', value) return values elif issubclass(type, bool): if value.lower() == 'true': return True else: return False - elif issubclass(type, unicode): - if isinstance(value, str): - return value.decode(sys.getfilesystemencoding()) - return value else: return value @classmethod def parse_MSRPRelayAddress(cls, type, value): return type.from_description(value) @classmethod def parse_SIPProxyAddress(cls, type, value): return type.from_description(value) @classmethod def parse_STUNServerAddress(cls, type, value): return type.from_description(value) @classmethod def parse_STUNServerAddressList(cls, type, value): values = re.split(r'\s*,\s*', value) return [STUNServerAddress.from_description(v) for v in values] @classmethod def parse_PortRange(cls, type, value): return type(*value.split(':', 1)) @classmethod def parse_Resolution(cls, type, value): return type(*value.split('x', 1)) @classmethod def parse_SoundFile(cls, type, value): if ',' in value: path, volume = value.split(',', 1) else: path, volume = value, 100 return type(path, volume) @classmethod def parse_AccountSoundFile(cls, type, value): if ',' in value: path, volume = value.split(',', 1) else: path, volume = value, 100 return type(path, volume) @classmethod def parse(cls, type, value): if value == 'None': return None if value == 'DEFAULT': return DefaultValue parser = getattr(cls, 'parse_%s' % type.__name__, cls.parse_default) return parser(type, value) class AccountConfigurator(object): def __init__(self): Account.register_extension(AccountExtension) BonjourAccount.register_extension(AccountExtension) self.configuration_manager = ConfigurationManager() self.configuration_manager.start() self.account_manager = AccountManager() self.account_manager.load() def list(self): - print 'Accounts:' + print('Accounts:') bonjour_account = BonjourAccount() accounts = [account for account in self.account_manager.get_accounts() if account.id != bonjour_account.id] - accounts.sort(cmp=lambda a, b: cmp(a.id, b.id)) + accounts.sort(key=lambda x: x.id) accounts.append(bonjour_account) for account in accounts: - print ' %s (%s)%s' % (account.id, 'enabled' if account.enabled else 'disabled', ' - default_account' if account is self.account_manager.default_account else '') + print(' %s (%s)%s' % (account.id, 'enabled' if account.enabled else 'disabled', ' - default_account' if account is self.account_manager.default_account else '')) def add(self, sip_address, password): if self.account_manager.has_account(sip_address): - print 'Account %s already exists' % sip_address + print('Account %s already exists' % sip_address) return try: account = Account(sip_address) - except ValueError, e: - print 'Cannot add SIP account: %s' % str(e) + except ValueError as e: + print('Cannot add SIP account: %s' % str(e)) return account.auth.password = password account.enabled = True account.save() - print 'Account added' + print('Account added') def delete(self, sip_address): if sip_address != 'ALL': possible_accounts = [account for account in self.account_manager.iter_accounts() if sip_address in account.id] if len(possible_accounts) > 1: - print "More than one account exists which matches %s: %s" % (sip_address, ", ".join(sorted(account.id for account in possible_accounts))) + print("More than one account exists which matches %s: %s" % (sip_address, ", ".join(sorted(account.id for account in possible_accounts)))) return if len(possible_accounts) == 0: - print 'Account %s does not exist' % sip_address + print('Account %s does not exist' % sip_address) return account = possible_accounts[0] if account == BonjourAccount(): - print 'Cannot delete bonjour account' + print('Cannot delete bonjour account') return account.delete() - print 'Account deleted' + print('Account deleted') else: for account in self.account_manager.get_accounts(): account.delete() - print 'Accounts deleted' + print('Accounts deleted') def show(self, sip_address=None): if sip_address is None: accounts = [self.account_manager.default_account] if accounts[0] is None: - print "No accounts configured" + print("No accounts configured") return else: if sip_address != 'ALL': accounts = [account for account in self.account_manager.iter_accounts() if sip_address in account.id] else: accounts = self.account_manager.get_accounts() if not accounts: - print 'No accounts which match %s' % sip_address + print('No accounts which match %s' % sip_address) return for account in accounts: - print 'Account %s:' % account.id + print('Account %s:' % account.id) display_object(account, 'account') def set(self, *args): if not args: raise TypeError("set must receive at least one argument") if '=' in args[0]: accounts = [self.account_manager.default_account] if accounts[0] is None: - print "No accounts configured" + print("No accounts configured") return else: sip_address = args[0] args = args[1:] if sip_address != 'ALL': accounts = [account for account in self.account_manager.iter_accounts() if sip_address in account.id] else: accounts = self.account_manager.get_accounts() if not accounts: - print 'No accounts which match %s' % sip_address + print('No accounts which match %s' % sip_address) return try: settings = dict(arg.split('=', 1) for arg in args) except ValueError: - print 'Illegal arguments: %s' % ' '.join(args) + print('Illegal arguments: %s' % ' '.join(args)) return for account in accounts: - for attrname, value in settings.iteritems(): + for attrname, value in settings.items(): object = account name = attrname while '.' in name: local_name, name = name.split('.', 1) + print(dir(account)) try: object = getattr(object, local_name) except AttributeError: - print 'Unknown setting: %s' % attrname + print('Unknown setting: %s' % attrname) object = None break if object is not None: try: attribute = getattr(type(object), name) value = SettingsParser.parse(attribute.type, value) setattr(object, name, value) except AttributeError: - print 'Unknown setting: %s' % attrname - except ValueError, e: - print '%s: %s' % (attrname, str(e)) + print('Unknown setting: %s' % attrname) + except ValueError as e: + print('%s: %s' % (attrname, str(e))) account.save() - print 'Account%s updated' % ('s' if len(accounts) > 1 else '') + print('Account%s updated' % ('s' if len(accounts) > 1 else '')) def default(self, sip_address): possible_accounts = [account for account in self.account_manager.iter_accounts() if sip_address in account.id] if len(possible_accounts) > 1: - print "More than one account exists which matches %s: %s" % (sip_address, ", ".join(sorted(account.id for account in possible_accounts))) + print("More than one account exists which matches %s: %s" % (sip_address, ", ".join(sorted(account.id for account in possible_accounts)))) return if len(possible_accounts) == 0: - print 'Account %s does not exist' % sip_address + print('Account %s does not exist' % sip_address) return account = possible_accounts[0] try: self.account_manager.default_account = account - except ValueError, e: - print str(e) + except ValueError as e: + print(str(e)) return - print 'Account %s is now default account' % account.id + print('Account %s is now default account' % account.id) class SIPSimpleConfigurator(object): def __init__(self): SIPSimpleSettings.register_extension(SIPSimpleSettingsExtension) self.configuration_manager = ConfigurationManager() self.configuration_manager.start() SIPSimpleSettings() def show(self): - print 'SIP SIMPLE settings:' + print('SIP SIMPLE settings:') display_object(SIPSimpleSettings(), 'SIP SIMPLE') def set(self, *args): sipsimple_settings = SIPSimpleSettings() try: settings = dict(arg.split('=', 1) for arg in args) except ValueError: - print 'Illegal arguments: %s' % ' '.join(args) + print('Illegal arguments: %s' % ' '.join(args)) return - for attrname, value in settings.iteritems(): + for attrname, value in settings.items(): object = sipsimple_settings name = attrname while '.' in name: local_name, name = name.split('.', 1) try: object = getattr(object, local_name) except AttributeError: - print 'Unknown setting: %s' % attrname + print('Unknown setting: %s' % attrname) object = None break if object is not None: try: attribute = getattr(type(object), name) value = SettingsParser.parse(attribute.type, value) setattr(object, name, value) except AttributeError: - print 'Unknown setting: %s' % attrname - except ValueError, e: - print '%s: %s' % (attrname, str(e)) + print('Unknown setting: %s' % attrname) + except ValueError as e: + print('%s: %s' % (attrname, str(e))) sipsimple_settings.save() - print 'SIP SIMPLE general settings updated' + print('SIP SIMPLE general settings updated') if __name__ == '__main__': description = "This script manages the SIP SIMPLE client SDK settings." usage = """%prog [--general|--account] [options] command [arguments] %prog --general show %prog --general set key1=value1 [key2=value2 ...] %prog --account list %prog --account add user@domain password %prog --account delete user@domain|ALL %prog --account show [user@domain|ALL] %prog --account set [user@domain|ALL] key1=value1|DEFAULT [key2=value2|DEFAULT ...] %prog --account default user@domain""" parser = OptionParser(usage=usage, description=description) parser.print_usage = parser.print_help parser.add_option('-c', '--config-directory', type='string', dest='config_directory', help='The configuration directory to use. This overrides the default location.') parser.add_option("-a", "--account", action="store_true", dest="account", help="Manage SIP accounts' settings") parser.add_option("-g", "--general", action="store_true", dest="general", help="Manage general SIP SIMPLE middleware settings") options, args = parser.parse_args() # exactly one of -a or -g must be specified if (not (options.account or options.general)) or (options.account and options.general): parser.print_usage() sys.exit(1) # there must be at least one command if not args: sys.stderr.write("Error: no command specified\n") parser.print_usage() sys.exit(1) SIPApplication.storage = FileStorage(options.config_directory or config_directory) thread_manager = ThreadManager() thread_manager.start() # execute the handlers try: if options.account: object = AccountConfigurator() else: object = SIPSimpleConfigurator() - except ConfigurationError, e: + except ConfigurationError as e: sys.stderr.write("Failed to load sipclient's configuration: %s\n" % str(e)) sys.stderr.write("If an old configuration file is in place, delete it or move it and recreate the configuration using the sip_settings script.\n") else: command, args = args[0], args[1:] handler = getattr(object, command, None) if handler is None or not callable(handler): sys.stderr.write("Error: illegal command: %s\n" % command) parser.print_usage() sys.exit(1) try: handler(*args) except TypeError: sys.stderr.write("Error: illegal usage of command %s\n" % command) parser.print_usage() sys.exit(1) finally: thread_manager.stop() diff --git a/sip-subscribe-mwi b/sip-subscribe-mwi index afa7034..ecb0ff9 100755 --- a/sip-subscribe-mwi +++ b/sip-subscribe-mwi @@ -1,380 +1,380 @@ -#!/usr/bin/env python2 +#!/usr/bin/env python3 import os import random import select import sys import termios from collections import deque from optparse import OptionParser from threading import Thread from time import time from application import log from application.notification import IObserver, NotificationCenter, NotificationData from application.python.queue import EventQueue from eventlib.twistedutil import join_reactor from twisted.internet import reactor from twisted.internet.error import ReactorNotRunning -from zope.interface import implements +from zope.interface import implementer from sipsimple.account import Account, AccountManager, BonjourAccount from sipsimple.application import SIPApplication from sipsimple.configuration import ConfigurationError, ConfigurationManager from sipsimple.configuration.settings import SIPSimpleSettings from sipsimple.core import ContactHeader, Engine, FromHeader, Header, Route, RouteHeader, SIPCoreError, SIPURI, Subscription, ToHeader from sipsimple.lookup import DNSLookup from sipsimple.payloads.messagesummary import MessageSummary from sipsimple.storage import FileStorage from sipsimple.threading import run_in_twisted_thread from sipclient.configuration import config_directory from sipclient.configuration.account import AccountExtension from sipclient.configuration.settings import SIPSimpleSettingsExtension from sipclient.log import Logger class InputThread(Thread): def __init__(self, application): Thread.__init__(self) self.application = application self.daemon = True self._old_terminal_settings = None def run(self): notification_center = NotificationCenter() while True: for char in self._getchars(): if char == "\x04": self.application.stop() return else: notification_center.post_notification('SAInputWasReceived', sender=self, data=NotificationData(input=char)) def stop(self): self._termios_restore() def _termios_restore(self): if self._old_terminal_settings is not None: termios.tcsetattr(sys.stdin.fileno(), termios.TCSADRAIN, self._old_terminal_settings) def _getchars(self): fd = sys.stdin.fileno() if os.isatty(fd): self._old_terminal_settings = termios.tcgetattr(fd) new = termios.tcgetattr(fd) new[3] = new[3] & ~termios.ICANON & ~termios.ECHO - new[6][termios.VMIN] = '\000' + new[6][termios.VMIN] = b'\000' try: termios.tcsetattr(fd, termios.TCSADRAIN, new) if select.select([fd], [], [], None)[0]: return sys.stdin.read(4192) finally: self._termios_restore() else: return os.read(fd, 4192) +@implementer(IObserver) class SubscriptionApplication(object): - implements(IObserver) def __init__(self, account_name, target, trace_sip, trace_pjsip, trace_notifications): self.account_name = account_name self.target = target self.input = InputThread(self) self.output = EventQueue(lambda event: sys.stdout.write(event+'\n')) self.logger = Logger(sip_to_stdout=trace_sip, pjsip_to_stdout=trace_pjsip, notifications_to_stdout=trace_notifications) self.success = False self.account = None self.subscription = None self.stopping = False self._subscription_routes = None self._subscription_timeout = 0.0 self._subscription_wait = 0.5 account_manager = AccountManager() engine = Engine() notification_center = NotificationCenter() notification_center.add_observer(self, sender=account_manager) notification_center.add_observer(self, sender=engine) notification_center.add_observer(self, sender=self.input) log.level.current = log.level.WARNING def run(self): account_manager = AccountManager() configuration = ConfigurationManager() engine = Engine() # start output thread self.output.start() # startup configuration Account.register_extension(AccountExtension) BonjourAccount.register_extension(AccountExtension) SIPSimpleSettings.register_extension(SIPSimpleSettingsExtension) SIPApplication.storage = FileStorage(config_directory) try: configuration.start() - except ConfigurationError, e: + except ConfigurationError as e: raise RuntimeError("Failed to load sipclient's configuration: %s\nIf an old configuration file is in place, delete it or move it and recreate the configuration using the sip_settings script." % str(e)) account_manager.load() if self.account_name is None: self.account = account_manager.default_account else: possible_accounts = [account for account in account_manager.iter_accounts() if self.account_name in account.id and account.enabled] if len(possible_accounts) > 1: raise RuntimeError("More than one account exists which matches %s: %s" % (self.account_name, ", ".join(sorted(account.id for account in possible_accounts)))) if len(possible_accounts) == 0: raise RuntimeError("No enabled account that matches %s was found. Available and enabled accounts: %s" % (self.account_name, ", ".join(sorted(account.id for account in account_manager.get_accounts() if account.enabled)))) self.account = possible_accounts[0] if self.account is None: raise RuntimeError("Unknown account %s. Available accounts: %s" % (self.account_name, ', '.join(account.id for account in account_manager.iter_accounts()))) elif self.account == BonjourAccount(): raise RuntimeError("Cannot use bonjour account for message summary subscription") elif not self.account.message_summary.enabled: raise RuntimeError("Message summary is not enabled for account %s" % self.account.id) for account in account_manager.iter_accounts(): if account == self.account: account.sip.register = False else: account.enabled = False self.output.put('Using account %s' % self.account.id) settings = SIPSimpleSettings() # start logging self.logger.start() # start the engine engine.start( auto_sound=False, events={'message-summary': ['application/simple-message-summary']}, udp_port=settings.sip.udp_port if "udp" in settings.sip.transport_list else None, tcp_port=settings.sip.tcp_port if "tcp" in settings.sip.transport_list else None, tls_port=settings.sip.tls_port if "tls" in settings.sip.transport_list else None, tls_verify_server=self.account.tls.verify_server, tls_ca_file=os.path.expanduser(settings.tls.ca_list) if settings.tls.ca_list else None, tls_cert_file=os.path.expanduser(self.account.tls.certificate) if self.account.tls.certificate else None, tls_privkey_file=os.path.expanduser(self.account.tls.certificate) if self.account.tls.certificate else None, user_agent=settings.user_agent, sample_rate=settings.audio.sample_rate, rtp_port_range=(settings.rtp.port_range.start, settings.rtp.port_range.end), trace_sip=settings.logs.trace_sip or self.logger.sip_to_stdout, log_level=settings.logs.pjsip_level if (settings.logs.trace_pjsip or self.logger.pjsip_to_stdout) else 0 ) if self.target is None: self.target = ToHeader(SIPURI(user=self.account.id.username, host=self.account.id.domain)) else: if '@' not in self.target: self.target = '%s@%s' % (self.target, self.account.id.domain) if not self.target.startswith('sip:') and not self.target.startswith('sips:'): self.target = 'sip:' + self.target try: self.target = ToHeader(SIPURI.parse(self.target)) except SIPCoreError: self.output.put('Illegal SIP URI: %s' % self.target) engine.stop() return 1 self.output.put('Subscribing to %s for the message-summary event' % self.target.uri) # start the input thread self.input.start() reactor.callLater(0, self._subscribe) # start twisted try: reactor.run() finally: self.input.stop() # stop the output self.output.stop() self.output.join() self.logger.stop() return 0 if self.success else 1 def stop(self): self.stopping = True if self.subscription is not None and self.subscription.state.lower() in ('accepted', 'pending', 'active'): self.subscription.end(timeout=1) else: engine = Engine() engine.stop() def print_help(self): message = 'Available control keys:\n' message += ' t: toggle SIP trace on the console\n' message += ' j: toggle PJSIP trace on the console\n' message += ' n: toggle notifications trace on the console\n' message += ' Ctrl-d: quit the program\n' message += ' ?: display this help message\n' self.output.put('\n'+message) def handle_notification(self, notification): handler = getattr(self, '_NH_%s' % notification.name, None) if handler is not None: handler(notification) def _NH_SIPSubscriptionDidStart(self, notification): route = Route(notification.sender.route_header.uri.host, notification.sender.route_header.uri.port, notification.sender.route_header.uri.parameters.get('transport', 'udp')) self._subscription_routes = None self._subscription_wait = 0.5 self.output.put('Subscription succeeded at %s:%d;transport=%s' % (route.address, route.port, route.transport)) self.success = True def _NH_SIPSubscriptionChangedState(self, notification): route = Route(notification.sender.route_header.uri.host, notification.sender.route_header.uri.port, notification.sender.route_header.uri.parameters.get('transport', 'udp')) if notification.data.state.lower() == "pending": self.output.put('Subscription pending at %s:%d;transport=%s' % (route.address, route.port, route.transport)) elif notification.data.state.lower() == "active": self.output.put('Subscription active at %s:%d;transport=%s' % (route.address, route.port, route.transport)) def _NH_SIPSubscriptionDidEnd(self, notification): notification_center = NotificationCenter() notification_center.remove_observer(self, sender=notification.sender) self.subscription = None route = Route(notification.sender.route_header.uri.host, notification.sender.route_header.uri.port, notification.sender.route_header.uri.parameters.get('transport', 'udp')) self.output.put('Unsubscribed from %s:%d;transport=%s' % (route.address, route.port, route.transport)) self.stop() def _NH_SIPSubscriptionDidFail(self, notification): notification_center = NotificationCenter() notification_center.remove_observer(self, sender=notification.sender) self.subscription = None route = Route(notification.sender.route_header.uri.host, notification.sender.route_header.uri.port, notification.sender.route_header.uri.parameters.get('transport', 'udp')) if notification.data.code: status = ': %d %s' % (notification.data.code, notification.data.reason) else: status = ': %s' % notification.data.reason self.output.put('Subscription failed at %s:%d;transport=%s%s' % (route.address, route.port, route.transport, status)) if self.stopping or notification.data.code in (401, 403, 407) or self.success: self.success = False self.stop() else: if not self._subscription_routes or time() > self._subscription_timeout: self._subscription_wait = min(self._subscription_wait*2, 30) timeout = random.uniform(self._subscription_wait, 2*self._subscription_wait) reactor.callFromThread(reactor.callLater, timeout, self._subscribe) else: route = self._subscription_routes.popleft() route_header = RouteHeader(route.uri) self.subscription = Subscription(self.target.uri, FromHeader(self.account.uri, self.account.display_name), self.target, ContactHeader(self.account.contact[route]), "message-summary", route_header, credentials=self.account.credentials, refresh=self.account.sip.subscribe_interval) notification_center.add_observer(self, sender=self.subscription) self.subscription.subscribe(extra_headers=[Header('Supported', 'eventlist')], timeout=5) def _NH_SIPSubscriptionGotNotify(self, notification): if notification.data.body: ms = MessageSummary.parse(notification.data.body) self.output.put('\nReceived NOTIFY:\n' + ms.to_string()) self.print_help() def _NH_DNSLookupDidSucceed(self, notification): # create subscription and register to get notifications from it self._subscription_routes = deque(notification.data.result) route = self._subscription_routes.popleft() route_header = RouteHeader(route.uri) self.subscription = Subscription(self.target.uri, FromHeader(self.account.uri, self.account.display_name), self.target, ContactHeader(self.account.contact[route]), "message-summary", route_header, credentials=self.account.credentials, refresh=self.account.sip.subscribe_interval) notification_center = NotificationCenter() notification_center.add_observer(self, sender=self.subscription) self.subscription.subscribe(extra_headers=[Header('Supported', 'eventlist')], timeout=5) def _NH_DNSLookupDidFail(self, notification): self.output.put('DNS lookup failed: %s' % notification.data.error) timeout = random.uniform(1.0, 2.0) reactor.callLater(timeout, self._subscribe) def _NH_SAInputWasReceived(self, notification): engine = Engine() settings = SIPSimpleSettings() key = notification.data.input if key == 't': self.logger.sip_to_stdout = not self.logger.sip_to_stdout engine.trace_sip = self.logger.sip_to_stdout or settings.logs.trace_sip self.output.put('SIP tracing to console is now %s.' % ('activated' if self.logger.sip_to_stdout else 'deactivated')) elif key == 'j': self.logger.pjsip_to_stdout = not self.logger.pjsip_to_stdout engine.log_level = settings.logs.pjsip_level if (self.logger.pjsip_to_stdout or settings.logs.trace_pjsip) else 0 self.output.put('PJSIP tracing to console is now %s.' % ('activated' if self.logger.pjsip_to_stdout else 'deactivated')) elif key == 'n': self.logger.notifications_to_stdout = not self.logger.notifications_to_stdout self.output.put('Notification tracing to console is now %s.' % ('activated' if self.logger.notifications_to_stdout else 'deactivated')) elif key == '?': self.print_help() @run_in_twisted_thread def _NH_SIPEngineDidEnd(self, notification): self._stop_reactor() @run_in_twisted_thread def _NH_SIPEngineDidFail(self, notification): self.output.put('Engine failed.') self._stop_reactor() def _NH_SIPEngineGotException(self, notification): self.output.put('An exception occured within the SIP core:\n'+notification.data.traceback) def _stop_reactor(self): try: reactor.stop() except ReactorNotRunning: pass def _subscribe(self): settings = SIPSimpleSettings() self._subscription_timeout = time()+30 lookup = DNSLookup() notification_center = NotificationCenter() notification_center.add_observer(self, sender=lookup) if self.account.sip.outbound_proxy is not None: uri = SIPURI(host=self.account.sip.outbound_proxy.host, port=self.account.sip.outbound_proxy.port, parameters={'transport': self.account.sip.outbound_proxy.transport}) elif self.account.sip.always_use_my_proxy: uri = SIPURI(host=self.account.id.domain) else: uri = self.target.uri lookup.lookup_sip_proxy(uri, settings.sip.transport_list) if __name__ == "__main__": description = "This script subscribes to the message summary event package for the specified SIP target. When a NOTIFY is received with the message summary information it will be displayed. The program will un-SUBSCRIBE and quit when CTRL+D is pressed." usage = "%prog [options] [target-user@target-domain.com]" parser = OptionParser(usage=usage, description=description) parser.print_usage = parser.print_help parser.add_option("-a", "--account-name", type="string", dest="account_name", help="The name of the account to use.") parser.add_option("-s", "--trace-sip", action="store_true", dest="trace_sip", default=False, help="Dump the raw contents of incoming and outgoing SIP messages (disabled by default).") parser.add_option("-j", "--trace-pjsip", action="store_true", dest="trace_pjsip", default=False, help="Print PJSIP logging output (disabled by default).") parser.add_option("-n", "--trace-notifications", action="store_true", dest="trace_notifications", default=False, help="Print all notifications (disabled by default).") options, args = parser.parse_args() try: application = SubscriptionApplication(options.account_name, args[0] if args else None, options.trace_sip, options.trace_pjsip, options.trace_notifications) return_code = application.run() - except RuntimeError, e: - print "Error: %s" % str(e) + except RuntimeError as e: + print("Error: %s" % str(e)) sys.exit(1) - except SIPCoreError, e: - print "Error: %s" % str(e) + except SIPCoreError as e: + print("Error: %s" % str(e)) sys.exit(1) else: sys.exit(return_code) diff --git a/sip-subscribe-presence b/sip-subscribe-presence index 372f917..b412ddd 100755 --- a/sip-subscribe-presence +++ b/sip-subscribe-presence @@ -1,620 +1,620 @@ -#!/usr/bin/env python2 +#!/usr/bin/env python3 import datetime import os import random import select import sys import termios -import urllib +import urllib.request, urllib.parse, urllib.error from collections import deque from optparse import OptionParser from threading import Thread from time import time from application import log from application.notification import IObserver, NotificationCenter, NotificationData from application.python.queue import EventQueue from eventlib.twistedutil import join_reactor from twisted.internet import reactor from twisted.internet.error import ReactorNotRunning -from zope.interface import implements +from zope.interface import implementer from sipsimple.account import Account, AccountManager, BonjourAccount from sipsimple.application import SIPApplication from sipsimple.lookup import DNSLookup from sipsimple.configuration import ConfigurationError, ConfigurationManager from sipsimple.configuration.settings import SIPSimpleSettings from sipsimple.core import ContactHeader, Engine, FromHeader, RouteHeader, SIPCoreError, SIPURI, Subscription, ToHeader, Route from sipsimple.payloads import ParserError from sipsimple.payloads import rpid # needed to register RPID extensions from sipsimple.payloads.pidf import Device, Person, Service, PIDF, PIDFDocument from sipsimple.storage import FileStorage from sipsimple.threading import run_in_twisted_thread from sipclient.configuration import config_directory from sipclient.configuration.account import AccountExtension from sipclient.configuration.settings import SIPSimpleSettingsExtension from sipclient.log import Logger class InputThread(Thread): def __init__(self, application): Thread.__init__(self) self.application = application self.daemon = True self._old_terminal_settings = None def run(self): notification_center = NotificationCenter() while True: for char in self._getchars(): if char == "\x04": self.application.stop() return else: notification_center.post_notification('SAInputWasReceived', sender=self, data=NotificationData(input=char)) def stop(self): self._termios_restore() def _termios_restore(self): if self._old_terminal_settings is not None: termios.tcsetattr(sys.stdin.fileno(), termios.TCSADRAIN, self._old_terminal_settings) def _getchars(self): fd = sys.stdin.fileno() if os.isatty(fd): self._old_terminal_settings = termios.tcgetattr(fd) new = termios.tcgetattr(fd) new[3] = new[3] & ~termios.ICANON & ~termios.ECHO - new[6][termios.VMIN] = '\000' + new[6][termios.VMIN] = b'\000' try: termios.tcsetattr(fd, termios.TCSADRAIN, new) if select.select([fd], [], [], None)[0]: return sys.stdin.read(4192) finally: self._termios_restore() else: return os.read(fd, 4192) +@implementer(IObserver) class SubscriptionApplication(object): - implements(IObserver) def __init__(self, account_name, target, trace_sip, trace_pjsip, trace_notifications): self.account_name = account_name self.target = target self.input = InputThread(self) self.output = EventQueue(self._write) self.logger = Logger(sip_to_stdout=trace_sip, pjsip_to_stdout=trace_pjsip, notifications_to_stdout=trace_notifications) self.success = False self.account = None self.subscription = None self.stopping = False self._subscription_routes = None self._subscription_timeout = 0.0 self._subscription_wait = 0.5 account_manager = AccountManager() engine = Engine() notification_center = NotificationCenter() notification_center.add_observer(self, sender=account_manager) notification_center.add_observer(self, sender=engine) notification_center.add_observer(self, sender=self.input) log.level.current = log.level.WARNING def _write(self, message): - if isinstance(message, unicode): + if isinstance(message, str): message = message.encode(sys.getfilesystemencoding()) - sys.stdout.write(message+'\n') + sys.stdout.write(message.decode()+'\n') def run(self): account_manager = AccountManager() configuration = ConfigurationManager() engine = Engine() # start output thread self.output.start() # startup configuration Account.register_extension(AccountExtension) BonjourAccount.register_extension(AccountExtension) SIPSimpleSettings.register_extension(SIPSimpleSettingsExtension) SIPApplication.storage = FileStorage(config_directory) try: configuration.start() - except ConfigurationError, e: + except ConfigurationError as e: raise RuntimeError("failed to load sipclient's configuration: %s\nIf an old configuration file is in place, delete it or move it and recreate the configuration using the sip_settings script." % str(e)) account_manager.load() if self.account_name is None: self.account = account_manager.default_account else: possible_accounts = [account for account in account_manager.iter_accounts() if self.account_name in account.id and account.enabled] if len(possible_accounts) > 1: raise RuntimeError("More than one account exists which matches %s: %s" % (self.account_name, ", ".join(sorted(account.id for account in possible_accounts)))) if len(possible_accounts) == 0: raise RuntimeError("No enabled account that matches %s was found. Available and enabled accounts: %s" % (self.account_name, ", ".join(sorted(account.id for account in account_manager.get_accounts() if account.enabled)))) self.account = possible_accounts[0] if self.account is None: raise RuntimeError("Unknown account %s. Available accounts: %s" % (self.account_name, ', '.join(account.id for account in account_manager.iter_accounts()))) elif self.account == BonjourAccount(): raise RuntimeError("Cannot use bonjour account for presence subscription") elif not self.account.presence.enabled: raise RuntimeError("Presence is not enabled for account %s" % self.account.id) for account in account_manager.iter_accounts(): if account == self.account: account.sip.register = False else: account.enabled = False self.output.put('Using account %s' % self.account.id) settings = SIPSimpleSettings() # start logging self.logger.start() # start the engine engine.start( auto_sound=False, events={'presence': [PIDFDocument.content_type]}, udp_port=settings.sip.udp_port if "udp" in settings.sip.transport_list else None, tcp_port=settings.sip.tcp_port if "tcp" in settings.sip.transport_list else None, tls_port=settings.sip.tls_port if "tls" in settings.sip.transport_list else None, tls_verify_server=self.account.tls.verify_server, tls_ca_file=os.path.expanduser(settings.tls.ca_list) if settings.tls.ca_list else None, tls_cert_file=os.path.expanduser(self.account.tls.certificate) if self.account.tls.certificate else None, tls_privkey_file=os.path.expanduser(self.account.tls.certificate) if self.account.tls.certificate else None, user_agent=settings.user_agent, sample_rate=settings.audio.sample_rate, rtp_port_range=(settings.rtp.port_range.start, settings.rtp.port_range.end), trace_sip=settings.logs.trace_sip or self.logger.sip_to_stdout, log_level=settings.logs.pjsip_level if (settings.logs.trace_pjsip or self.logger.pjsip_to_stdout) else 0 ) if self.target is None: self.target = ToHeader(SIPURI(user=self.account.id.username, host=self.account.id.domain)) else: if '@' not in self.target: self.target = '%s@%s' % (self.target, self.account.id.domain) if not self.target.startswith('sip:') and not self.target.startswith('sips:'): self.target = 'sip:' + self.target try: self.target = ToHeader(SIPURI.parse(self.target)) except SIPCoreError: self.output.put('Illegal SIP URI: %s' % self.target) engine.stop() return 1 self.output.put('Subscribing to %s for the presence event' % self.target.uri) # start the input thread self.input.start() reactor.callLater(0, self._subscribe) # start twisted try: reactor.run() finally: self.input.stop() # stop the output self.output.stop() self.output.join() self.logger.stop() return 0 if self.success else 1 def stop(self): self.stopping = True if self.subscription is not None and self.subscription.state.lower() in ('accepted', 'pending', 'active'): self.subscription.end(timeout=1) else: engine = Engine() engine.stop() def print_help(self): message = 'Available control keys:\n' message += ' t: toggle SIP trace on the console\n' message += ' j: toggle PJSIP trace on the console\n' message += ' n: toggle notifications trace on the console\n' message += ' Ctrl-d: quit the program\n' message += ' ?: display this help message\n' self.output.put('\n'+message) def handle_notification(self, notification): handler = getattr(self, '_NH_%s' % notification.name, None) if handler is not None: handler(notification) def _NH_SIPSubscriptionDidStart(self, notification): route = Route(notification.sender.route_header.uri.host, notification.sender.route_header.uri.port, notification.sender.route_header.uri.parameters.get('transport', 'udp')) self._subscription_routes = None self._subscription_wait = 0.5 self.output.put('Subscription succeeded at %s:%d;transport=%s' % (route.address, route.port, route.transport)) self.success = True def _NH_SIPSubscriptionChangedState(self, notification): route = Route(notification.sender.route_header.uri.host, notification.sender.route_header.uri.port, notification.sender.route_header.uri.parameters.get('transport', 'udp')) if notification.data.state.lower() == "pending": self.output.put('Subscription pending at %s:%d;transport=%s' % (route.address, route.port, route.transport)) elif notification.data.state.lower() == "active": self.output.put('Subscription active at %s:%d;transport=%s' % (route.address, route.port, route.transport)) def _NH_SIPSubscriptionDidEnd(self, notification): notification_center = NotificationCenter() notification_center.remove_observer(self, sender=notification.sender) self.subscription = None route = Route(notification.sender.route_header.uri.host, notification.sender.route_header.uri.port, notification.sender.route_header.uri.parameters.get('transport', 'udp')) self.output.put('Unsubscribed from %s:%d;transport=%s' % (route.address, route.port, route.transport)) self.stop() def _NH_SIPSubscriptionDidFail(self, notification): notification_center = NotificationCenter() notification_center.remove_observer(self, sender=notification.sender) self.subscription = None route = Route(notification.sender.route_header.uri.host, notification.sender.route_header.uri.port, notification.sender.route_header.uri.parameters.get('transport', 'udp')) if notification.data.code: status = ': %d %s' % (notification.data.code, notification.data.reason) else: status = ': %s' % notification.data.reason self.output.put('Subscription failed at %s:%d;transport=%s%s' % (route.address, route.port, route.transport, status)) if self.stopping or notification.data.code in (401, 403, 407) or self.success: self.success = False self.stop() else: if not self._subscription_routes or time() > self._subscription_timeout: self._subscription_wait = min(self._subscription_wait*2, 30) timeout = random.uniform(self._subscription_wait, 2*self._subscription_wait) reactor.callFromThread(reactor.callLater, timeout, self._subscribe) else: route = self._subscription_routes.popleft() route_header = RouteHeader(route.uri) self.subscription = Subscription(self.target.uri, FromHeader(self.account.uri, self.account.display_name), self.target, ContactHeader(self.account.contact[route]), "presence", route_header, credentials=self.account.credentials, refresh=self.account.sip.subscribe_interval) notification_center.add_observer(self, sender=self.subscription) self.subscription.subscribe(timeout=5) def _NH_SIPSubscriptionGotNotify(self, notification): if notification.data.content_type == PIDFDocument.content_type: self.output.put('Received NOTIFY:') try: pidf = PIDF.parse(notification.data.body) - except ParserError, e: + except ParserError as e: self.output.put('Got illegal PIDF document: %s\n%s' % (str(e), notification.data.body)) else: self._display_pidf(pidf) self.print_help() def _NH_DNSLookupDidSucceed(self, notification): # create subscription and register to get notifications from it self._subscription_routes = deque(notification.data.result) route = self._subscription_routes.popleft() route_header = RouteHeader(route.uri) self.subscription = Subscription(self.target.uri, FromHeader(self.account.uri, self.account.display_name), self.target, ContactHeader(self.account.contact[route]), "presence", route_header, credentials=self.account.credentials, refresh=self.account.sip.subscribe_interval) notification_center = NotificationCenter() notification_center.add_observer(self, sender=self.subscription) self.subscription.subscribe(timeout=5) def _NH_DNSLookupDidFail(self, notification): self.output.put('DNS lookup failed: %s' % notification.data.error) timeout = random.uniform(1.0, 2.0) reactor.callLater(timeout, self._subscribe) def _NH_SAInputWasReceived(self, notification): engine = Engine() settings = SIPSimpleSettings() key = notification.data.input if key == 't': self.logger.sip_to_stdout = not self.logger.sip_to_stdout engine.trace_sip = self.logger.sip_to_stdout or settings.logs.trace_sip self.output.put('SIP tracing to console is now %s.' % ('activated' if self.logger.sip_to_stdout else 'deactivated')) elif key == 'j': self.logger.pjsip_to_stdout = not self.logger.pjsip_to_stdout engine.log_level = settings.logs.pjsip_level if (self.logger.pjsip_to_stdout or settings.logs.trace_pjsip) else 0 self.output.put('PJSIP tracing to console is now %s.' % ('activated' if self.logger.pjsip_to_stdout else 'deactivated')) elif key == 'n': self.logger.notifications_to_stdout = not self.logger.notifications_to_stdout self.output.put('Notification tracing to console is now %s.' % ('activated' if self.logger.notifications_to_stdout else 'deactivated')) elif key == '?': self.print_help() @run_in_twisted_thread def _NH_SIPEngineDidEnd(self, notification): self._stop_reactor() @run_in_twisted_thread def _NH_SIPEngineDidFail(self, notification): self.output.put('Engine failed.') self._stop_reactor() def _NH_SIPEngineGotException(self, notification): self.output.put('An exception occured within the SIP core:\n'+notification.data.traceback) def _stop_reactor(self): try: reactor.stop() except ReactorNotRunning: pass def _subscribe(self): settings = SIPSimpleSettings() self._subscription_timeout = time()+30 lookup = DNSLookup() notification_center = NotificationCenter() notification_center.add_observer(self, sender=lookup) if self.account.sip.outbound_proxy is not None: uri = SIPURI(host=self.account.sip.outbound_proxy.host, port=self.account.sip.outbound_proxy.port, parameters={'transport': self.account.sip.outbound_proxy.transport}) elif self.account.sip.always_use_my_proxy: uri = SIPURI(host=self.account.id.domain) else: uri = self.target.uri lookup.lookup_sip_proxy(uri, settings.sip.transport_list) def _format_note(self, note): text = "Note" if note.lang is not None: text += "(%s)" % note.lang text += ": %s" % note return text def _format_person(self, person, pidf): buf = [] # display class if person.rpid_class is not None: buf.append(" Class: %s" % person.rpid_class) # display timestamp if person.timestamp is not None: buf.append(" Timestamp: %s" % person.timestamp) # display notes if person.notes: for note in person.notes: buf.append(" %s" % self._format_note(note)) elif pidf.notes: for note in pidf.notes: buf.append(" %s" % self._format_note(note)) # display activities if person.activities is not None: activities = list(person.activities) if len(activities) > 0: text = " Activities" if person.activities.since is not None or person.activities.until is not None: text += " valid" if person.activities.since is not None: text += " from %s" % person.activities.since if person.activities.until is not None: text += " until %s" % person.activities.until text += ": %s" % ', '.join(str(activity) for activity in activities) buf.append(text) if len(person.activities.notes) > 0: for note in person.activities.notes: buf.append(" %s" % self._format_note(note)) elif len(person.activities.notes) > 0: buf.append(" Activities") for note in person.activities.notes: buf.append(" %s" % self._format_note(note)) # display mood if person.mood is not None: moods = list(person.mood) if len(moods) > 0: text = " Mood" if person.mood.since is not None or person.mood.until is not None: text += " valid" if person.mood.since is not None: text += " from %s" % person.mood.since if person.mood.until is not None: text += " until %s" % person.mood.until text += ": %s" % ', '.join(str(mood) for mood in moods) buf.append(text) if len(person.mood.notes) > 0: for note in person.mood.notes: buf.append(" %s" % self._format_note(note)) # display place is if person.place_is is not None: place_info = ', '.join('%s %s' % (key.capitalize(), getattr(person.place_is, key).value) for key in ('audio', 'video', 'text') if getattr(person.place_is, key) and getattr(person.place_is, key).value) if place_info != '': buf.append(" Place information: " + place_info) # display privacy if person.privacy is not None: text = " Private conversation possible with: " private = [] if person.privacy.audio: private.append("Audio") if person.privacy.video: private.append("Video") if person.privacy.text: private.append("Text") if len(private) > 0: text += ", ".join(private) else: text += "None" buf.append(text) # display sphere if person.sphere is not None: timeinfo = [] if person.sphere.since is not None: timeinfo.append('from %s' % str(person.sphere.since)) if person.sphere.until is not None: timeinfo.append('until %s' % str(person.sphere.until)) if len(timeinfo) != 0: timeinfo = ' (' + ', '.join(timeinfo) + ')' else: timeinfo = '' buf.append(" Current sphere%s: %s" % (timeinfo, person.sphere.value)) # display status icon if person.status_icon is not None: buf.append(" Status icon: %s" % person.status_icon) # display time and time offset if person.time_offset is not None: ctime = datetime.datetime.utcnow() + datetime.timedelta(minutes=int(person.time_offset)) time_offset = int(person.time_offset)/60.0 if time_offset == int(time_offset): offset_info = '(UTC+%d%s)' % (time_offset, (person.time_offset.description is not None and (' (%s)' % person.time_offset.description) or '')) else: offset_info = '(UTC+%.1f%s)' % (time_offset, (person.time_offset.description is not None and (' (%s)' % person.time_offset.description) or '')) buf.append(" Current user time: %s %s" % (ctime.strftime("%H:%M"), offset_info)) # display user input if person.user_input is not None: buf.append(" User is %s" % person.user_input) if person.user_input.last_input: buf.append(" Last input at: %s" % person.user_input.last_input) if person.user_input.idle_threshold: buf.append(" Idle threshold: %s seconds" % person.user_input.idle_threshold) return buf def _format_service(self, service, pidf): buf = [] # display class if service.rpid_class is not None: buf.append(" Class: %s" % service.rpid_class) # display timestamp if service.timestamp is not None: buf.append(" Timestamp: %s" % service.timestamp) # display notes for note in service.notes: buf.append(" %s" % self._format_note(note)) # display status if service.status is not None: if service.status.basic is not None: buf.append(" Basic status: %s" % service.status.basic) if service.status.extended is not None: buf.append(" Extended status: %s" % service.status.extended) # display contact if service.contact is not None: - buf.append(" Contact%s: %s" % ((service.contact.priority is not None) and (' priority %s' % service.contact.priority) or '', urllib.unquote(service.contact.value))) + buf.append(" Contact%s: %s" % ((service.contact.priority is not None) and (' priority %s' % service.contact.priority) or '', urllib.parse.unquote(service.contact.value))) # display device ID if service.device_info is not None: - description = " (%s)" % urllib.unquote(service.device_info.description.value).decode('utf-8') if service.device_info.description else "" + description = " (%s)" % urllib.parse.unquote(service.device_info.description.value).decode('utf-8') if service.device_info.description else "" buf.append(" Service offered by device: %s%s" % (service.device_info.id, description)) # display relationship if service.relationship is not None: buf.append(" Relationship: %s" % service.relationship.value) # display service-class if service.service_class is not None: buf.append(" Service class: %s" % service.service_class.value) # display status icon if service.status_icon is not None: buf.append(" Status icon: %s" % service.status_icon) # display user input if service.user_input is not None: buf.append(" Service is %s" % service.user_input) if service.user_input.last_input: buf.append(" Last input at: %s" % service.user_input.last_input) if service.user_input.idle_threshold: buf.append(" Idle threshold: %s seconds" % service.user_input.idle_threshold) return buf def _format_device(self, device, pidf): buf = [] # display device ID if device.device_id is not None: buf.append(" Device id: %s" % device.device_id) # display class if device.rpid_class is not None: buf.append(" Class: %s" % device.rpid_class) # display timestamp if device.timestamp is not None: buf.append(" Timestamp: %s" % device.timestamp) # display notes for note in device.notes: buf.append(" %s" % self._format_note(note)) # display user input if device.user_input is not None: buf.append(" Device is %s" % device.user_input) if device.user_input.last_input: buf.append(" Last input at: %s" % device.user_input.last_input) if device.user_input.idle_threshold: buf.append(" Idle threshold: %s seconds" % device.user_input.idle_threshold) return buf def _display_pidf(self, pidf): buf = ["-"*16] - buf.append("Presence for %s:" % urllib.unquote(pidf.entity)) + buf.append("Presence for %s:" % urllib.parse.unquote(pidf.entity)) persons = {} devices = {} services = {} printed_sep = True for child in pidf: if isinstance(child, Person): persons[child.id] = child elif isinstance(child, Device): devices[child.id] = child elif isinstance(child, Service): services[child.id] = child # handle person information if len(persons) == 0: if list(pidf.notes): buf.append(" Person information:") for note in pidf.notes: buf.append(" %s" % self._format_note(note)) printed_sep = False else: - for person in persons.values(): + for person in list(persons.values()): buf.append(" Person: %s" % person.id) buf.extend(self._format_person(person, pidf)) printed_sep = False # handle services informaation if len(services) > 0: if not printed_sep: buf.append(" " + "-"*3) - for service in services.values(): + for service in list(services.values()): buf.append(" Service: %s" % service.id) buf.extend(self._format_service(service, pidf)) # handle devices informaation if len(devices) > 0: if not printed_sep: buf.append(" " + "-"*3) - for device in devices.values(): + for device in list(devices.values()): buf.append(" Device: %s" % device.id) buf.extend(self._format_device(device, pidf)) buf.append("-"*16) # push the data self.output.put('\n'.join(buf)) if __name__ == "__main__": description = "This script subscribes to the presence event package published by the specified SIP target. If a SIP target is not specified, it will subscribe to its own address. It will then interprete PIDF bodies contained in NOTIFYs and display their meaning. The program will un-SUBSCRIBE and quit when CTRL+D is pressed." usage = "%prog [options] [target-user@target-domain.com]" parser = OptionParser(usage=usage, description=description) parser.print_usage = parser.print_help parser.add_option("-a", "--account-name", type="string", dest="account_name", help="The name of the account to use.") parser.add_option("-s", "--trace-sip", action="store_true", dest="trace_sip", default=False, help="Dump the raw contents of incoming and outgoing SIP messages (disabled by default).") parser.add_option("-j", "--trace-pjsip", action="store_true", dest="trace_pjsip", default=False, help="Print PJSIP logging output (disabled by default).") parser.add_option("-n", "--trace-notifications", action="store_true", dest="trace_notifications", default=False, help="Print all notifications (disabled by default).") options, args = parser.parse_args() try: application = SubscriptionApplication(options.account_name, args[0] if args else None, options.trace_sip, options.trace_pjsip, options.trace_notifications) return_code = application.run() - except RuntimeError, e: - print "Error: %s" % str(e) + except RuntimeError as e: + print("Error: %s" % str(e)) sys.exit(1) - except SIPCoreError, e: - print "Error: %s" % str(e) + except SIPCoreError as e: + print("Error: %s" % str(e)) sys.exit(1) else: sys.exit(return_code) diff --git a/sip-subscribe-rls b/sip-subscribe-rls index bf9d2cd..18e2a8e 100755 --- a/sip-subscribe-rls +++ b/sip-subscribe-rls @@ -1,376 +1,376 @@ -#!/usr/bin/env python2 +#!/usr/bin/env python3 import os import random import select import sys import termios from collections import deque from optparse import OptionParser from threading import Thread from time import time from application import log from application.notification import IObserver, NotificationCenter, NotificationData from application.python.queue import EventQueue from eventlib.twistedutil import join_reactor from twisted.internet import reactor from twisted.internet.error import ReactorNotRunning from zope.interface import implements from sipsimple.account import Account, AccountManager, BonjourAccount from sipsimple.application import SIPApplication from sipsimple.configuration import ConfigurationError, ConfigurationManager from sipsimple.configuration.settings import SIPSimpleSettings from sipsimple.core import ContactHeader, Engine, FromHeader, Header, RouteHeader, SIPCoreError, SIPURI, Subscription, ToHeader, Route from sipsimple.lookup import DNSLookup from sipsimple.storage import FileStorage from sipsimple.threading import run_in_twisted_thread from sipclient.configuration import config_directory from sipclient.configuration.account import AccountExtension from sipclient.configuration.settings import SIPSimpleSettingsExtension from sipclient.log import Logger class InputThread(Thread): def __init__(self, application): Thread.__init__(self) self.application = application self.daemon = True self._old_terminal_settings = None def run(self): notification_center = NotificationCenter() while True: for char in self._getchars(): if char == "\x04": self.application.stop() return else: notification_center.post_notification('SAInputWasReceived', sender=self, data=NotificationData(input=char)) def stop(self): self._termios_restore() def _termios_restore(self): if self._old_terminal_settings is not None: termios.tcsetattr(sys.stdin.fileno(), termios.TCSADRAIN, self._old_terminal_settings) def _getchars(self): fd = sys.stdin.fileno() if os.isatty(fd): self._old_terminal_settings = termios.tcgetattr(fd) new = termios.tcgetattr(fd) new[3] = new[3] & ~termios.ICANON & ~termios.ECHO - new[6][termios.VMIN] = '\000' + new[6][termios.VMIN] = b'\000' try: termios.tcsetattr(fd, termios.TCSADRAIN, new) if select.select([fd], [], [], None)[0]: return sys.stdin.read(4192) finally: self._termios_restore() else: return os.read(fd, 4192) +@implementer(IObserver) class SubscriptionApplication(object): - implements(IObserver) def __init__(self, account_name, target, trace_sip, trace_pjsip, trace_notifications): self.account_name = account_name self.target = target self.input = InputThread(self) self.output = EventQueue(lambda event: sys.stdout.write(event+'\n')) self.logger = Logger(sip_to_stdout=trace_sip, pjsip_to_stdout=trace_pjsip, notifications_to_stdout=trace_notifications) self.success = False self.account = None self.subscription = None self.stopping = False self._subscription_routes = None self._subscription_timeout = 0.0 self._subscription_wait = 0.5 account_manager = AccountManager() engine = Engine() notification_center = NotificationCenter() notification_center.add_observer(self, sender=account_manager) notification_center.add_observer(self, sender=engine) notification_center.add_observer(self, sender=self.input) log.level.current = log.level.WARNING def run(self): account_manager = AccountManager() configuration = ConfigurationManager() engine = Engine() # start output thread self.output.start() # startup configuration Account.register_extension(AccountExtension) BonjourAccount.register_extension(AccountExtension) SIPSimpleSettings.register_extension(SIPSimpleSettingsExtension) SIPApplication.storage = FileStorage(config_directory) try: configuration.start() - except ConfigurationError, e: + except ConfigurationError as e: raise RuntimeError("Failed to load sipclient's configuration: %s\nIf an old configuration file is in place, delete it or move it and recreate the configuration using the sip_settings script." % str(e)) account_manager.load() if self.account_name is None: self.account = account_manager.default_account else: possible_accounts = [account for account in account_manager.iter_accounts() if self.account_name in account.id and account.enabled] if len(possible_accounts) > 1: raise RuntimeError("More than one account exists which matches %s: %s" % (self.account_name, ", ".join(sorted(account.id for account in possible_accounts)))) if len(possible_accounts) == 0: raise RuntimeError("No enabled account that matches %s was found. Available and enabled accounts: %s" % (self.account_name, ", ".join(sorted(account.id for account in account_manager.get_accounts() if account.enabled)))) self.account = possible_accounts[0] if self.account is None: raise RuntimeError("Unknown account %s. Available accounts: %s" % (self.account_name, ', '.join(account.id for account in account_manager.iter_accounts()))) elif self.account == BonjourAccount(): raise RuntimeError("Cannot use bonjour account for presence subscription") elif not self.account.presence.enabled: raise RuntimeError("Presence is not enabled for account %s" % self.account.id) for account in account_manager.iter_accounts(): if account == self.account: account.sip.register = False else: account.enabled = False self.output.put('Using account %s' % self.account.id) settings = SIPSimpleSettings() # start logging self.logger.start() # start the engine engine.start( auto_sound=False, events={'presence': ['multipart/related', 'application/rlmi+xml', 'application/pidf+xml']}, udp_port=settings.sip.udp_port if "udp" in settings.sip.transport_list else None, tcp_port=settings.sip.tcp_port if "tcp" in settings.sip.transport_list else None, tls_port=settings.sip.tls_port if "tls" in settings.sip.transport_list else None, tls_verify_server=self.account.tls.verify_server, tls_ca_file=os.path.expanduser(settings.tls.ca_list) if settings.tls.ca_list else None, tls_cert_file=os.path.expanduser(self.account.tls.certificate) if self.account.tls.certificate else None, tls_privkey_file=os.path.expanduser(self.account.tls.certificate) if self.account.tls.certificate else None, user_agent=settings.user_agent, sample_rate=settings.audio.sample_rate, rtp_port_range=(settings.rtp.port_range.start, settings.rtp.port_range.end), trace_sip=settings.logs.trace_sip or self.logger.sip_to_stdout, log_level=settings.logs.pjsip_level if (settings.logs.trace_pjsip or self.logger.pjsip_to_stdout) else 0 ) if self.target is None: self.target = ToHeader(SIPURI(user='%s+presence' % self.account.id.username, host=self.account.id.domain)) else: if '@' not in self.target: self.target = '%s@%s' % (self.target, self.account.id.domain) if not self.target.startswith('sip:') and not self.target.startswith('sips:'): self.target = 'sip:' + self.target try: self.target = ToHeader(SIPURI.parse(self.target)) except SIPCoreError: self.output.put('Illegal SIP URI: %s' % self.target) engine.stop() return 1 self.output.put('Subscribing to %s for the presence event' % self.target.uri) # start the input thread self.input.start() reactor.callLater(0, self._subscribe) # start twisted try: reactor.run() finally: self.input.stop() # stop the output self.output.stop() self.output.join() self.logger.stop() return 0 if self.success else 1 def stop(self): self.stopping = True if self.subscription is not None and self.subscription.state.lower() in ('accepted', 'pending', 'active'): self.subscription.end(timeout=1) else: engine = Engine() engine.stop() def print_help(self): message = 'Available control keys:\n' message += ' t: toggle SIP trace on the console\n' message += ' j: toggle PJSIP trace on the console\n' message += ' n: toggle notifications trace on the console\n' message += ' Ctrl-d: quit the program\n' message += ' ?: display this help message\n' self.output.put('\n'+message) def handle_notification(self, notification): handler = getattr(self, '_NH_%s' % notification.name, None) if handler is not None: handler(notification) def _NH_SIPSubscriptionDidStart(self, notification): route = Route(notification.sender.route_header.uri.host, notification.sender.route_header.uri.port, notification.sender.route_header.uri.parameters.get('transport', 'udp')) self._subscription_routes = None self._subscription_wait = 0.5 self.output.put('Subscription succeeded at %s:%d;transport=%s' % (route.address, route.port, route.transport)) self.success = True def _NH_SIPSubscriptionChangedState(self, notification): route = Route(notification.sender.route_header.uri.host, notification.sender.route_header.uri.port, notification.sender.route_header.uri.parameters.get('transport', 'udp')) if notification.data.state.lower() == "pending": self.output.put('Subscription pending at %s:%d;transport=%s' % (route.address, route.port, route.transport)) elif notification.data.state.lower() == "active": self.output.put('Subscription active at %s:%d;transport=%s' % (route.address, route.port, route.transport)) def _NH_SIPSubscriptionDidEnd(self, notification): notification_center = NotificationCenter() notification_center.remove_observer(self, sender=notification.sender) self.subscription = None route = Route(notification.sender.route_header.uri.host, notification.sender.route_header.uri.port, notification.sender.route_header.uri.parameters.get('transport', 'udp')) self.output.put('Unsubscribed from %s:%d;transport=%s' % (route.address, route.port, route.transport)) self.stop() def _NH_SIPSubscriptionDidFail(self, notification): notification_center = NotificationCenter() notification_center.remove_observer(self, sender=notification.sender) self.subscription = None route = Route(notification.sender.route_header.uri.host, notification.sender.route_header.uri.port, notification.sender.route_header.uri.parameters.get('transport', 'udp')) if notification.data.code: status = ': %d %s' % (notification.data.code, notification.data.reason) else: status = ': %s' % notification.data.reason self.output.put('Subscription failed at %s:%d;transport=%s%s' % (route.address, route.port, route.transport, status)) if self.stopping or notification.data.code in (401, 403, 407) or self.success: self.success = False self.stop() else: if not self._subscription_routes or time() > self._subscription_timeout: self._subscription_wait = min(self._subscription_wait*2, 30) timeout = random.uniform(self._subscription_wait, 2*self._subscription_wait) reactor.callFromThread(reactor.callLater, timeout, self._subscribe) else: route = self._subscription_routes.popleft() route_header = RouteHeader(route.uri) self.subscription = Subscription(self.target.uri, FromHeader(self.account.uri, self.account.display_name), self.target, ContactHeader(self.account.contact[route]), "presence", route_header, credentials=self.account.credentials, refresh=self.account.sip.subscribe_interval) notification_center.add_observer(self, sender=self.subscription) self.subscription.subscribe(extra_headers=[Header('Supported', 'eventlist')], timeout=5) def _NH_SIPSubscriptionGotNotify(self, notification): if notification.data.body: self.output.put('Received NOTIFY:\n' + notification.data.body) self.print_help() def _NH_DNSLookupDidSucceed(self, notification): # create subscription and register to get notifications from it self._subscription_routes = deque(notification.data.result) route = self._subscription_routes.popleft() route_header = RouteHeader(route.uri) self.subscription = Subscription(self.target.uri, FromHeader(self.account.uri, self.account.display_name), self.target, ContactHeader(self.account.contact[route]), "presence", route_header, credentials=self.account.credentials, refresh=self.account.sip.subscribe_interval) notification_center = NotificationCenter() notification_center.add_observer(self, sender=self.subscription) self.subscription.subscribe(extra_headers=[Header('Supported', 'eventlist')], timeout=5) def _NH_DNSLookupDidFail(self, notification): self.output.put('DNS lookup failed: %s' % notification.data.error) timeout = random.uniform(1.0, 2.0) reactor.callLater(timeout, self._subscribe) def _NH_SAInputWasReceived(self, notification): engine = Engine() settings = SIPSimpleSettings() key = notification.data.input if key == 't': self.logger.sip_to_stdout = not self.logger.sip_to_stdout engine.trace_sip = self.logger.sip_to_stdout or settings.logs.trace_sip self.output.put('SIP tracing to console is now %s.' % ('activated' if self.logger.sip_to_stdout else 'deactivated')) elif key == 'j': self.logger.pjsip_to_stdout = not self.logger.pjsip_to_stdout engine.log_level = settings.logs.pjsip_level if (self.logger.pjsip_to_stdout or settings.logs.trace_pjsip) else 0 self.output.put('PJSIP tracing to console is now %s.' % ('activated' if self.logger.pjsip_to_stdout else 'deactivated')) elif key == 'n': self.logger.notifications_to_stdout = not self.logger.notifications_to_stdout self.output.put('Notification tracing to console is now %s.' % ('activated' if self.logger.notifications_to_stdout else 'deactivated')) elif key == '?': self.print_help() @run_in_twisted_thread def _NH_SIPEngineDidEnd(self, notification): self._stop_reactor() @run_in_twisted_thread def _NH_SIPEngineDidFail(self, notification): self.output.put('Engine failed.') self._stop_reactor() def _NH_SIPEngineGotException(self, notification): self.output.put('An exception occured within the SIP core:\n'+notification.data.traceback) def _stop_reactor(self): try: reactor.stop() except ReactorNotRunning: pass def _subscribe(self): settings = SIPSimpleSettings() self._subscription_timeout = time()+30 lookup = DNSLookup() notification_center = NotificationCenter() notification_center.add_observer(self, sender=lookup) if self.account.sip.outbound_proxy is not None: uri = SIPURI(host=self.account.sip.outbound_proxy.host, port=self.account.sip.outbound_proxy.port, parameters={'transport': self.account.sip.outbound_proxy.transport}) else: uri = self.target.uri lookup.lookup_sip_proxy(uri, settings.sip.transport_list) if __name__ == "__main__": description = "This script subscribes to the presence event package published by the specified SIP target assuming it is a resource list handled by a RLS server. The RLS server will then SUBSCRIBE in behalf of the account, collect NOTIFYs with the presence information of the recipients and provide periodically aggregated NOTIFYs back to the subscriber. If a target address is not specified, it will subscribe to the address 'username-buddies@domain.com', where username and domain are taken from the account's SIP address. It will then interprete PIDF bodies contained in NOTIFYs and display their meaning. The program will un-SUBSCRIBE and quit when CTRL+D is pressed." usage = "%prog [options] [target-user@target-domain.com]" parser = OptionParser(usage=usage, description=description) parser.print_usage = parser.print_help parser.add_option("-a", "--account-name", type="string", dest="account_name", help="The name of the account to use.") parser.add_option("-s", "--trace-sip", action="store_true", dest="trace_sip", default=False, help="Dump the raw contents of incoming and outgoing SIP messages (disabled by default).") parser.add_option("-j", "--trace-pjsip", action="store_true", dest="trace_pjsip", default=False, help="Print PJSIP logging output (disabled by default).") parser.add_option("-n", "--trace-notifications", action="store_true", dest="trace_notifications", default=False, help="Print all notifications (disabled by default).") options, args = parser.parse_args() try: application = SubscriptionApplication(options.account_name, args[0] if args else None, options.trace_sip, options.trace_pjsip, options.trace_notifications) return_code = application.run() - except RuntimeError, e: - print "Error: %s" % str(e) + except RuntimeError as e: + print("Error: %s" % str(e)) sys.exit(1) - except SIPCoreError, e: - print "Error: %s" % str(e) + except SIPCoreError as e: + print("Error: %s" % str(e)) sys.exit(1) else: sys.exit(return_code) diff --git a/sip-subscribe-winfo b/sip-subscribe-winfo index da3e27f..7ef9557 100755 --- a/sip-subscribe-winfo +++ b/sip-subscribe-winfo @@ -1,393 +1,393 @@ -#!/usr/bin/env python2 +#!/usr/bin/env python3 import os import random import select import sys import termios from collections import deque from optparse import OptionParser from threading import Thread from time import time from application import log from application.notification import IObserver, NotificationCenter, NotificationData from application.python.queue import EventQueue from eventlib.twistedutil import join_reactor from twisted.internet import reactor from twisted.internet.error import ReactorNotRunning -from zope.interface import implements +from zope.interface import implementer from sipsimple.account import Account, AccountManager, BonjourAccount from sipsimple.application import SIPApplication from sipsimple.configuration import ConfigurationError, ConfigurationManager from sipsimple.configuration.settings import SIPSimpleSettings from sipsimple.core import ContactHeader, Engine, FromHeader, Route, RouteHeader, SIPCoreError, SIPURI, Subscription, ToHeader from sipsimple.lookup import DNSLookup from sipsimple.payloads import ParserError from sipsimple.payloads.watcherinfo import WatcherInfoDocument from sipsimple.storage import FileStorage from sipsimple.threading import run_in_twisted_thread from sipclient.configuration import config_directory from sipclient.configuration.account import AccountExtension from sipclient.configuration.settings import SIPSimpleSettingsExtension from sipclient.log import Logger class InputThread(Thread): def __init__(self, application): Thread.__init__(self) self.application = application self.daemon = True self._old_terminal_settings = None def run(self): notification_center = NotificationCenter() while True: for char in self._getchars(): if char == "\x04": self.application.stop() return else: notification_center.post_notification('SAInputWasReceived', sender=self, data=NotificationData(input=char)) def stop(self): self._termios_restore() def _termios_restore(self): if self._old_terminal_settings is not None: termios.tcsetattr(sys.stdin.fileno(), termios.TCSADRAIN, self._old_terminal_settings) def _getchars(self): fd = sys.stdin.fileno() if os.isatty(fd): self._old_terminal_settings = termios.tcgetattr(fd) new = termios.tcgetattr(fd) new[3] = new[3] & ~termios.ICANON & ~termios.ECHO - new[6][termios.VMIN] = '\000' + new[6][termios.VMIN] = b'\000' try: termios.tcsetattr(fd, termios.TCSADRAIN, new) if select.select([fd], [], [], None)[0]: return sys.stdin.read(4192) finally: self._termios_restore() else: return os.read(fd, 4192) +@implementer(IObserver) class WinfoApplication(object): - implements(IObserver) def __init__(self, account_name, trace_sip, trace_pjsip, trace_notifications): self.account_name = account_name self.input = InputThread(self) self.output = EventQueue(lambda event: sys.stdout.write(event+'\n')) self.logger = Logger(sip_to_stdout=trace_sip, pjsip_to_stdout=trace_pjsip, notifications_to_stdout=trace_notifications) self.success = False self.account = None self.subscription = None self.stopping = False self._subscription_routes = None self._subscription_timeout = 0.0 self._subscription_wait = 0.5 account_manager = AccountManager() engine = Engine() notification_center = NotificationCenter() notification_center.add_observer(self, sender=account_manager) notification_center.add_observer(self, sender=engine) notification_center.add_observer(self, sender=self.input) log.level.current = log.level.WARNING def run(self): account_manager = AccountManager() configuration = ConfigurationManager() engine = Engine() # start output thread self.output.start() # startup configuration Account.register_extension(AccountExtension) BonjourAccount.register_extension(AccountExtension) SIPSimpleSettings.register_extension(SIPSimpleSettingsExtension) SIPApplication.storage = FileStorage(config_directory) try: configuration.start() - except ConfigurationError, e: + except ConfigurationError as e: raise RuntimeError("Failed to load sipclient's configuration: %s\nIf an old configuration file is in place, delete it or move it and recreate the configuration using the sip_settings script." % str(e)) account_manager.load() if self.account_name is None: self.account = account_manager.default_account else: possible_accounts = [account for account in account_manager.iter_accounts() if self.account_name in account.id and account.enabled] if len(possible_accounts) > 1: raise RuntimeError("More than one account exists which matches %s: %s" % (self.account_name, ", ".join(sorted(account.id for account in possible_accounts)))) if len(possible_accounts) == 0: raise RuntimeError("No enabled account that matches %s was found. Available and enabled accounts: %s" % (self.account_name, ", ".join(sorted(account.id for account in account_manager.get_accounts() if account.enabled)))) self.account = possible_accounts[0] if self.account is None: raise RuntimeError("Unknown account %s. Available accounts: %s" % (self.account_name, ', '.join(account.id for account in account_manager.iter_accounts()))) elif self.account == BonjourAccount(): raise RuntimeError("Cannot use bonjour account for watcherinfo subscription") elif not self.account.presence.enabled: raise RuntimeError("Presence is not enabled for account %s" % self.account.id) elif not self.account.xcap.enabled: raise RuntimeError("XCAP is not enabled for account %s" % self.account.id) elif self.account.xcap.xcap_root is None: raise RuntimeError("XCAP root is not defined for account %s" % self.account.id) for account in account_manager.iter_accounts(): if account == self.account: account.sip.register = False else: account.enabled = False self.output.put('Using account %s' % self.account.id) settings = SIPSimpleSettings() # start logging self.logger.start() # start the engine engine.start( auto_sound=False, events={'presence.winfo': [WatcherInfoDocument.content_type]}, udp_port=settings.sip.udp_port if "udp" in settings.sip.transport_list else None, tcp_port=settings.sip.tcp_port if "tcp" in settings.sip.transport_list else None, tls_port=settings.sip.tls_port if "tls" in settings.sip.transport_list else None, tls_verify_server=self.account.tls.verify_server, tls_ca_file=os.path.expanduser(settings.tls.ca_list) if settings.tls.ca_list else None, tls_cert_file=os.path.expanduser(self.account.tls.certificate) if self.account.tls.certificate else None, tls_privkey_file=os.path.expanduser(self.account.tls.certificate) if self.account.tls.certificate else None, user_agent=settings.user_agent, sample_rate=settings.audio.sample_rate, rtp_port_range=(settings.rtp.port_range.start, settings.rtp.port_range.end), trace_sip=settings.logs.trace_sip or self.logger.sip_to_stdout, log_level=settings.logs.pjsip_level if (settings.logs.trace_pjsip or self.logger.pjsip_to_stdout) else 0 ) self.output.put('Subscribing to the presence.winfo event') # start the input thread self.input.start() reactor.callLater(0, self._subscribe) # start twisted try: reactor.run() finally: self.input.stop() # stop the output self.output.stop() self.output.join() self.logger.stop() return 0 if self.success else 1 def stop(self): self.stopping = True if self.subscription is not None and self.subscription.state.lower() in ('accepted', 'pending', 'active'): self.subscription.end(timeout=1) else: engine = Engine() engine.stop() def print_help(self): message = 'Available control keys:\n' message += ' t: toggle SIP trace on the console\n' message += ' j: toggle PJSIP trace on the console\n' message += ' Ctrl-d: quit the program\n' message += ' ?: display this help message\n' self.output.put('\n'+message) def handle_notification(self, notification): handler = getattr(self, '_NH_%s' % notification.name, None) if handler is not None: handler(notification) def _NH_SIPSubscriptionDidStart(self, notification): route = Route(notification.sender.route_header.uri.host, notification.sender.route_header.uri.port, notification.sender.route_header.uri.parameters.get('transport', 'udp')) self._subscription_routes = None self._subscription_wait = 0.5 self.output.put('Subscription succeeded at %s:%d;transport=%s' % (route.address, route.port, route.transport)) self.success = True def _NH_SIPSubscriptionChangedState(self, notification): route = Route(notification.sender.route_header.uri.host, notification.sender.route_header.uri.port, notification.sender.route_header.uri.parameters.get('transport', 'udp')) if notification.data.state.lower() == "pending": self.output.put('Subscription pending at %s:%d;transport=%s' % (route.address, route.port, route.transport)) elif notification.data.state.lower() == "active": self.output.put('Subscription active at %s:%d;transport=%s' % (route.address, route.port, route.transport)) def _NH_SIPSubscriptionDidEnd(self, notification): notification_center = NotificationCenter() notification_center.remove_observer(self, sender=notification.sender) self.subscription = None route = Route(notification.sender.route_header.uri.host, notification.sender.route_header.uri.port, notification.sender.route_header.uri.parameters.get('transport', 'udp')) self.output.put('Unsubscribed from %s:%d;transport=%s' % (route.address, route.port, route.transport)) self.stop() def _NH_SIPSubscriptionDidFail(self, notification): notification_center = NotificationCenter() notification_center.remove_observer(self, sender=notification.sender) self.subscription = None route = Route(notification.sender.route_header.uri.host, notification.sender.route_header.uri.port, notification.sender.route_header.uri.parameters.get('transport', 'udp')) if notification.data.code: status = ': %d %s' % (notification.data.code, notification.data.reason) else: status = ': %s' % notification.data.reason self.output.put('Subscription failed at %s:%d;transport=%s%s' % (route.address, route.port, route.transport, status)) if self.stopping or notification.data.code in (401, 403, 407) or self.success: self.success = False self.stop() else: if not self._subscription_routes or time() > self._subscription_timeout: self._subscription_wait = min(self._subscription_wait*2, 30) timeout = random.uniform(self._subscription_wait, 2*self._subscription_wait) reactor.callFromThread(reactor.callLater, timeout, self._subscribe) else: route = self._subscription_routes.popleft() route_header = RouteHeader(route.uri) self.subscription = Subscription(self.account.uri, FromHeader(self.account.uri, self.account.display_name), ToHeader(self.account.uri, self.account.display_name), ContactHeader(self.account.contact[route]), "presence.winfo", route_header, credentials=self.account.credentials, refresh=self.account.sip.subscribe_interval) notification_center.add_observer(self, sender=self.subscription) self.subscription.subscribe(timeout=5) def _NH_SIPSubscriptionGotNotify(self, notification): if notification.data.content_type == WatcherInfoDocument.content_type: self._handle_winfo(notification.data.body) def _NH_DNSLookupDidSucceed(self, notification): # create subscription and register to get notifications from it self._subscription_routes = deque(notification.data.result) route = self._subscription_routes.popleft() route_header = RouteHeader(route.uri) self.subscription = Subscription(self.account.uri, FromHeader(self.account.uri, self.account.display_name), ToHeader(self.account.uri, self.account.display_name), ContactHeader(self.account.contact[route]), "presence.winfo", route_header, credentials=self.account.credentials, refresh=self.account.sip.subscribe_interval) notification_center = NotificationCenter() notification_center.add_observer(self, sender=self.subscription) self.subscription.subscribe(timeout=5) def _NH_DNSLookupDidFail(self, notification): self.output.put('DNS lookup failed: %s' % notification.data.error) timeout = random.uniform(1.0, 2.0) reactor.callLater(timeout, self._subscribe) def _NH_SAInputWasReceived(self, notification): engine = Engine() settings = SIPSimpleSettings() key = notification.data.input if key == 't': self.logger.sip_to_stdout = not self.logger.sip_to_stdout engine.trace_sip = self.logger.sip_to_stdout or settings.logs.trace_sip self.output.put('SIP tracing to console is now %s.' % ('activated' if self.logger.sip_to_stdout else 'deactivated')) elif key == 'j': self.logger.pjsip_to_stdout = not self.logger.pjsip_to_stdout engine.log_level = settings.logs.pjsip_level if (self.logger.pjsip_to_stdout or settings.logs.trace_pjsip) else 0 self.output.put('PJSIP tracing to console is now %s.' % ('activated' if self.logger.pjsip_to_stdout else 'deactivated')) elif key == 'n': self.logger.notifications_to_stdout = not self.logger.notifications_to_stdout self.output.put('Notification tracing to console is now %s.' % ('activated' if self.logger.notifications_to_stdout else 'deactivated')) elif key == '?': self.print_help() @run_in_twisted_thread def _NH_SIPEngineDidEnd(self, notification): self._stop_reactor() @run_in_twisted_thread def _NH_SIPEngineDidFail(self, notification): self.output.put('Engine failed.') self._stop_reactor() def _NH_SIPEngineGotException(self, notification): self.output.put('An exception occured within the SIP core:\n'+notification.data.traceback) def _stop_reactor(self): try: reactor.stop() except ReactorNotRunning: pass def _subscribe(self): settings = SIPSimpleSettings() self._subscription_timeout = time()+30 lookup = DNSLookup() notification_center = NotificationCenter() notification_center.add_observer(self, sender=lookup) if self.account.sip.outbound_proxy is not None: uri = SIPURI(host=self.account.sip.outbound_proxy.host, port=self.account.sip.outbound_proxy.port, parameters={'transport': self.account.sip.outbound_proxy.transport}) else: uri = SIPURI(host=self.account.id.domain) lookup.lookup_sip_proxy(uri, settings.sip.transport_list) def _handle_winfo(self, body): try: watcher_info = WatcherInfoDocument.parse(body) - except ParserError, e: + except ParserError as e: self.output.put("Got illegal winfo document: %s\n%s" % (str(e), body)) else: try: wlist = watcher_info['sip:' + self.account.id] except KeyError: self.output.put("Expected an entry for account %s in the winfo document" % self.account.id) else: buf = ["Received NOTIFY:", "----"] buf.append("Active watchers:") for watcher in wlist.active: buf.append(" %s" % watcher) buf.append("Terminated watchers:") for watcher in wlist.terminated: buf.append(" %s" % watcher) buf.append("Pending watchers:") for watcher in wlist.pending: buf.append(" %s" % watcher) buf.append("Waiting watchers:") for watcher in wlist.waiting: buf.append(" %s" % watcher) buf.append("----") self.output.put('\n'.join(buf)) if __name__ == "__main__": description = "This script subscribes to the presence.winfo event package and shows the received watcher info document's content. The program will un-SUBSCRIBE and quit when CTRL+D is pressed." usage = "%prog [options] [target-user@target-domain.com]" parser = OptionParser(usage=usage, description=description) parser.print_usage = parser.print_help parser.add_option("-a", "--account-name", type="string", dest="account_name", help="The name of the account to use.") parser.add_option("-s", "--trace-sip", action="store_true", dest="trace_sip", default=False, help="Dump the raw contents of incoming and outgoing SIP messages (disabled by default).") parser.add_option("-j", "--trace-pjsip", action="store_true", dest="trace_pjsip", default=False, help="Print PJSIP logging output (disabled by default).") parser.add_option("-n", "--trace-notifications", action="store_true", dest="trace_notifications", default=False, help="Print all notifications (disabled by default).") options, args = parser.parse_args() try: application = WinfoApplication(options.account_name, options.trace_sip, options.trace_pjsip, options.trace_notifications) return_code = application.run() - except RuntimeError, e: - print "Error: %s" % str(e) + except RuntimeError as e: + print("Error: %s" % str(e)) sys.exit(1) - except SIPCoreError, e: - print "Error: %s" % str(e) + except SIPCoreError as e: + print("Error: %s" % str(e)) sys.exit(1) else: sys.exit(return_code) diff --git a/sip-subscribe-xcap-diff b/sip-subscribe-xcap-diff index e88f07f..0cbab29 100755 --- a/sip-subscribe-xcap-diff +++ b/sip-subscribe-xcap-diff @@ -1,411 +1,411 @@ -#!/usr/bin/env python2 +#!/usr/bin/env python3 import os import random import select import sys import termios from collections import deque from optparse import OptionParser from threading import Thread from time import time from application import log from application.notification import IObserver, NotificationCenter, NotificationData from application.python.queue import EventQueue from eventlib.twistedutil import join_reactor from twisted.internet import reactor from twisted.internet.error import ReactorNotRunning -from zope.interface import implements +from zope.interface import implementer from sipsimple.account import Account, AccountManager, BonjourAccount from sipsimple.application import SIPApplication from sipsimple.configuration import ConfigurationError, ConfigurationManager from sipsimple.configuration.settings import SIPSimpleSettings from sipsimple.core import ContactHeader, Engine, FromHeader, Route, RouteHeader, SIPCoreError, SIPURI, Subscription, ToHeader from sipsimple.lookup import DNSLookup from sipsimple.payloads import ParserError from sipsimple.payloads.xcapdiff import XCAPDiffDocument, Document, Element, Attribute from sipsimple.payloads.resourcelists import ResourceListsDocument, ResourceLists, List, Entry from sipsimple.storage import FileStorage from sipsimple.threading import run_in_twisted_thread from sipclient.configuration import config_directory from sipclient.configuration.account import AccountExtension from sipclient.configuration.settings import SIPSimpleSettingsExtension from sipclient.log import Logger class InputThread(Thread): def __init__(self, application): Thread.__init__(self) self.application = application self.daemon = True self._old_terminal_settings = None def run(self): notification_center = NotificationCenter() while True: for char in self._getchars(): if char == "\x04": self.application.stop() return else: notification_center.post_notification('SAInputWasReceived', sender=self, data=NotificationData(input=char)) def stop(self): self._termios_restore() def _termios_restore(self): if self._old_terminal_settings is not None: termios.tcsetattr(sys.stdin.fileno(), termios.TCSADRAIN, self._old_terminal_settings) def _getchars(self): fd = sys.stdin.fileno() if os.isatty(fd): self._old_terminal_settings = termios.tcgetattr(fd) new = termios.tcgetattr(fd) new[3] = new[3] & ~termios.ICANON & ~termios.ECHO - new[6][termios.VMIN] = '\000' + new[6][termios.VMIN] = b'\000' try: termios.tcsetattr(fd, termios.TCSADRAIN, new) if select.select([fd], [], [], None)[0]: return sys.stdin.read(4192) finally: self._termios_restore() else: return os.read(fd, 4192) +@implementer(IObserver) class SubscriptionApplication(object): - implements(IObserver) def __init__(self, account_name, trace_sip, trace_pjsip, trace_notifications): self.account_name = account_name self.target = None self.input = InputThread(self) self.output = EventQueue(lambda event: sys.stdout.write(event+'\n')) self.logger = Logger(sip_to_stdout=trace_sip, pjsip_to_stdout=trace_pjsip, notifications_to_stdout=trace_notifications) self.success = False self.account = None self.subscription = None self.stopping = False self.body = None self.content_type = None self._subscription_routes = None self._subscription_timeout = 0.0 self._subscription_wait = 0.5 account_manager = AccountManager() engine = Engine() notification_center = NotificationCenter() notification_center.add_observer(self, sender=account_manager) notification_center.add_observer(self, sender=engine) notification_center.add_observer(self, sender=self.input) log.level.current = log.level.WARNING def run(self): account_manager = AccountManager() configuration = ConfigurationManager() engine = Engine() # start output thread self.output.start() # startup configuration Account.register_extension(AccountExtension) BonjourAccount.register_extension(AccountExtension) SIPSimpleSettings.register_extension(SIPSimpleSettingsExtension) SIPApplication.storage = FileStorage(config_directory) try: configuration.start() - except ConfigurationError, e: + except ConfigurationError as e: raise RuntimeError("Failed to load sipclient's configuration: %s\nIf an old configuration file is in place, delete it or move it and recreate the configuration using the sip_settings script." % str(e)) account_manager.load() if self.account_name is None: self.account = account_manager.default_account else: possible_accounts = [account for account in account_manager.iter_accounts() if self.account_name in account.id and account.enabled] if len(possible_accounts) > 1: raise RuntimeError("More than one account exists which matches %s: %s" % (self.account_name, ", ".join(sorted(account.id for account in possible_accounts)))) if len(possible_accounts) == 0: raise RuntimeError("No enabled account that matches %s was found. Available and enabled accounts: %s" % (self.account_name, ", ".join(sorted(account.id for account in account_manager.get_accounts() if account.enabled)))) self.account = possible_accounts[0] if self.account is None: raise RuntimeError("Unknown account %s. Available accounts: %s" % (self.account_name, ', '.join(account.id for account in account_manager.iter_accounts()))) elif not self.account.enabled: raise RuntimeError("Account %s is not enabled" % self.account.id) elif self.account == BonjourAccount(): raise RuntimeError("Cannot use bonjour account for presence subscription") elif not self.account.xcap.enabled: raise RuntimeError("XCAP is not enabled for account %s" % self.account.id) elif self.account.xcap.xcap_root is None: raise RuntimeError("XCAP root is not defined for account %s" % self.account.id) for account in account_manager.iter_accounts(): if account == self.account: account.sip.register = False else: account.enabled = False self.output.put('Using account %s' % self.account.id) settings = SIPSimpleSettings() # generate the body list = List() resource_lists = ResourceLists([list]) list.add(Entry('resource-lists/users/sip:%s/index' % self.account.id)) list.add(Entry('rls-services/users/sip:%s/index' % self.account.id)) list.add(Entry('pres-rules/users/sip:%s/index' % self.account.id)) self.body = resource_lists.toxml(pretty_print=True) self.content_type = ResourceListsDocument.content_type # start logging self.logger.start() # start the engine engine.start( auto_sound=False, events={'xcap-diff': ['application/xcap-diff+xml']}, udp_port=settings.sip.udp_port if "udp" in settings.sip.transport_list else None, tcp_port=settings.sip.tcp_port if "tcp" in settings.sip.transport_list else None, tls_port=settings.sip.tls_port if "tls" in settings.sip.transport_list else None, tls_verify_server=self.account.tls.verify_server, tls_ca_file=os.path.expanduser(settings.tls.ca_list) if settings.tls.ca_list else None, tls_cert_file=os.path.expanduser(self.account.tls.certificate) if self.account.tls.certificate else None, tls_privkey_file=os.path.expanduser(self.account.tls.certificate) if self.account.tls.certificate else None, user_agent=settings.user_agent, sample_rate=settings.audio.sample_rate, rtp_port_range=(settings.rtp.port_range.start, settings.rtp.port_range.end), trace_sip=settings.logs.trace_sip or self.logger.sip_to_stdout, log_level=settings.logs.pjsip_level if (settings.logs.trace_pjsip or self.logger.pjsip_to_stdout) else 0 ) self.target = ToHeader(SIPURI(user=self.account.id.username, host=self.account.id.domain)) self.output.put('Subscribing to %s for the xcap-diff event' % self.target.uri) # start the input thread self.input.start() reactor.callLater(0, self._subscribe) # start twisted try: reactor.run() finally: self.input.stop() # stop the output self.output.stop() self.output.join() self.logger.stop() return 0 if self.success else 1 def stop(self): self.stopping = True if self.subscription is not None and self.subscription.state.lower() in ('accepted', 'pending', 'active'): self.subscription.end(timeout=1) else: engine = Engine() engine.stop() def print_help(self): message = 'Available control keys:\n' message += ' t: toggle SIP trace on the console\n' message += ' j: toggle PJSIP trace on the console\n' message += ' n: toggle notifications trace on the console\n' message += ' Ctrl-d: quit the program\n' message += ' ?: display this help message\n' self.output.put('\n'+message) def handle_notification(self, notification): handler = getattr(self, '_NH_%s' % notification.name, None) if handler is not None: handler(notification) def _NH_SIPSubscriptionDidStart(self, notification): route = Route(notification.sender.route_header.uri.host, notification.sender.route_header.uri.port, notification.sender.route_header.uri.parameters.get('transport', 'udp')) self._subscription_routes = None self._subscription_wait = 0.5 self.output.put('Subscription succeeded at %s:%d;transport=%s' % (route.address, route.port, route.transport)) self.success = True def _NH_SIPSubscriptionChangedState(self, notification): route = Route(notification.sender.route_header.uri.host, notification.sender.route_header.uri.port, notification.sender.route_header.uri.parameters.get('transport', 'udp')) if notification.data.state.lower() == "pending": self.output.put('Subscription pending at %s:%d;transport=%s' % (route.address, route.port, route.transport)) elif notification.data.state.lower() == "active": self.output.put('Subscription active at %s:%d;transport=%s' % (route.address, route.port, route.transport)) def _NH_SIPSubscriptionDidEnd(self, notification): notification_center = NotificationCenter() notification_center.remove_observer(self, sender=notification.sender) self.subscription = None route = Route(notification.sender.route_header.uri.host, notification.sender.route_header.uri.port, notification.sender.route_header.uri.parameters.get('transport', 'udp')) self.output.put('Unsubscribed from %s:%d;transport=%s' % (route.address, route.port, route.transport)) self.stop() def _NH_SIPSubscriptionDidFail(self, notification): notification_center = NotificationCenter() notification_center.remove_observer(self, sender=notification.sender) self.subscription = None route = Route(notification.sender.route_header.uri.host, notification.sender.route_header.uri.port, notification.sender.route_header.uri.parameters.get('transport', 'udp')) if notification.data.code: status = ': %d %s' % (notification.data.code, notification.data.reason) else: status = ': %s' % notification.data.reason self.output.put('Subscription failed at %s:%d;transport=%s%s' % (route.address, route.port, route.transport, status)) if self.stopping or notification.data.code in (401, 403, 407, 489) or self.success: self.success = False self.stop() else: if not self._subscription_routes or time() > self._subscription_timeout: self._subscription_wait = min(self._subscription_wait*2, 30) timeout = random.uniform(self._subscription_wait, 2*self._subscription_wait) reactor.callFromThread(reactor.callLater, timeout, self._subscribe) else: route = self._subscription_routes.popleft() route_header = RouteHeader(route.uri) self.subscription = Subscription(self.target.uri, FromHeader(self.account.uri, self.account.display_name), self.target, ContactHeader(self.account.contact[route]), "xcap-diff", route_header, credentials=self.account.credentials, refresh=self.account.sip.subscribe_interval) notification_center.add_observer(self, sender=self.subscription) self.subscription.subscribe(body=self.body, content_type=self.content_type, timeout=5) def _NH_SIPSubscriptionGotNotify(self, notification): if notification.data.content_type == XCAPDiffDocument.content_type: try: xcap_diff = XCAPDiffDocument.parse(notification.data.body) - except ParserError, e: + except ParserError as e: self.output.put("xcap-diff document is invalid: %s" % str(e)) else: self._display_xcapdiff(xcap_diff) self.print_help() def _NH_DNSLookupDidSucceed(self, notification): # create subscription and register to get notifications from it self._subscription_routes = deque(notification.data.result) route = self._subscription_routes.popleft() route_header = RouteHeader(route.uri) self.subscription = Subscription(self.target.uri, FromHeader(self.account.uri, self.account.display_name), self.target, ContactHeader(self.account.contact[route]), "xcap-diff", route_header, credentials=self.account.credentials, refresh=self.account.sip.subscribe_interval) notification_center = NotificationCenter() notification_center.add_observer(self, sender=self.subscription) self.subscription.subscribe(body=self.body, content_type=self.content_type, timeout=5) def _NH_DNSLookupDidFail(self, notification): self.output.put('DNS lookup failed: %s' % notification.data.error) timeout = random.uniform(1.0, 2.0) reactor.callLater(timeout, self._subscribe) def _NH_SAInputWasReceived(self, notification): engine = Engine() settings = SIPSimpleSettings() key = notification.data.input if key == 't': self.logger.sip_to_stdout = not self.logger.sip_to_stdout engine.trace_sip = self.logger.sip_to_stdout or settings.logs.trace_sip self.output.put('SIP tracing to console is now %s.' % ('activated' if self.logger.sip_to_stdout else 'deactivated')) elif key == 'j': self.logger.pjsip_to_stdout = not self.logger.pjsip_to_stdout engine.log_level = settings.logs.pjsip_level if (self.logger.pjsip_to_stdout or settings.logs.trace_pjsip) else 0 self.output.put('PJSIP tracing to console is now %s.' % ('activated' if self.logger.pjsip_to_stdout else 'deactivated')) elif key == 'n': self.logger.notifications_to_stdout = not self.logger.notifications_to_stdout self.output.put('Notification tracing to console is now %s.' % ('activated' if self.logger.notifications_to_stdout else 'deactivated')) elif key == '?': self.print_help() @run_in_twisted_thread def _NH_SIPEngineDidEnd(self, notification): self._stop_reactor() @run_in_twisted_thread def _NH_SIPEngineDidFail(self, notification): self.output.put('Engine failed.') self._stop_reactor() def _NH_SIPEngineGotException(self, notification): self.output.put('An exception occured within the SIP core:\n'+notification.data.traceback) def _stop_reactor(self): try: reactor.stop() except ReactorNotRunning: pass def _subscribe(self): settings = SIPSimpleSettings() self._subscription_timeout = time()+30 lookup = DNSLookup() notification_center = NotificationCenter() notification_center.add_observer(self, sender=lookup) if self.account.sip.outbound_proxy is not None: uri = SIPURI(host=self.account.sip.outbound_proxy.host, port=self.account.sip.outbound_proxy.port, parameters={'transport': self.account.sip.outbound_proxy.transport}) else: uri = self.target.uri lookup.lookup_sip_proxy(uri, settings.sip.transport_list) def _display_xcapdiff(self, xcap_diff): message = [] message.append('XCAP diff for XCAP root %s' % xcap_diff.xcap_root) for child in xcap_diff: if isinstance(child, Document): message.append(' %s document %s for AUID %s changed' % ('Global' if child.selector.globaltree is not None else "User's %s" % child.selector.userstree, child.selector.document, child.selector.auid)) message.append(' URL: %s' % child.selector) if child.previous_etag: message.append(' Previous ETag: %s' % child.previous_etag) if child.new_etag: message.append(' New ETag: %s' % child.new_etag) if child.empty_body: message.append(' Body did not change') elif isinstance(child, Element): message.append(' %s element %s in document %s for AUID %s changed' % ('Global' if child.selector.globaltree is not None else "User's %s" % child.selector.userstree, child.selector.node, child.selector.document, child.selector.auid)) message.append(' URL: %s/%s' % (xcap_diff.xcap_root, child.selector)) elif isinstance(child, Attribute): message.append(' %s attribute %s in document %s for AUID %s changed' % ('Global' if child.selector.globaltree is not None else "User's %s" % child.selector.userstree, child.selector.node, child.selector.document, child.selector.auid)) message.append(' URL: %s/%s' % (xcap_diff.xcap_root, child.selector)) if child.value: message.append(' New value: %s' % child.value) self.output.put('\n'.join(message)) if __name__ == "__main__": description = "This script subscribes to the xcap-diff event package for the given SIP account. The program will un-SUBSCRIBE and quit when CTRL+D is pressed." usage = "%prog [options]" parser = OptionParser(usage=usage, description=description) parser.print_usage = parser.print_help parser.add_option("-a", "--account-name", type="string", dest="account_name", help="The name of the account to use.") parser.add_option("-s", "--trace-sip", action="store_true", dest="trace_sip", default=False, help="Dump the raw contents of incoming and outgoing SIP messages (disabled by default).") parser.add_option("-j", "--trace-pjsip", action="store_true", dest="trace_pjsip", default=False, help="Print PJSIP logging output (disabled by default).") parser.add_option("-n", "--trace-notifications", action="store_true", dest="trace_notifications", default=False, help="Print all notifications (disabled by default).") options, args = parser.parse_args() try: application = SubscriptionApplication(options.account_name, options.trace_sip, options.trace_pjsip, options.trace_notifications) return_code = application.run() - except RuntimeError, e: - print "Error: %s" % str(e) + except RuntimeError as e: + print("Error: %s" % str(e)) sys.exit(1) - except SIPCoreError, e: - print "Error: %s" % str(e) + except SIPCoreError as e: + print("Error: %s" % str(e)) sys.exit(1) else: sys.exit(return_code) diff --git a/sipclient/configuration/datatypes.py b/sipclient/configuration/datatypes.py index 14e5e13..28edc34 100644 --- a/sipclient/configuration/datatypes.py +++ b/sipclient/configuration/datatypes.py @@ -1,212 +1,212 @@ """Definitions of datatypes for use in settings extensions""" __all__ = ['ResourcePath', 'UserDataPath', 'SoundFile', 'AccountSoundFile'] import os import sys -import urlparse +import urllib.parse from application.python.descriptor import classproperty, WriteOnceAttribute from sipsimple.configuration.datatypes import Hostname ## Path datatypes class ResourcePath(object): def __init__(self, path): - if not isinstance(path, unicode): + if not isinstance(path, str): path = path.decode(sys.getfilesystemencoding()) self.path = os.path.normpath(path) def __getstate__(self): - return unicode(self.path) + return str(self.path) def __setstate__(self, state): self.__init__(state) @property def normalized(self): path = os.path.expanduser(self.path) if os.path.isabs(path): return os.path.realpath(path) return os.path.realpath(os.path.join(self.resources_directory, path)) @classproperty def resources_directory(cls): binary_directory = os.path.dirname(os.path.realpath(sys.argv[0])) if os.path.basename(binary_directory) == 'bin': application_directory = os.path.dirname(binary_directory) else: application_directory = binary_directory from sipsimple.configuration.settings import SIPSimpleSettings settings = SIPSimpleSettings() if os.path.basename(binary_directory) == 'bin': resources_component = settings.resources_directory or 'share/sipclients' else: resources_component = settings.resources_directory or 'resources' return os.path.realpath(os.path.join(application_directory, resources_component)) def __eq__(self, other): try: return self.path == other.path except AttributeError: return False def __hash__(self): return hash(self.path) def __repr__(self): return '%s(%r)' % (self.__class__.__name__, self.path) def __unicode__(self): - return unicode(self.path) + return str(self.path) class UserDataPath(object): def __init__(self, path): - if not isinstance(path, unicode): + if not isinstance(path, str): path = path.decode(sys.getfilesystemencoding()) self.path = os.path.normpath(path) def __getstate__(self): - return unicode(self.path) + return str(self.path) def __setstate__(self, state): self.__init__(state) @property def normalized(self): path = os.path.expanduser(self.path) if os.path.isabs(path): return path from sipsimple.configuration.settings import SIPSimpleSettings settings = SIPSimpleSettings() return os.path.realpath(os.path.join(settings.user_data_directory, path)) def __eq__(self, other): try: return self.path == other.path except AttributeError: return False def __hash__(self): return hash(self.path) def __repr__(self): return '%s(%r)' % (self.__class__.__name__, self.path) def __unicode__(self): - return unicode(self.path) + return str(self.path) class SoundFile(object): def __init__(self, path, volume=100): self.path = ResourcePath(path) self.volume = int(volume) if self.volume < 0 or self.volume > 100: raise ValueError("illegal volume level: %d" % self.volume) def __getstate__(self): - return u'%s,%s' % (self.path.__getstate__(), self.volume) + return '%s,%s' % (self.path.__getstate__(), self.volume) def __setstate__(self, state): try: - path, volume = state.rsplit(u',', 1) + path, volume = state.rsplit(',', 1) except ValueError: self.__init__(state) else: self.__init__(path, volume) def __repr__(self): return '%s(%r, %r)' % (self.__class__.__name__, self.path, self.volume) def __unicode__(self): - return u'%s,%d' % (self.path, self.volume) + return '%s,%d' % (self.path, self.volume) class AccountSoundFile(object): class DefaultSoundFile(object): def __init__(self, setting): self.setting = setting def __repr__(self): return 'AccountSoundFile.DefaultSoundFile(%s)' % self.setting __str__ = __repr__ def __init__(self, sound_file, *args, **kwargs): if isinstance(sound_file, self.DefaultSoundFile): self._sound_file = sound_file if args or kwargs: raise ValueError("other parameters cannot be specified if sound file is instance of DefaultSoundFile") else: self._sound_file = SoundFile(sound_file, *args, **kwargs) def __getstate__(self): if isinstance(self._sound_file, self.DefaultSoundFile): - return u'default:%s' % self._sound_file.setting + return 'default:%s' % self._sound_file.setting else: - return u'file:%s' % self._sound_file.__getstate__() + return 'file:%s' % self._sound_file.__getstate__() def __setstate__(self, state): - type, value = state.split(u':', 1) - if type == u'default': + type, value = state.split(':', 1) + if type == 'default': self._sound_file = self.DefaultSoundFile(value) - elif type == u'file': + elif type == 'file': self._sound_file = SoundFile.__new__(SoundFile) self._sound_file.__setstate__(value) @property def sound_file(self): if isinstance(self._sound_file, self.DefaultSoundFile): from sipsimple.configuration.settings import SIPSimpleSettings setting = SIPSimpleSettings() for comp in self._sound_file.setting.split('.'): setting = getattr(setting, comp) return setting else: return self._sound_file def __repr__(self): if isinstance(self._sound_file, self.DefaultSoundFile): return '%s(%r)' % (self.__class__.__name__, self._sound_file) else: return '%s(%r, volume=%d)' % (self.__class__.__name__, self._sound_file.path, self._sound_file.volume) def __unicode__(self): if isinstance(self._sound_file, self.DefaultSoundFile): - return u'DEFAULT' + return 'DEFAULT' else: - return u'%s,%d' % (self._sound_file.path, self._sound_file.volume) + return '%s,%d' % (self._sound_file.path, self._sound_file.volume) class HTTPURL(object): url = WriteOnceAttribute() def __init__(self, value): - url = urlparse.urlparse(value) - if url.scheme not in (u'http', u'https'): + url = urllib.parse.urlparse(value) + if url.scheme not in ('http', 'https'): raise ValueError(NSLocalizedString("Illegal HTTP URL scheme (http and https only): %s", "Preference option error") % url.scheme) # check port and hostname Hostname(url.hostname) if url.port is not None: if not (0 < url.port < 65536): raise ValueError(NSLocalizedString("Illegal port value: %d", "Preference option error") % url.port) self.url = url def __getstate__(self): - return unicode(self.url.geturl()) + return str(self.url.geturl()) def __setstate__(self, state): self.__init__(state) def __getitem__(self, index): return self.url.__getitem__(index) def __getattr__(self, attr): if attr in ('scheme', 'netloc', 'path', 'params', 'query', 'fragment', 'username', 'password', 'hostname', 'port'): return getattr(self.url, attr) else: raise AttributeError("'%s' object has no attribute '%s'" % (self.__class__.__name__, attr)) def __unicode__(self): - return unicode(self.url.geturl()) + return str(self.url.geturl()) diff --git a/sipclient/log.py b/sipclient/log.py index 42ce8e9..8e3e411 100644 --- a/sipclient/log.py +++ b/sipclient/log.py @@ -1,321 +1,321 @@ """Logging support for SIP SIMPLE Client""" __all__ = ["Logger"] import datetime import os import sys from pprint import pformat from application import log from application.notification import IObserver, NotificationCenter from application.python.queue import EventQueue from application.system import makedirs -from zope.interface import implements +from zope.interface import implementer from sipsimple.configuration.settings import SIPSimpleSettings +@implementer(IObserver) class Logger(object): - implements(IObserver) # public methods # def __init__(self, sip_to_stdout=False, msrp_to_stdout=False, pjsip_to_stdout=False, notifications_to_stdout=False, msrp_level=log.level.ERROR): self.sip_to_stdout = sip_to_stdout self.msrp_to_stdout = msrp_to_stdout self.pjsip_to_stdout = pjsip_to_stdout self.notifications_to_stdout = notifications_to_stdout self.msrp_level = msrp_level self._siptrace_filename = None self._siptrace_file = None self._siptrace_error = False self._siptrace_start_time = None self._siptrace_packet_count = 0 self._msrptrace_filename = None self._msrptrace_file = None self._msrptrace_error = False self._pjsiptrace_filename = None self._pjsiptrace_file = None self._pjsiptrace_error = False self._notifications_filename = None self._notifications_file = None self._notifications_error = False self._event_queue = EventQueue(handler=self._process_notification, name='Log handling') self._log_directory_error = False def start(self): # try to create the log directory try: self._init_log_directory() except Exception: pass # register to receive log notifications notification_center = NotificationCenter() notification_center.add_observer(self) # start the thread processing the notifications self._event_queue.start() def stop(self): # stop the thread processing the notifications self._event_queue.stop() self._event_queue.join() # close sip trace file if self._siptrace_file is not None: self._siptrace_file.close() self._siptrace_file = None # close msrp trace file if self._msrptrace_file is not None: self._msrptrace_file.close() self._msrptrace_file = None # close pjsip trace file if self._pjsiptrace_file is not None: self._pjsiptrace_file.close() self._pjsiptrace_file = None # close notifications trace file if self._notifications_file is not None: self._notifications_file.close() self._notifications_file = None # unregister from receiving notifications notification_center = NotificationCenter() notification_center.remove_observer(self) def handle_notification(self, notification): self._event_queue.put(notification) def _process_notification(self, notification): settings = SIPSimpleSettings() handler = getattr(self, '_NH_%s' % notification.name, None) if handler is not None: handler(notification) handler = getattr(self, '_LH_%s' % notification.name, None) if handler is not None: handler(notification) if notification.name not in ('SIPEngineLog', 'SIPEngineSIPTrace') and (self.notifications_to_stdout or settings.logs.trace_notifications): message = 'Notification name=%s sender=%s' % (notification.name, notification.sender) if notification.data is not None: message += '\n%s' % pformat(notification.data.__dict__) if self.notifications_to_stdout: - print '%s: %s' % (datetime.datetime.now(), message) + print(('%s: %s' % (datetime.datetime.now(), message))) if settings.logs.trace_notifications: try: self._init_log_file('notifications') except Exception: pass else: self._notifications_file.write('%s [%s %d]: %s\n' % (datetime.datetime.now(), os.path.basename(sys.argv[0]).rstrip('.py'), os.getpid(), message)) self._notifications_file.flush() # notification handlers # def _NH_CFGSettingsObjectDidChange(self, notification): settings = SIPSimpleSettings() if notification.sender is settings: if 'logs.directory' in notification.data.modified: # sip trace if self._siptrace_file is not None: self._siptrace_file.close() self._siptrace_file = None # pjsip trace if self._pjsiptrace_file is not None: self._pjsiptrace_file.close() self._pjsiptrace_file = None # notifications trace if self._notifications_file is not None: self._notifications_file.close() self._notifications_file = None # try to create the log directory try: self._init_log_directory() except Exception: pass # log handlers # def _LH_SIPEngineSIPTrace(self, notification): settings = SIPSimpleSettings() if not self.sip_to_stdout and not settings.logs.trace_sip: return if self._siptrace_start_time is None: self._siptrace_start_time = notification.datetime self._siptrace_packet_count += 1 if notification.data.received: direction = "RECEIVED" else: direction = "SENDING" buf = ["%s: Packet %d, +%s" % (direction, self._siptrace_packet_count, (notification.datetime - self._siptrace_start_time))] buf.append("%(source_ip)s:%(source_port)d -(SIP over %(transport)s)-> %(destination_ip)s:%(destination_port)d" % notification.data.__dict__) buf.append(notification.data.data) buf.append('--') message = '\n'.join(buf) if self.sip_to_stdout: - print '%s: %s\n' % (notification.datetime, message) + print(('%s: %s\n' % (notification.datetime, message))) if settings.logs.trace_sip: try: self._init_log_file('siptrace') except Exception: pass else: self._siptrace_file.write('%s [%s %d]: %s\n' % (notification.datetime, os.path.basename(sys.argv[0]).rstrip('.py'), os.getpid(), message)) self._siptrace_file.flush() def _LH_SIPEngineLog(self, notification): settings = SIPSimpleSettings() if not self.pjsip_to_stdout and not settings.logs.trace_pjsip: return message = "(%(level)d) %(message)s" % notification.data.__dict__ if self.pjsip_to_stdout: - print message + print(message) if settings.logs.trace_pjsip: try: self._init_log_file('pjsiptrace') except Exception: pass else: self._pjsiptrace_file.write('[%s %d] %s\n' % (os.path.basename(sys.argv[0]).rstrip('.py'), os.getpid(), message)) self._pjsiptrace_file.flush() def _LH_DNSLookupTrace(self, notification): settings = SIPSimpleSettings() if not self.sip_to_stdout and not settings.logs.trace_sip: return message = 'DNS lookup %(query_type)s %(query_name)s' % notification.data.__dict__ if notification.data.error is None: message += ' succeeded, ttl=%d: ' % notification.data.answer.ttl if notification.data.query_type == 'A': message += ", ".join(record.address for record in notification.data.answer) elif notification.data.query_type == 'SRV': message += ", ".join('%d %d %d %s' % (record.priority, record.weight, record.port, record.target) for record in notification.data.answer) elif notification.data.query_type == 'NAPTR': message += ", ".join('%d %d "%s" "%s" "%s" %s' % (record.order, record.preference, record.flags, record.service, record.regexp, record.replacement) for record in notification.data.answer) else: import dns.resolver message_map = {dns.resolver.NXDOMAIN: 'DNS record does not exist', dns.resolver.NoAnswer: 'DNS response contains no answer', dns.resolver.NoNameservers: 'no DNS name servers could be reached', dns.resolver.Timeout: 'no DNS response received, the query has timed out'} message += ' failed: %s' % message_map.get(notification.data.error.__class__, '') if self.sip_to_stdout: - print '%s: %s' % (notification.datetime, message) + print(('%s: %s' % (notification.datetime, message))) if settings.logs.trace_sip: try: self._init_log_file('siptrace') except Exception: pass else: self._siptrace_file.write('%s [%s %d]: %s\n' % (notification.datetime, os.path.basename(sys.argv[0]).rstrip('.py'), os.getpid(), message)) self._siptrace_file.flush() def _LH_MSRPTransportTrace(self, notification): settings = SIPSimpleSettings() if not self.msrp_to_stdout and not settings.logs.trace_msrp: return if getattr(notification.sender, 'socket', None) is None: return arrow = {'incoming': '<--', 'outgoing': '-->'}[notification.data.direction] local_address = notification.sender.getHost() local_address = '%s:%d' % (local_address.host, local_address.port) remote_address = notification.sender.getPeer() remote_address = '%s:%d' % (remote_address.host, remote_address.port) message = '%s %s %s\n' % (local_address, arrow, remote_address) + notification.data.data if self.msrp_to_stdout: - print '%s: %s' % (notification.datetime, message) + print(('%s: %s' % (notification.datetime, message))) if settings.logs.trace_msrp: try: self._init_log_file('msrptrace') except Exception: pass else: self._msrptrace_file.write('%s [%s %d]: %s\n' % (notification.datetime, os.path.basename(sys.argv[0]).rstrip('.py'), os.getpid(), message)) self._msrptrace_file.flush() def _LH_MSRPLibraryLog(self, notification): settings = SIPSimpleSettings() if not self.msrp_to_stdout and not settings.logs.trace_msrp: return if notification.data.level < self.msrp_level: return message = '%s%s' % (notification.data.level.prefix, notification.data.message) if self.msrp_to_stdout: - print '%s: %s' % (notification.datetime, message) + print(('%s: %s' % (notification.datetime, message))) if settings.logs.trace_msrp: try: self._init_log_file('msrptrace') except Exception: pass else: self._msrptrace_file.write('%s [%s %d]: %s\n' % (notification.datetime, os.path.basename(sys.argv[0]).rstrip('.py'), os.getpid(), message)) self._msrptrace_file.flush() # private methods # def _init_log_directory(self): settings = SIPSimpleSettings() log_directory = settings.logs.directory.normalized try: makedirs(log_directory) - except Exception, e: + except Exception as e: if not self._log_directory_error: - print "failed to create logs directory '%s': %s" % (log_directory, e) + print(("failed to create logs directory '%s': %s" % (log_directory, e))) self._log_directory_error = True self._siptrace_error = True self._pjsiptrace_error = True self._notifications_error = True raise else: self._log_directory_error = False # sip trace if self._siptrace_filename is None: self._siptrace_filename = os.path.join(log_directory, 'sip_trace.txt') self._siptrace_error = False # msrp trace if self._msrptrace_filename is None: self._msrptrace_filename = os.path.join(log_directory, 'msrp_trace.txt') self._msrptrace_error = False # pjsip trace if self._pjsiptrace_filename is None: self._pjsiptrace_filename = os.path.join(log_directory, 'pjsip_trace.txt') self._pjsiptrace_error = False # notifications trace if self._notifications_filename is None: self._notifications_filename = os.path.join(log_directory, 'notifications_trace.txt') self._notifications_error = False def _init_log_file(self, type): if getattr(self, '_%s_file' % type) is None: self._init_log_directory() filename = getattr(self, '_%s_filename' % type) try: setattr(self, '_%s_file' % type, open(filename, 'a')) - except Exception, e: + except Exception as e: if not getattr(self, '_%s_error' % type): - print "failed to create log file '%s': %s" % (filename, e) + print(("failed to create log file '%s': %s" % (filename, e))) setattr(self, '_%s_error' % type, True) raise else: setattr(self, '_%s_error' % type, False) diff --git a/sipclient/ui.py b/sipclient/ui.py index 5e607c0..15b4594 100644 --- a/sipclient/ui.py +++ b/sipclient/ui.py @@ -1,687 +1,686 @@ """ Implements a library that can be used for buildina a fully featured SIP User Agent working in a terminal text window. See sip_session.py script for an actual implementation. """ __all__ = ["RichText", "CompoundRichText", "Prompt", "Question", "UI"] import atexit -import cPickle as pickle +import pickle as pickle import fcntl import os import re import select import signal import struct import sys import termios from collections import deque from threading import RLock, Thread from application.python.decorator import decorator, preserve_signature from application.python.queue import EventQueue from application.python.types import Singleton from application.system import openfile from application.notification import NotificationCenter, NotificationData @decorator def run_in_ui_thread(func): @preserve_signature(func) def wrapper(self, *args, **kwargs): self.event_queue.put((func, self, args, kwargs)) return wrapper class RichText(object): colors = {'default': 9, 'red': 61, 'darkred': 1, 'lightgreen': 62, 'darkgreen': 2, 'yellow': 63, 'darkyellow': 3, 'cyan': 66, 'lightblue': 6, 'blue': 64, 'darkblue': 4, 'magenta': 65, 'purple': 5, 'white': 67, 'lightgrey': 7, 'darkgrey': 60, 'black': 0} def __init__(self, text, foreground='default', background='default', bold=False, underline=False, blink=False): self.text = text self.foreground = foreground self.background = background self.bold = bold self.underline = underline self.blink = blink def __str__(self): - if isinstance(self.text, unicode): + if isinstance(self.text, str): text = self.text.encode(sys.getfilesystemencoding()) else: text = self.text return '\x1b[%sm%s\x1b[0m' % (self.mode, text) def __len__(self): return len(self.text) def __getitem__(self, index): return self.__class__(self.text.__getitem__(index), foreground=self.foreground, background=self.background, bold=self.bold, underline=self.underline, blink=self.blink) def __add__(self, other): return CompoundRichText([self, other]) @property def mode(self): attributes = [str(30+self.colors.get(self.foreground)), str(40+self.colors.get(self.background)), '1' if self.bold else '22', '4' if self.underline else '24', '5' if self.blink else '25'] return ';'.join(attributes) class CompoundRichText(RichText): def __init__(self, text_list): self.text_list = text_list def __str__(self): txt = '' for text in self.text_list: - if isinstance(text, unicode): + if isinstance(text, str): text = text.encode(sys.getfilesystemencoding()) else: text = str(text) txt += text return txt def __len__(self): return sum(len(text) for text in self.text_list) def __add__(self, other): if isinstance(other, CompoundRichText): return CompoundRichText(self.text_list+other.text_list) else: return CompoundRichText(self.text_list+[other]) def __iadd__(self, other): self.text_list.append(other) return self class Prompt(RichText): def __str__(self): - if isinstance(self.text, unicode): + if isinstance(self.text, str): text = self.text.encode(sys.getfilesystemencoding()) else: text = self.text return '\x1b[%sm%s>\x1b[0m ' % (self.mode, text) def __len__(self): return len(self.text)+2 class Question(RichText): def __init__(self, text, answers, *args, **kwargs): RichText.__init__(self, text, *args, **kwargs) self.answers = answers def __getitem__(self, index): return self.__class__(self.text.__getitem__(index), answers=self.answers, foreground=self.foreground, background=self.background, bold=self.bold, underline=self.underline, blink=self.blink) class Input(object): def __init__(self): self.history_file = None self.lines = [] self.current_line_index = None self.cursor_position = None def _get_current_line(self): if self.current_line_index is None: raise RuntimeError('no current line available') return self.lines[self.current_line_index] def _set_current_line(self, value): if value is None: self.current_line_index = None return if self.current_line_index is None: raise RuntimeError('no current line available') self.lines[self.current_line_index] = value current_line = property(_get_current_line, _set_current_line) del _get_current_line, _set_current_line def add_history(self, history_file): self.history_file = history_file try: self.lines = pickle.load(open(history_file, 'rb')) except (IOError, TypeError, EOFError): self.lines = [] def save_history(self): - with openfile(self.history_file, 'wb', permissions=0600) as history_file: + with openfile(self.history_file, 'wb', permissions=0o600) as history_file: pickle.dump(self.lines, history_file) def add_line(self, text=''): self.lines.append(text) self.current_line_index = len(self.lines)-1 self.cursor_position = len(text) def copy_current_line(self): if self.current_line_index != len(self.lines) - 1: self.lines[-1] = self.current_line def line_up(self, count=1): if self.current_line_index is None: raise RuntimeError('no current line available') if self.current_line_index - count < 0: raise KeyError('too many lines up') self.current_line_index -= count self.cursor_position = len(self.current_line) def line_down(self, count=1): if self.current_line_index is None: raise RuntimeError('no current line available') if self.current_line_index + count >= len(self.lines): raise KeyError('too many lines down') self.current_line_index += count self.cursor_position = len(self.current_line) class TTYFileWrapper(object): def __init__(self, file): if not file.isatty(): raise RuntimeError('TTYFileWrapper is supposed to wrap a tty file') self.file = file self.buffer = '' self.lock = RLock() # no-ops / simple ops def close(self): pass def fileno(self): return self.file.fileno() def isatty(self): return True def tell(self): return self.file.tell() def write(self, str): with self.lock: if not str: return ui = UI() if ui.stopping: self.file.write(str) else: lines = re.split(r'\r\n|\r|\n', str) lines[0] = self.buffer + lines[0] self.buffer = lines[-1] ui.writelines(lines[:-1]) def writelines(self, sequence): with self.lock: for text in sequence: self.write(text) def flush(self): with self.lock: if self.buffer: ui = UI() ui.writelines([self.buffer]) self.buffer = '' def send_to_file(self): if self.buffer: self.file.write(self.buffer) -class UI(Thread): - __metaclass__ = Singleton - +class UI(Thread, metaclass=Singleton): control_chars = {'\x01': 'home', '\x04': 'eof', '\x05': 'end', '\x0a': 'newline', '\x0d': 'newline', '\x1b[A': 'cursorup', '\x1b[B': 'cursordown', '\x1b[C': 'cursorright', '\x1b[D': 'cursorleft', '\x1b[F': 'end', '\x1b[H': 'home', '\x7f': 'delete'} # public functions # def __init__(self, history_file=None): Thread.__init__(self, target=self._run, name='UI-Thread') self.setDaemon(True) self.__dict__['prompt'] = Prompt('') self.__dict__['status'] = None self.command_sequence = '/' self.application_control_char = '\x18' # ctrl-X self.application_control_bindings = {} self.display_commands = True self.display_text = True self.cursor_x = None self.cursor_y = None self.displaying_question = False self.input = Input() self.input.add_history(history_file) self.last_window_size = None self.prompt_y = None self.questions = deque() self.stopping = False self.lock = RLock() - self.event_queue = EventQueue(handler=lambda (function, self, args, kwargs): function(self, *args, **kwargs), name='UI operation handling') + self.event_queue = EventQueue(handler=lambda function_self_args_kwargs: function_self_args_kwargs[0](function_self_args_kwargs[1], *function_self_args_kwargs[2], **function_self_args_kwargs[3]), name='UI operation handling') def start(self, prompt='', command_sequence='/', control_char='\x18', control_bindings={}, display_commands=True, display_text=True): with self.lock: if self.isAlive(): raise RuntimeError('UI already active') if not sys.stdin.isatty(): raise RuntimeError('UI cannot be used on a non-TTY') if not sys.stdout.isatty(): raise RuntimeError('UI cannot be used on a non-TTY') stdin_fd = sys.stdin.fileno() self.command_sequence = command_sequence self.application_control_char = control_char self.application_control_bindings = control_bindings self.display_commands = display_commands self.display_text = display_text # wrap sys.stdout sys.stdout = TTYFileWrapper(sys.stdout) # and possibly sys.stderr if sys.stderr.isatty(): sys.stderr = TTYFileWrapper(sys.stderr) # change input to character-mode old_settings = termios.tcgetattr(stdin_fd) new_settings = termios.tcgetattr(stdin_fd) new_settings[3] &= ~termios.ECHO & ~termios.ICANON new_settings[6][termios.VMIN] = '\000' termios.tcsetattr(stdin_fd, termios.TCSADRAIN, new_settings) atexit.register(termios.tcsetattr, stdin_fd, termios.TCSADRAIN, old_settings) # find out cursor position in terminal self._raw_write('\x1b[6n') if select.select([stdin_fd], [], [], None)[0]: line, col = os.read(stdin_fd, 10)[2:-1].split(';') line = int(line) col = int(col) # scroll down the terminal until everything goes up self._scroll_up(line-1) # move the cursor to the upper left side corner self._raw_write('\x1b[H') self.cursor_x = 1 self.cursor_y = 1 # display the prompt self.prompt_y = 1 self.input.add_line() self._update_prompt() # make sure we know when the window gets resized self.last_window_size = self.window_size signal.signal(signal.SIGWINCH, lambda signum, frame: self._window_resized()) self.event_queue.start() Thread.start(self) # this will trigger the update of the prompt self.prompt = prompt @run_in_ui_thread def stop(self): with self.lock: self.stopping = True self.status = None sys.stdout.send_to_file() if isinstance(sys.stderr, TTYFileWrapper): sys.stderr.send_to_file() self._raw_write('\n\x1b[2K') self.input.save_history() def write(self, text): self.writelines([text]) @run_in_ui_thread def writelines(self, text_lines): with self.lock: if not text_lines: return # go to beginning of prompt line self._raw_write('\x1b[%d;%dH' % (self.prompt_y, 1)) # erase everything beneath it self._raw_write('\x1b[0J') # start writing lines window_size = self.window_size for text in text_lines: # write the line self._raw_write('%s\n' % text) # calculate the number of lines the text will produce text_lines = (len(text)-1)/window_size.x + 1 # calculate how much the text will automatically scroll the window window_height = struct.unpack('HHHH', fcntl.ioctl(sys.stdout.fileno(), termios.TIOCGWINSZ, struct.pack('HHHH', 0, 0, 0, 0)))[0] auto_scroll_amount = max(0, (self.prompt_y+text_lines-1) - (window_height-1)) # calculate the new position of the prompt self.prompt_y += text_lines - auto_scroll_amount # we might need to scroll up to make the prompt position visible again scroll_up = self.prompt_y - window_height if scroll_up > 0: self.prompt_y -= scroll_up self._scroll_up(scroll_up) # redraw the prompt self._update_prompt() @run_in_ui_thread def add_question(self, question): with self.lock: self.questions.append(question) if len(self.questions) == 1: self._update_prompt() @run_in_ui_thread def remove_question(self, question): with self.lock: first_question = (question == self.questions[0]) self.questions.remove(question) if not self.questions or first_question: self.displaying_question = False self._update_prompt() # properties # @property def window_size(self): class WindowSize(tuple): - def __init__(ws_self, (y, x)): + def __init__(ws_self, xxx_todo_changeme): + (y, x) = xxx_todo_changeme ws_self.x = x ws_self.y = y if self.status is None else y-1 return WindowSize(struct.unpack('HHHH', fcntl.ioctl(sys.stdout.fileno(), termios.TIOCGWINSZ, struct.pack('HHHH', 0, 0, 0, 0)))[:2]) def _get_prompt(self): return self.__dict__['prompt'] @run_in_ui_thread def _set_prompt(self, value): with self.lock: if not isinstance(value, Prompt): value = Prompt(value) self.__dict__['prompt'] = value self._update_prompt() prompt = property(_get_prompt, _set_prompt) del _get_prompt, _set_prompt def _get_status(self): return self.__dict__['status'] @run_in_ui_thread def _set_status(self, status): with self.lock: - if isinstance(status, unicode): + if isinstance(status, str): status = status.encode(sys.getfilesystemencoding()) try: old_status = self.__dict__['status'] except KeyError: self.__dict__['status'] = status else: self.__dict__['status'] = status if old_status is not None and status is None: status_y, window_length = struct.unpack('HHHH', fcntl.ioctl(sys.stdout.fileno(), termios.TIOCGWINSZ, struct.pack('HHHH', 0, 0, 0, 0)))[:2] # save current cursor position self._raw_write('\x1b[s') # goto line status_y self._raw_write('\x1b[%d;%dH' % (status_y, 1)) # erase it self._raw_write('\x1b[2K') # restore the cursor position self._raw_write('\x1b[u') else: self._update_prompt() status = property(_get_status, _set_status) del _get_status, _set_status # private functions # def _run(self): wait_control_char = False while True: stdin_fd = sys.__stdin__.fileno() if select.select([stdin_fd], [], [], None)[0]: chars = list(os.read(stdin_fd, 4096)) while chars: if self.stopping: return with self.lock: char = chars.pop(0) if ord(char) < 32 or ord(char) == 127: if char == '\x1b': if chars and chars[0] == '[': char += chars.pop(0) while chars and not chars[0].isalpha(): char += chars.pop(0) if chars: char += chars.pop(0) if self.questions: pass elif char == self.application_control_char: wait_control_char = not wait_control_char elif not self.questions: wait_control_char = False handler = getattr(self, '_CH_%s' % self.control_chars.get(char, 'default')) handler(char) elif wait_control_char: wait_control_char = False if char in self.application_control_bindings: notification_center = NotificationCenter() words = [word for word in re.split(r'\s+', self.application_control_bindings[char]) if word] notification_center.post_notification('UIInputGotCommand', sender=self, data=NotificationData(command=words[0], args=words[1:])) elif self.questions: question = self.questions[0] if char in question.answers: self._raw_write(char) self.displaying_question = False self.remove_question(question) notification_center = NotificationCenter() notification_center.post_notification('UIQuestionGotAnswer', sender=question, data=NotificationData(answer=char)) else: # insert char in input.current_line at input.cursor_position and advance cursor self.input.current_line = self.input.current_line[:self.input.cursor_position] + char + self.input.current_line[self.input.cursor_position:] self.input.cursor_position += 1 self._update_prompt() def _raw_write(self, text): sys.__stdout__.write(str(text)) sys.__stdout__.flush() def _window_resized(self): pass def _update_prompt(self): # The (X-1)/window_size.x+1 are because the position in the terminal is # a 1-based index; the + 1 when calculating the indexes are because the # positions we keep are 0-based. if self.displaying_question or self.stopping: return if self.questions: window_size = self.window_size question = self.questions[0] # we also want to leave a space after the question and we need an # extra position for the cursor text_len = len(question) + 2 # calculate how much we need to scroll up text_lines = (text_len-1)/window_size.x + 1 scroll_up = text_lines - (window_size.y - self.prompt_y + 1) if scroll_up > 0: self._scroll_up(scroll_up) self.prompt_y -= scroll_up # go to the position where the question will be rendered self._raw_write('\x1b[%d;%dH' % (self.prompt_y, 1)) # erase everything beneath it self._raw_write('\x1b[0J') # might need to draw the status self._draw_status() # draw the question self._raw_write(question) # and a space self._raw_write(' ') # calculate the cursor position self.cursor_y = (text_len-1)/window_size.x + self.prompt_y # no need to add 1 since we had to subtract 1 self.cursor_x = (text_len-1)%window_size.x + 1 # the new prompt will now be just under the question self.prompt_y += text_lines self.displaying_question = True else: window_size = self.window_size text_len = len(self.prompt) + len(self.input.current_line) # we also need a position for the cursor if it's at the end of the line if self.input.cursor_position == len(self.input.current_line): text_len += 1 # calculate how much we need to scroll up text_lines = (text_len-1)/window_size.x + 1 scroll_up = text_lines - (window_size.y - self.prompt_y + 1) if scroll_up > 0: self._scroll_up(scroll_up) self.prompt_y -= scroll_up # goto the position of the new prompt self._raw_write('\x1b[%d;%dH' % (self.prompt_y, 1)) # erase everything beneath it self._raw_write('\x1b[0J') # might need to draw the status self._draw_status() # force going to the new prompt position self._raw_write('\x1b[%d;%dH' % (self.prompt_y, 1)) # draw the prompt and the text self._raw_write(self.prompt) self._raw_write(self.input.current_line) # move the cursor to it's correct position cursor_position = len(self.prompt) + self.input.cursor_position + 1 self.cursor_y = (cursor_position-1)/window_size.x + self.prompt_y # no need to add 1 since we had to subtract 1 self.cursor_x = (cursor_position-1)%window_size.x + 1 self._raw_write('\x1b[%d;%dH' % (self.cursor_y, self.cursor_x)) def _draw_status(self): status = self.status if status is not None: status_y, window_length = struct.unpack('HHHH', fcntl.ioctl(sys.stdout.fileno(), termios.TIOCGWINSZ, struct.pack('HHHH', 0, 0, 0, 0)))[:2] # save current cursor position self._raw_write('\x1b[s') # goto line status_y self._raw_write('\x1b[%d;%dH' % (status_y, 1)) # erase it self._raw_write('\x1b[2K') # display the status if len(status) > window_length: status = status[:window_length] self._raw_write(status) # restore the cursor position self._raw_write('\x1b[u') def _scroll_up(self, lines): window_height = struct.unpack('HHHH', fcntl.ioctl(sys.stdout.fileno(), termios.TIOCGWINSZ, struct.pack('HHHH', 0, 0, 0, 0)))[0] self._raw_write('\x1b[s\x1b[%d;1H' % window_height + '\x1bD' * lines + '\x1b[u') # control character handlers # def _CH_default(self, char): #print 'Got control char %s' % ''.join('%02X' % ord(c) for c in char) pass def _CH_home(self, char): if self.input.cursor_position > 0: self.input.cursor_position = 0 self._update_prompt() def _CH_eof(self, char): notification_center = NotificationCenter() notification_center.post_notification('UIInputGotCommand', sender=self, data=NotificationData(command='eof', args=[])) def _CH_end(self, char): if self.input.cursor_position < len(self.input.current_line): self.input.cursor_position = len(self.input.current_line) self._update_prompt() def _CH_newline(self, char): if self.input.current_line: # copy the current line to the last line self.input.copy_current_line() window_size = self.window_size # calculate the length of the line just entered text_len = len(self.prompt) + len(self.input.current_line) text_lines = (text_len-1)/window_size.x + 1 # save the current line and add a new input line current_line = self.input.current_line self.input.add_line() # see if it's a command or plain text notification_center = NotificationCenter() if current_line.startswith(self.command_sequence): # calculate the new position of the prompt if self.display_commands: self.prompt_y += text_lines # we need to scroll if the new prompt position is below the window margin, otherwise # some text might go over it scroll_up = self.prompt_y - window_size.y if scroll_up > 0: self.prompt_y -= scroll_up self._scroll_up(scroll_up) # send a notification about the new input words = [word for word in re.split(r'\s+', current_line[len(self.command_sequence):]) if word] notification_center.post_notification('UIInputGotCommand', sender=self, data=NotificationData(command=words[0], args=words[1:])) else: # calculate the new position of the prompt if self.display_text: self.prompt_y += text_lines # we need to scroll if the new prompt position is below the window margin, otherwise # some text might go over it scroll_up = self.prompt_y - window_size.y if scroll_up > 0: self.prompt_y -= scroll_up self._scroll_up(scroll_up) # send a notification about the new input notification_center.post_notification('UIInputGotText', sender=self, data=NotificationData(text=current_line)) # redisplay the prompt self._update_prompt() def _CH_cursorup(self, char): try: self.input.line_up() except KeyError: pass else: self._update_prompt() def _CH_cursordown(self, char): try: self.input.line_down() except KeyError: pass else: self._update_prompt() def _CH_cursorright(self, char): if self.input.cursor_position < len(self.input.current_line): self.input.cursor_position += 1 self._update_prompt() def _CH_cursorleft(self, char): if self.input.cursor_position > 0: self.input.cursor_position -= 1 self._update_prompt() def _CH_delete(self, char): # delete the character in input.current_line at input.cursor_position if self.input.cursor_position > 0: self.input.current_line = self.input.current_line[:self.input.cursor_position-1]+self.input.current_line[self.input.cursor_position:] self.input.cursor_position -= 1 self._update_prompt()