diff --git a/sip-audio-session3 b/sip-audio-session3 index 9aa04d0..7744ddf 100755 --- a/sip-audio-session3 +++ b/sip-audio-session3 @@ -1,1516 +1,1514 @@ #!/usr/bin/env python3 import atexit import glob import os import platform import select import shutil import signal import re import sys import termios import uuid import subprocess from datetime import datetime from eventlib import api from itertools import chain from optparse import OptionParser from threading import Thread from time import sleep from lxml import html 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 gnutls.errors import GNUTLSError from gnutls.crypto import X509Certificate, X509PrivateKey from twisted.internet import reactor from pathlib import Path 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, Header, CORE_REVISION, PJ_VERSION, PJ_SVN_REVISION from sipsimple import __version__ as version 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, BonjourAccountExtension from sipclient.configuration.datatypes import ResourcePath from sipclient.configuration.settings import SIPSimpleSettingsExtension from sipclient.log import Logger from sipclient.system import IPAddressMonitor, copy_default_certificates 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] = 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 self.play_file = None self.playback_wave_player = None self.disable_ringtone = False self.disable_hanguptone = False self.enable_playback = False self.play_failure_code = False def _write(self, message): 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.log_register = options.log_register self.play_failure_code = options.play_failure_code 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') notification_center.add_observer(self, name='RTPStreamICENegotiationStateDidChange') notification_center.add_observer(self, name='SIPSessionGotConferenceInfo') notification_center.add_observer(self, name='TLSTransportHasChanged') notification_center.add_observer(self, name='MediaStreamDidStart') 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(BonjourAccountExtension) SIPSimpleSettings.register_extension(SIPSimpleSettingsExtension) self.config_directory = options.config_directory or config_directory self.disable_ringtone = options.disable_ringtone self.disable_hanguptone = options.disable_hanguptone self.auto_record = options.auto_record try: self.output.put("Using config directory: %s\n" % self.config_directory) SIPApplication.start(self, FileStorage(self.config_directory)) 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() else: self.output.put("SDK version %s, core version %s, PJSIP version %s (%s)\n" % (version, CORE_REVISION, PJ_VERSION.decode(), PJ_SVN_REVISION)) 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) self.scripts_dir = "%s/scripts" % self.config_directory try: makedirs(self.scripts_dir) except Exception as e: log.error('Failed to create scripts directory at {directory}: {exception!s}'.format(directory=dir, exception=e)) self.enable_playback = options.enable_playback if options.playback_dir: self.playback_dir = options.playback_dir else: self.playback_dir = "%s/spool/playback" % self.config_directory makedirs(self.playback_dir) if self.enable_playback: lock_file = "%s/playback.lock" % self.playback_dir if os.path.exists(lock_file): os.remove(lock_file) self.playback_queue = EventQueue(self._handle_outgoing_playback) active_playback_dir = self.playback_dir + '/active' makedirs(active_playback_dir) history_playback_dir = self.playback_dir + '/history' makedirs(history_playback_dir) scripts_playback_dir = self.playback_dir + '/scripts' makedirs(scripts_playback_dir) def poll_playback_directory(self): if self.outgoing_session: reactor.callLater(0.1, self.poll_playback_directory) return files = list(filter(os.path.isfile, glob.glob(self.playback_dir + "/*.wav"))) files.sort(key=lambda x: os.path.getmtime(x)) for file in files: if len(file.split('@')) == 2: active_playback_dir = self.playback_dir + '/active' basename = os.path.basename(file) self.output.put("Audio recording detected %s" % basename) filename = '%s/%s-%s' % (active_playback_dir, datetime.now().strftime("%Y%m%d-%H%M%S"), basename) os.replace(file, filename) play_object = {'target': os.path.splitext(basename)[0], 'filename': filename} self.playback_queue.put(play_object) reactor.callLater(0.1, self.poll_playback_directory) def _handle_outgoing_playback(self, play_object): print(play_object) self.play_file = play_object['filename'] self.start_outgoing_call(play_object['target']) 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() if self.enable_playback: self.output.put("Polling %s for wav files" % self.playback_dir) self.playback_queue.start() reactor.callLater(3, self.poll_playback_directory) 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))) self.output.put('Available audio codecs: %s\n' % ', '.join([codec.decode() for codec in engine._ua.available_codecs])) self.output.put('Available video codecs: %s\n' % ', '.join([codec.decode() for codec in engine._ua.available_video_codecs])) if engine.video_devices: self.output.put('Available cameras: %s\n' % ', '.join(sorted(engine.video_devices))) 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 and not self.disable_ringtone: 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 not self.disable_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 settings.tls.ca_list is None: copy_default_certificates() self.output.put('Initializing default TLS certificates and settings') settings.tls.ca_list = os.path.join(config_directory, 'tls/ca.crt') settings.tls.certificate = os.path.join(config_directory, 'tls/default.crt') settings.tls.verify_server = True settings.save() if self.target is not None: self.start_outgoing_call(target) def start_outgoing_call(self, target): self.target = target if isinstance(self.account, BonjourAccount) and '@' not in self.target: self.output.put('Bonjour mode requires a host in the destination address\n') if not self.enable_playback: self.stop() self.end_cancel_thread() return if self.play_file: lock_file = "%s/playback.lock" % self.playback_dir Path(lock_file).touch() self.output.put("Lock file %s created\n" % lock_file) 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) if not self.enable_playback: self.stop() else: if '.' not in self.target.host.decode() 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) self.session_spool_dir = self.spool_dir + "/" + (self.options.external_id or str(uuid.uuid1())) 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}) tls_name = self.account.sip.tls_name or self.account.sip.outbound_proxy.host elif isinstance(self.account, Account) and self.account.sip.always_use_my_proxy: uri = SIPURI(host=self.account.id.domain) tls_name = self.account.sip.tls_name or self.account.id.domain else: uri = self.target tls_name = uri.host if self.account is not BonjourAccount(): if self.account.id.domain == uri.host.decode(): tls_name = self.account.sip.tls_name or self.account.id.domain elif "isfocus" in str(uri) and uri.host.decode().endswith(self.account.id.domain): tls_name = self.account.conference.tls_name or self.account.sip.tls_name or self.account.id.domain else: is_ip_address = re.match("^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$", uri.host.decode()) or ":" in uri.host.decode() if "isfocus" in str(uri) and self.account.conference.tls_name: tls_name = self.account.conference.tls_name elif is_ip_address and self.account.sip.tls_name: tls_name = self.account.sip.tls_name self.output.put('DNS lookup for %s\n' % uri) lookup.lookup_sip_proxy(uri, settings.sip.transport_list, tls_name=tls_name) 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', 'chat'] if self.enable_video else ['audio', 'chat'] 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, '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 as e: self.output.put('Failed to set input device to %s: %s\n' % (new_input_device, str(e))) else: 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, '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 as e: self.output.put('Failed to set output device to %s: %s\n' % (new_output_device, str(e))) else: 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, '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 as e: self.output.put('Failed to set alert device to %s: %s\n' % (new_output_device, str(e))) else: 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 if self.log_register: 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): if self.log_register: 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): if self.log_register: self.output.put('%s Registration ended.\n' % datetime.now().replace(microsecond=0)) def _NH_BonjourAccountRegistrationDidSucceed(self, notification): if self.log_register: self.output.put('%s Registered Bonjour contact %s\n' % (datetime.now().replace(microsecond=0), notification.data.name)) def _NH_BonjourAccountRegistrationDidFail(self, notification): pass #self.output.put('%s Failed to register Bonjour contact: %s\n' % (datetime.now().replace(microsecond=0), notification.data.reason)) def _NH_BonjourAccountRegistrationDidEnd(self, notification): if self.log_register: 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 Bonjour neighbour joined: %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 Bonjour neighbour joined: "%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 Bonjour neighbour updated: "%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("\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) if not self.enable_playback: self.play_file = None self.stop() self.end_cancel_thread() self._remove_lock() 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 in ('audio', 'chat'): break else: 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('Session from %s rejected due to incompatible media\n' % remote_identity) 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', 'chat'] if self.enable_video else ['audio', 'chat'] 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: if len(self.incoming_sessions) == 0: session.accept([stream for stream in session.proposed_streams if stream.type in accepted_types]) else: session.reject() return else: def auto_answer(): self.incoming_sessions.remove(session) if len(self.incoming_sessions) == 0: session.accept([stream for stream in session.proposed_streams if stream.type in accepted_types]) else: session.reject() 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.disable_ringtone: 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 and not self.disable_ringtone: self.wave_outbound_ringtone.start() def _NH_SIPSessionDidFail(self, notification): self.play_file = None self._remove_lock() session = notification.sender script = "%s/%s-%s-fail" % (self.scripts_dir, str(session.remote_identity.uri).split(":")[1], session.direction) if os.path.exists(script): self.output.put("Running script %s\n" % script) p = subprocess.Popen(script) code = notification.data.code if code and self.play_failure_code: if code == 486: text = 'User is busy' elif code == 404: text = 'User not found' elif code == 480: text = 'User not online' elif code == 408: text = 'Connection failed' else: text = str(notification.data.code) try: p = subprocess.Popen(['festival', '--tts', '--pipe'], stdin=subprocess.PIPE) except FileNotFoundError: pass else: p.communicate(text.encode()) if not self.disable_ringtone: 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: if not self.enable_playback: 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 and not self.enable_playback: self.stop() self.end_cancel_thread() else: self.output.put('Session failed: %s %s\n' % (notification.data.code, notification.data.failure_reason)) if session is self.outgoing_session and not self.enable_playback: 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 not self.disable_ringtone: 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 not self.disable_ringtone: 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)] script = "%s/%s-%s-start" % (self.scripts_dir, str(session.remote_identity.uri).split(":")[1], session.direction) if os.path.exists(script): self.output.put("Running script %s\n" % script) p = subprocess.Popen(script) def _NH_SIPSessionDidStart(self, notification): notification_center = NotificationCenter() session = notification.sender self.output.put('Session started with %d streams\n' % 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) for stream in notification.data.streams: if stream.type in ('audio', 'video'): self.output.put('%s stream established using %s codec at %sHz\n' % (stream.type.title(), stream.codec.capitalize(), stream.sample_rate)) if stream.ice_active: self.output.put('%s RTP endpoints %s:%d (ICE type %s) <-> %s:%d (ICE type %s)\n' % (stream.type.title(), 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: self.output.put('%s RTP endpoints %s:%d <-> %s:%d\n' % (stream.type.title(), stream.local_rtp_address, stream.local_rtp_port, stream.remote_rtp_address, stream.remote_rtp_port)) if stream.encryption.active: self.output.put('%s stream is encrypted using %s (%s)\n' % (stream.type.title(), stream.encryption.type, 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: if not self.disable_ringtone: 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 if self.auto_record: settings = SIPSimpleSettings() audio_stream = self.active_session.streams[0] 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)) 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)] if not self.disable_hanguptone: 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_TLSTransportHasChanged(self, notification): self.output.put('TLS verify server: %s\n' % notification.data.verify_server) self.output.put('TLS certificate: %s\n' % notification.data.certificate) if notification.data.certificate: try: contents = open(os.path.expanduser(notification.data.certificate), 'rb').read() certificate = X509Certificate(contents) # validate the certificate except (GNUTLSError, OSError) as e: self.output.put('Cannot read TLS certificate: %s\n' % str(e)) pass else: self.output.put('TLS Subject: %s\n' % certificate.subject) def _NH_SIPSessionDidEnd(self, notification): self.play_file = None self.end_cancel_thread() - self._remove_lock() + reactor.callLater(1, self._remove_lock) 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) try: self.started_sessions.remove(session) except ValueError: self.output.put('Session ended without starting') return 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 and not self.enable_playback: self.stop() self.end_cancel_thread() script = "%s/%s-%s-end" % (self.scripts_dir, str(session.remote_identity.uri).split(":")[1], session.direction) if os.path.exists(script): self.output.put("Running script %s\n" % script) p = subprocess.Popen(script) 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 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', 'chat'] if self.enable_video else ['audio', 'chat'] streams = [stream for stream in notification.data.proposed_streams if stream.type in accepted_types] if audio_streams: session.accept_proposal(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) self.output.put('DTMF code received: %s\n' % digit) wave_player = WavePlayer(self.voice_audio_mixer, ResourcePath(filename).normalized) notification_center = NotificationCenter() notification_center.add_observer(self, sender=wave_player) self.voice_audio_bridge.add(wave_player) wave_player.start() def _NH_SIPSessionGotConferenceInfo(self, notification): info = notification.data.conference_info self.output.put('Conference users: %s\n' % ", ".join(user.entity.split(":")[1] for user in info.users)) if info.conference_description.resources: for file in info.conference_description.resources.files: self.output.put('Conference shared file: %s (%s bytes)\n' % (file.name, file.size)) def _NH_RTPStreamICENegotiationStateDidChange(self, notification): data = notification.data if data.state == 'GATHERING': self.output.put("Gathering ICE Candidates...\n") elif data.state == 'NEGOTIATION_START': self.output.put("Connecting...\n") elif data.state == 'NEGOTIATING': self.output.put("Negotiating ICE...\n") elif data.state == 'GATHERING_COMPLETE': self.output.put("Gathering Complete\n") elif data.state == 'RUNNING': self.output.put("ICE Negotiation Succeeded\n") elif data.state == 'FAILED': self.output.put("ICE Negotiation Failed\n", True) def _NH_RTPStreamICENegotiationDidFail(self, notification): stream = notification.sender self.output.put('%s ICE negotiation failed: %s' % (notification.sender.type, notification.data.reason)) def _NH_MediaStreamDidStart(self, notification): stream = notification.sender if stream.type == 'audio' and self.play_file: script = "%s/scripts/%s-playback-start" % (self.playback_dir, self.target) if os.path.exists(script): self.output.put("Running script %s\n" % script) p = subprocess.Popen(script) else: self.output.put("Script does not exist %s\n" % script) self.playback_wave_player = WavePlayer(self.voice_audio_mixer, ResourcePath(self.play_file).normalized) notification_center = NotificationCenter() notification_center.add_observer(self, sender=self.playback_wave_player) self.output.put("Playback file %s\n" % self.play_file) stream.bridge.add(self.playback_wave_player) self.playback_wave_player.start() def _NH_RTPStreamICENegotiationDidSucceed(self, notification): stream = notification.sender self.output.put('%s ICE negotiation succeeded\n' % stream.type) self.output.put('%s RTP endpoints: %s:%d (%s) <-> %s:%d (%s)\n' % (stream.type, stream.local_rtp_address, stream.local_rtp_port, self.stream.local_rtp_candidate.type.lower(), stream.remote_rtp_address, stream.remote_rtp_port, stream.remote_rtp_candidate.type.lower())) if stream.local_rtp_candidate.type.lower() != 'relay' and stream.remote_rtp_candidate.type.lower() != 'relay': self.output.put('%s stream is peer to peer\n' % stream.type) else: self.output.put('%s stream is relayed by server\n' % stream.type) def _NH_RTPStreamDidChangeHoldState(self, notification): if notification.data.on_hold: if self.hold_tone and 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 self.hold_tone and 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_RTPStreamZRTPLog(self, notification): if 'Dropping packet' in notification.data.message: return #self.output.put('ZRTP message: %s\n' % notification.data.message) def _NH_RTPStreamDidEnableEncryption(self, notification): stream = notification.sender self.output.put("%s encryption activated using %s (%s)\n" % (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 sas = stream.encryption.zrtp.sas.decode() if isinstance(stream.encryption.zrtp.sas, bytes) else stream.encryption.zrtp.sas self.output.put("ZRTP secret is %s\n" % sas) self.output.put("ZRTP peer name is %s\n" % (peer_name or 'not set')) self.output.put("ZRTP peer is %s\n" % ('verified' if stream.encryption.zrtp.verified else 'not verified')) def _NH_ChatStreamGotMessage(self, notification): if not notification.data.message.content_type.startswith("text/"): return self.output.put(notification.data.message.content) def _NH_AudioStreamDidStartRecording(self, notification): self.output.put('Recording audio to %s\n' % notification.data.filename) def _NH_AudioStreamDidStopRecording(self, notification): self.output.put('Stopped recording audio to %s\n' % notification.data.filename) def _NH_WavePlayerDidFail(self, notification): notification_center = NotificationCenter() notification_center.remove_observer(self, sender=notification.sender) self.output.put('Failed to play %s: %s\n' % (notification.sender.filename, notification.data.error)) if notification.sender == self.playback_wave_player or self.play_file: self._playback_end() self.play_file = None if self.active_session: self.active_session.end() def _NH_WavePlayerDidEnd(self, notification): notification_center = NotificationCenter() notification_center.remove_observer(self, sender=notification.sender) if self.play_file: self._playback_end() self.play_file = None settings = SIPSimpleSettings() - api.sleep(0.3) rogertone = settings.sounds.roger_beep roger_tone = WavePlayer(SIPApplication.voice_audio_mixer, rogertone.path.normalized) SIPApplication.voice_audio_bridge.add(roger_tone) roger_tone.start() if notification.sender == self.playback_wave_player: self.active_session.end() def _playback_end(self): script = "%s/scripts/%s-playback-end" % (self.playback_dir, self.target) if os.path.exists(script): self.output.put("Running script %s\n" % script) p = subprocess.Popen(script) save_path = self.playback_dir + '/history/' + os.path.basename(self.play_file) self.output.put("Saved playback file to %s\n" % save_path) os.rename(self.play_file, save_path) - self._remove_lock() def _remove_lock(self): lock_file = "%s/playback.lock" % self.playback_dir if os.path.exists(lock_file): self.outgoing_session = None os.remove(lock_file) 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.decode()) 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('-d', '--playback-dir', type='string', dest='playback_dir', help='Directory with wav files to be played after calling. The destination SIP address is taken from the filename. After playback the call is hangup') parser.add_option('-p', '--enable_playback', action='store_true', dest='enable_playback', default=False, help='Enable polling playback directory for new wavs.') parser.add_option('-l', '--log-register', action='store_true', dest='log_register', default=False, help='Log result of registrations.') 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('-r', '--auto-record', action='store_true', dest='auto_record', default=False, help='Automatic recording of voice calls.') parser.add_option('-n', '--trace-notifications', action='store_true', dest='trace_notifications', default=False, help='Print all notifications (disabled by default).') parser.add_option('--disable-ringtone', action='store_true', dest='disable_ringtone', default=False, help='Disable ringtone.') parser.add_option('-g', '--disable-hanguptone', action='store_true', dest='disable_hanguptone', default=False, help='Disable hangup tone.') 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('-f', '--play-failure-code', action='store_true', dest='play_failure_code', default=False, help='Play failure code using festival.') 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-session3 b/sip-session3 index 52133f8..41e0a1a 100755 --- a/sip-session3 +++ b/sip-session3 @@ -1,3633 +1,3632 @@ #!/usr/bin/env python3 import hashlib import glob import os import re import signal import sys import urllib.request, urllib.parse, urllib.error import random import requests import uuid import pgpy import subprocess 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 pathlib import Path from threading import Event, Thread from time import sleep from application import log from application.system import makedirs from application.notification import IObserver, NotificationCenter from application.python.queue import EventQueue from application.python import Null from eventlib import api from gnutls.errors import GNUTLSError from gnutls.crypto import X509Certificate, X509PrivateKey from twisted.internet import reactor from zope.interface import implementer from otr import OTRTransport, OTRState, SMPStatus from otr.exceptions import IgnoreMessage, UnencryptedMessage, EncryptedMessageError, OTRError, OTRFinishedError 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.core import Route from sipsimple.core import CORE_REVISION, PJ_VERSION, PJ_SVN_REVISION from sipsimple import __version__ as version from sipsimple.lookup import DNSLookup from sipsimple.payloads.iscomposing import IsComposingMessage, IsComposingDocument 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, CPIMHeader, CPIMNamespace, SimplePayload, CPIMParserError, ChatIdentity, OTREncryption from sipsimple.payloads.imdn import IMDNDocument, DisplayNotification, DeliveryNotification 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, BonjourAccountExtension from sipclient.configuration.datatypes import ResourcePath from sipclient.configuration.settings import SIPSimpleSettingsExtension from sipclient.log import Logger from sipclient.system import IPAddressMonitor, copy_default_certificates 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', call_id=None): self.id = msg_id self.content = content self.timestamp = None self.content_type = content_type self.encrypted = False self.call_id = None class OTRInternalMessage(QueuedMessage): def __init__(self, content): super(OTRInternalMessage, self).__init__('OTR', content, 'text/plain') @implementer(IObserver) class MessageSession(object): def __init__(self, account, target, route=None): self.account = account self.target = target self.routes = None self.msg_id = 0 self.started = False self.msg_map = {} self.route = None self.ended = False self.route = route 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() if self.route: show_notice('Message session started with %s via %s' % (self.target, self.route)) else: 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 if self.route: self.routes = [self.route] show_notice('%s Session to %s will start via %s' % (datetime.now().replace(microsecond=0), self.remote_uri, self.routes[0])) if not self.started: self.message_queue.start() if not self.encryption.active: self.encryption.start() self.started = True 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}) tls_name = self.account.sip.tls_name or self.account.sip.outbound_proxy.host elif isinstance(self.account, Account) and self.account.sip.always_use_my_proxy: uri = SIPURI(host=self.account.id.domain) tls_name = self.account.sip.tls_name or self.account.id.domain else: uri = self.remote_uri tls_name = uri.host if self.account is not BonjourAccount(): if self.account.id.domain == uri.host.decode(): tls_name = self.account.sip.tls_name or self.account.id.domain elif "isfocus" in str(uri) and uri.host.decode().endswith(self.account.id.domain): tls_name = self.account.conference.tls_name or self.account.sip.tls_name or self.account.id.domain else: is_ip_address = re.match("^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$", uri.host.decode()) or ":" in uri.host.decode() if "isfocus" in str(uri) and self.account.conference.tls_name: tls_name = self.account.conference.tls_name elif is_ip_address and self.account.sip.tls_name: tls_name = self.account.sip.tls_name lookup.lookup_sip_proxy(uri, settings.sip.transport_list, tls_name=tls_name) 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): if not self.encryption.active: ui = UI() ui.status = 'Negotiating OTR encryption...' messageObject = OTRInternalMessage(data) self.send_message(messageObject) def _NH_DNSLookupDidSucceed(self, notification): self.notification_center.remove_observer(self, sender=notification.sender) self.routes = notification.data.result show_notice('%s Session to %s will start via %s' % (datetime.now().replace(microsecond=0), self.remote_uri, self.routes[0])) if not self.started: self.message_queue.start() if not self.encryption.active and self.account.sms.enable_otr: self.encryption.start() 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() if not self.routes: self.start() self.msg_id = self.msg_id + 1 if not isinstance(message, OTRInternalMessage): messageObject = QueuedMessage(self.msg_id , message, content_type) self.message_queue.put(messageObject) else: self.message_queue.put(message) return self.msg_id def send_message(self, message, content_type='text/plain'): if not self.routes: self.start() self.msg_id = self.msg_id + 1 if not isinstance(message, OTRInternalMessage): messageObject = QueuedMessage(self.msg_id , message, content_type) self.message_queue.put(messageObject) else: self.message_queue.put(message) return self.msg_id @run_in_green_thread def send_imdn_notification(self, imdn_id, imdn_timestamp, recipient, sender_identity, event): show_notice('Send %s notification for %s' % (event, imdn_id)) if event == 'delivered': notification = DeliveryNotification('delivered') elif event == 'displayed': notification = DisplayNotification(event) else: return content = IMDNDocument.create(message_id=imdn_id, datetime=imdn_timestamp, recipient_uri=recipient, notification=notification) self.send_message(content.decode(), content_type=IMDNDocument.content_type) @run_in_green_thread 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, str): charset = 'utf8' message.content = message.content.encode(charset) else: charset = None what_type = None if not isinstance(message, OTRInternalMessage): if message.content_type not in (IsComposingDocument.content_type, IMDNDocument.content_type): 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 except OTRFinishedError: show_notice('Encryption has finished, please resend the message again') return if self.encryption.active and not message.content.startswith(b'?OTR:'): show_notice('Encryption has been disabled by remote party, please resend the message again') self.encryption.stop() return None else: what_type = 'OTR' if message.timestamp is None: message.timestamp = ISOTimestamp.now() additional_cpim_headers = [] additional_sip_headers = [] if self.account.sms.use_cpim: if self.account.sms.enable_imdn and message.content_type != IsComposingDocument.content_type: ns = CPIMNamespace('urn:ietf:params:imdn', 'imdn') additional_cpim_headers = [CPIMHeader('Message-ID', ns, str(uuid.uuid4()))] if message.content_type != IsComposingDocument.content_type: # request IMDN additional_cpim_headers.append(CPIMHeader('Disposition-Notification', ns, 'positive-delivery, display')) content_type = 'message/cpim' payload = CPIMPayload(message.content, message.content_type, charset='utf-8', timestamp=message.timestamp, sender=ChatIdentity(self.account.uri, self.account.display_name), recipients=[ChatIdentity(self.remote_uri, None)], additional_headers=additional_cpim_headers) else: payload = SimplePayload(message.content, message.content_type, charset) content, content_type = payload.encode() from_uri = self.account.uri if self.account is BonjourAccount(): settings = SIPSimpleSettings() from_uri.parameters['instance_id'] = settings.instance_id message_request = Message(FromHeader(from_uri, self.account.display_name), ToHeader(self.target_uri), RouteHeader(self.route.uri), content_type, content, credentials=self.account.credentials, extra_headers=additional_sip_headers) self.notification_center.add_observer(self, sender=message_request) self.msg_map[str(message_request)] = message message_request.send(15) call_id = message_request._request.call_id.decode() message.call_id = call_id ui = UI() if message.id != 'OTR': if '?OTR:' in content: if message.content_type not in (IsComposingDocument.content_type, IMDNDocument.content_type): ui.status = 'Encrypted message sent to %s' % self.route.uri reactor.callLater(2, setattr, ui, 'status', None) message.encrypted = True else: if message.content_type not in (IsComposingDocument.content_type, IMDNDocument.content_type): ui.status = 'Message sent to %s' % (self.route.uri) reactor.callLater(3, setattr, ui, 'status', None) else: ui.status = 'OTR message %s sent' % call_id reactor.callLater(3, setattr, ui, 'status', None) def handle_incoming_message(self, from_header, content_type, data, call_id): self.msg_id = self.msg_id + 1 cpim_imdn_events = None imdn_timestamp = None imdn_id = None if content_type == 'message/cpim': try: cpim_message = CPIMPayload.decode(data) except CPIMParserError as e: show_notice('CPIM parse error: %s' % str(e)) return None else: content = cpim_message.content payload = cpim_message content_type = cpim_message.content_type if cpim_message.sender: identity = '%s@%s' % (cpim_message.sender.uri.user.decode(), cpim_message.sender.uri.host.decode()) if from_header.uri == cpim_message.sender.uri: if cpim_message.sender.display_name: identity = '"%s" <%s>' % (cpim_message.sender.display_name, identity) elif from_header.display_name: identity = '"%s" <%s>' % (from_header.display_name, identity) else: identity = from_header sender_identity = cpim_message.sender or data.from_header imdn_timestamp = cpim_message.timestamp for h in cpim_message.additional_headers: if h.name == "Message-ID": imdn_id = h.value if h.name == "Disposition-Notification": cpim_imdn_events = h.value if content_type == IMDNDocument.content_type: document = IMDNDocument.parse(content) imdn_message_id = document.message_id.value imdn_status = document.notification.status.__str__() ui = UI() msg = "Message %s was %s" % (imdn_message_id, imdn_status) ui.status = msg reactor.callLater(2, setattr, ui, 'status', None) show_notice(msg) return else: payload = SimplePayload.decode(data, content_type) from_header = FromHeader.new(from_header) identity = '%s@%s' % (from_header.uri.user.decode(), from_header.uri.host.decode()) content = payload.content content_type = payload.content_type if cpim_imdn_events and imdn_timestamp: if self.account.sms.enable_imdn: if 'delivery' in cpim_imdn_events: self.send_imdn_notification(imdn_id, imdn_timestamp, identity, sender_identity, 'delivered') if 'display' in cpim_imdn_events: self.send_imdn_notification(imdn_id, imdn_timestamp, identity, sender_identity, 'displayed') # if content_type not in (IsComposingDocument.content_type, IMDNDocument.content_type): try: content = self.encryption.otr_session.handle_input(content, content_type) except IgnoreMessage: show_notice('OTR message %s received' % call_id) 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, OTRFinishedError) 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[str(notification.sender)] except KeyError: message = None else: del(self.msg_map[str(notification.sender)]) if not message: return if message.id in (None, 'OTR'): return if message.content_type in (IsComposingDocument.content_type, IMDNDocument.content_type): return ui = UI() if notification.data.code == 202: ui.status = 'Message %s to %s will be delivered later by the server' % (message.id, self.remote_uri) reactor.callLater(4, setattr, ui, 'status', None) elif notification.data.code == 200: ui.status = 'Message %s received by %s' % (message.id, self.remote_uri) reactor.callLater(4, setattr, ui, 'status', None) else: ui.status = 'Message %s received by %s (code %d)' % (message.id, self.remote_uri, notification.data.code) reactor.callLater(4, setattr, ui, 'status', None) def _NH_SIPMessageDidFail(self, notification): self.notification_center.remove_observer(self, sender=notification.sender) try: message = self.msg_map[str(notification.sender)] except KeyError: message = None else: del(self.msg_map[str(notification.sender)]) if not message: return if message.id in (None, 'OTR'): return if hasattr(notification.data, 'headers'): server = notification.data.headers.get('Server', Null).body client = notification.data.headers.get('Client', Null).body else: server = 'local' client = 'local' 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.decode(), 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.decode() if isinstance(notification.data.reason, bytes) else notification.data.reason, notification.data.code)) OTRTransport.register(MessageSession) @implementer(IObserver) class OutgoingCallInitializer(object): def __init__(self, account, target, audio=False, chat=False, video=False, play_file=None): self.account = account self.target = target self.streams = [] self.play_file = play_file self.playback_wave_player = None self.play_file = play_file application = SIPSessionApplication() self.playback_dir = application.playback_dir self.remote_identity = None if video: self.streams.append(MediaStreamRegistry.VideoStream()) if audio: audio_stream = MediaStreamRegistry.AudioStream() self.streams.append(audio_stream) if self.play_file: notification_center = NotificationCenter() notification_center.add_observer(self, sender=audio_stream) if chat: self.streams.append(MediaStreamRegistry.ChatStream()) self.wave_ringtone = None def start(self): if self.play_file: lock_file = "%s/playback.lock" % self.playback_dir Path(lock_file).touch() show_notice("Lock file %s created" % lock_file) 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.decode() 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}) tls_name = self.account.sip.tls_name or self.account.sip.outbound_proxy.host elif isinstance(self.account, Account) and self.account.sip.always_use_my_proxy: uri = SIPURI(host=self.account.id.domain) tls_name = self.account.sip.tls_name or self.account.id.domain else: uri = self.target tls_name = uri.host if self.account is not BonjourAccount(): if self.account.id.domain == uri.host.decode(): tls_name = self.account.sip.tls_name or self.account.id.domain elif "isfocus" in str(uri) and uri.host.decode().endswith(self.account.id.domain): tls_name = self.account.conference.tls_name or self.account.sip.tls_name or self.account.id.domain else: is_ip_address = re.match("^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$", uri.host.decode()) or ":" in uri.host.decode() if "isfocus" in str(uri) and self.account.conference.tls_name: tls_name = self.account.conference.tls_name elif is_ip_address and self.account.sip.tls_name: tls_name = self.account.sip.tls_name show_notice('DNS lookup for %s' % uri) lookup.lookup_sip_proxy(uri, settings.sip.transport_list, tls_name=tls_name) 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) self._remove_lock() 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) self.remote_identity = remote_identity.split(":")[1] 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 and not self.play_file: 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_SIPSessionDidEnd(self, notification): - self._remove_lock() + reactor.callLater(1, self._remove_lock) 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) for stream in notification.data.streams: if stream.type in ('audio', 'video'): show_notice('%s session established using %s codec at %sHz' % (stream.type.title(), stream.codec.capitalize(), stream.sample_rate)) if stream.ice_active: show_notice('%s RTP endpoints %s:%d (ICE type %s) <-> %s:%d (ICE type %s)' % (stream.type.title(), 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('%s RTP endpoints %s:%d <-> %s:%d' % (stream.type.title(), 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_MediaStreamDidStart(self, notification): stream = notification.sender if stream.type == 'audio' and self.play_file: script = "%s/scripts/%s-playback-start" % (self.playback_dir, self.remote_identity) if os.path.exists(script): show_notice("Running script %s" % script) p = subprocess.Popen(script) else: show_notice("Script does not exist %s" % script) file = ResourcePath(self.play_file).normalized show_notice("Playback file %s\n" % file) self.playback_wave_player = WavePlayer(SIPApplication.voice_audio_mixer, file) notification_center = NotificationCenter() notification_center.add_observer(self, sender=self.playback_wave_player) stream.bridge.add(self.playback_wave_player) #SIPApplication.voice_audio_bridge.add(self.playback_wave_player) self.playback_wave_player.start() def _NH_WavePlayerDidEnd(self, notification): notification_center = NotificationCenter() notification_center.remove_observer(self, sender=notification.sender) show_notice("Playback finished") if self.play_file: self._playback_end() self.play_file = None settings = SIPSimpleSettings() api.sleep(0.3) rogertone = settings.sounds.roger_beep roger_tone = WavePlayer(SIPApplication.voice_audio_mixer, rogertone.path.normalized) show_notice("Play roger beep") SIPApplication.voice_audio_bridge.add(roger_tone) roger_tone.start() if notification.sender == self.playback_wave_player: application = SIPSessionApplication() if application.outgoing_session: application.outgoing_session.end() def _NH_WavePlayerDidFail(self, notification): notification_center = NotificationCenter() notification_center.remove_observer(self, sender=notification.sender) show_notice('Playback %s failed: %s' % (notification.sender.filename, notification.data.error)) if notification.sender == self.playback_wave_player or self.play_file: self._playback_end() self.play_file = None application = SIPSessionApplication() if application.outgoing_session: application.outgoing_session.end() def _NH_SIPSessionDidFail(self, notification): self._remove_lock() + notification_center = NotificationCenter() session = notification.sender notification_center.remove_observer(self, sender=session) self.play_file = None if self.playback_wave_player: SIPApplication.voice_audio_bridge.remove(self.playback_wave_player) 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: pass #show_notice('SIP session failed: %s' % notification.data.failure_reason) def _playback_end(self): script = "%s/scripts/%s-playback-end" % (self.playback_dir, self.remote_identity) if os.path.exists(script): show_notice("Running script %s" % script) p = subprocess.Popen(script) save_path = self.playback_dir + '/history/' + os.path.basename(self.play_file) show_notice("Saved playback file to %s" % save_path) os.rename(self.play_file, save_path) - self._remove_lock() def _remove_lock(self): lock_file = "%s/playback.lock" % self.playback_dir if os.path.exists(lock_file): os.remove(lock_file) @implementer(IObserver) class IncomingCallInitializer(object): 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 and 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() application = SIPSessionApplication() notification_center.remove_observer(self, sender=notification.sender) answer = notification.data.answer self.question = None if answer == 'a': if application.received_private_key: show_notice('Please enter password to decrypt the key:') return self.session.accept(self.session.proposed_streams) ui.status = 'Accepting...' elif answer == 'r': if application.received_private_key: application.received_private_key = None return self.session.reject() ui.status = 'Rejected' reactor.callLater(3, setattr, ui, 'status', None) elif answer == 'b': self.session.reject(486) ui.status = 'Sent Busy Here' reactor.callLater(3, setattr, ui, 'status', None) 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 in ('audio', 'video'): show_notice('%s stream using %s codec at %sHz' % (stream.type.title(), stream.codec.capitalize(), stream.sample_rate)) if stream.ice_active: show_notice('%s RTP endpoints %s:%d (ICE type %s) <-> %s:%d (ICE type %s)\n' % (stream.type.title(), 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('%s RTP endpoints %s:%d <-> %s:%d\n' % (stream.type.title(), stream.local_rtp_address, stream.local_rtp_port, stream.remote_rtp_address, stream.remote_rtp_port)) if stream.encryption.active: show_notice('%s RTP stream is encrypted using %s (%s)\n' % (stream.type.title(), 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): 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.discard_observer(self, sender=self.session) @implementer(IObserver) class IncomingProposalHandler(object): 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.discard_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): def __init__(self, account, target, filepath): self.account = account self.target = target self.filepath = filepath 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.decode() 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}) tls_name = self.account.sip.tls_name or self.account.sip.outbound_proxy.host elif isinstance(self.account, Account) and self.account.sip.always_use_my_proxy: uri = SIPURI(host=self.account.id.domain) tls_name = self.account.sip.tls_name or self.account.id.domain else: uri = self.target tls_name = uri.host if self.account is not BonjourAccount(): if self.account.id.domain == uri.host.decode(): tls_name = self.account.sip.tls_name or self.account.id.domain elif "isfocus" in str(uri) and uri.host.decode().endswith(self.account.id.domain): tls_name = self.account.conference.tls_name or self.account.sip.tls_name or self.account.id.domain else: is_ip_address = re.match("^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$", uri.host.decode()) or ":" in uri.host.decode() if "isfocus" in str(uri) and self.account.conference.tls_name: tls_name = self.account.conference.tls_name elif is_ip_address and self.account.sip.tls_name: tls_name = self.account.sip.tls_name lookup.lookup_sip_proxy(uri, settings.sip.transport_list, tls_name=tls_name) 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 succeeded' % self.filepath) else: show_notice('File transfer of %s failed: %s' % (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) routes = notification.data.result self.session.connect(ToHeader(self.target), routes=routes, 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_FileTransferHandlerHashProgress(self, notification): progress = int(notification.data.processed*100/notification.data.total) if progress % 10 == 0: ui = UI() if progress < 100: ui.status = 'Calculating checksum for %s: %s%%' % (self.filepath, progress) else: ui.status = 'Sending file transfer request...' 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): 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 and 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 saved to %s' % self.filename) else: show_notice('File transfer of %s failed: %s' % (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 = 'Rejected' reactor.callLater(3, setattr, ui, 'status', None) 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.filepath = None self.enable_video = False 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.received_private_key = None self.private_keys = {} self.public_keys = {} 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 self.must_exit = False self.last_failure_reason = None self.enable_playback = False def start(self, target, options, filepath=None): notification_center = NotificationCenter() ui = UI() history_file = os.path.join(config_directory, 'input.history') self.keys_path = os.path.join(config_directory, 'keys') makedirs(self.keys_path) self.enable_playback = options.enable_playback if options.playback_dir: self.playback_dir = options.playback_dir else: self.playback_dir = "%s/spool/playback" % config_directory makedirs(self.playback_dir) if self.enable_playback: lock_file = "%s/playback.lock" % self.playback_dir if os.path.exists(lock_file): os.remove(lock_file) self.playback_queue = EventQueue(self._handle_outgoing_playback) active_playback_dir = self.playback_dir + '/active' makedirs(active_playback_dir) history_playback_dir = self.playback_dir + '/history' makedirs(history_playback_dir) scripts_playback_dir = self.playback_dir + '/scripts' makedirs(scripts_playback_dir) ui.input.add_history(history_file) self.options = options self.target = target self.filepath = filepath 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') notification_center.add_observer(self, name='SIPSessionDidEnd') notification_center.add_observer(self, name='SIPSessionDidFail') notification_center.add_observer(self, name='MediaStreamDidNotInitialize') notification_center.add_observer(self, name='SIPSessionGotConferenceInfo') notification_center.add_observer(self, name='SIPSessionTransferGotProgress') notification_center.add_observer(self, name='SIPSessionTransferDidFail') notification_center.add_observer(self, name='SIPSessionTransferNewIncoming') notification_center.add_observer(self, name='RTPStreamICENegotiationStateDidChange') notification_center.add_observer(self, name='RTPStreamICENegotiationDidFail') notification_center.add_observer(self, name='RTPStreamICENegotiationDidSucceed') notification_center.add_observer(self, name='ChatStreamOTREncryptionStateChanged') notification_center.add_observer(self, name='TLSTransportHasChanged') notification_center.add_observer(self, name='MediaStreamDidFail') notification_center.add_observer(self, name='AudioDevicesDidChange') notification_center.add_observer(self, name='DefaultAudioDeviceDidChange') 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', 'c': 'camera', '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(BonjourAccountExtension) SIPSimpleSettings.register_extension(SIPSimpleSettingsExtension) try: SIPApplication.start(self, FileStorage(options.config_directory or config_directory)) 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() else: show_notice("SDK version %s, core version %s, PJSIP version %s (%s)" % (version, CORE_REVISION, PJ_VERSION.decode(), PJ_SVN_REVISION)) # 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 or settings.logs.trace_sip settings.logs.trace_msrp = self.options.trace_msrp or settings.logs.trace_msrp settings.logs.trace_pjsip = self.options.trace_pjsip or settings.logs.trace_pjsip settings.logs.trace_notifications = self.options.trace_notifications or settings.logs.trace_notifications 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_TLSTransportHasChanged(self, notification): show_notice('TLS verify server: %s\n' % notification.data.verify_server) show_notice('TLS certificate: %s\n' % notification.data.certificate) if notification.data.certificate: try: contents = open(os.path.expanduser(notification.data.certificate), 'rb').read() certificate = X509Certificate(contents) # validate the certificate except (GNUTLSError, OSError) as e: show_notice('Cannot read TLS certificate: %s\n' % str(e)) pass else: show_notice('TLS Subject: %s\n' % certificate.subject) def _NH_MediaStreamDidFail(self, notification): reason = notification.data.reason if hasattr(notification.data, 'transport'): reason = reason + " transport=%s" % notification.data.transport if hasattr(notification.data, 'credentials'): reason = reason + " verify_server=%s" % notification.data.credentials.verify_peer show_notice('%s media stream failed: %s\n' % (notification.sender.type, reason)) def _NH_SIPApplicationDidStart(self, notification): settings = SIPSimpleSettings() engine = Engine() show_notice('Available audio codecs: %s\n' % ', '.join([codec.decode() for codec in engine._ua.available_codecs])) show_notice('Available video codecs: %s\n' % ', '.join([codec.decode() for codec in engine._ua.available_video_codecs])) self.ip_address_monitor.start() if self.enable_playback: show_notice("Polling %s for wav files" % self.playback_dir) self.playback_queue.start() reactor.callLater(3, self.poll_playback_directory) # 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 settings.tls.ca_list is None: show_notice('Initializing default TLS certificates and settings') copy_default_certificates() settings.tls.ca_list = os.path.join(config_directory, 'tls/ca.crt') settings.tls.certificate = os.path.join(config_directory, 'tls/default.crt') settings.tls.verify_server = True settings.save() if self.target is not None: if self.filepath: self.send_outgoing_file(self.target, self.filepath) self.must_exit = True else: call_initializer = OutgoingCallInitializer(self.account, self.target, audio=True, chat=True, video=True) call_initializer.start() def poll_playback_directory(self): if self.outgoing_session: reactor.callLater(0.1, self.poll_playback_directory) return files = list(filter(os.path.isfile, glob.glob(self.playback_dir + "/*.wav"))) files.sort(key=lambda x: os.path.getmtime(x)) for file in files: if len(file.split('@')) == 2: active_playback_dir = self.playback_dir + '/active' basename = os.path.basename(file) show_notice("Audio recording detected %s" % basename) filename = '%s/%s-%s' % (active_playback_dir, datetime.now().strftime("%Y%m%d-%H%M%S"), basename) os.replace(file, filename) play_object = {'target': os.path.splitext(basename)[0], 'filename': filename} self.playback_queue.put(play_object) reactor.callLater(0.1, self.poll_playback_directory) def _handle_outgoing_playback(self, play_object): self.play_file = play_object['filename'] call_initializer = OutgoingCallInitializer(self.account, play_object['target'], audio=True, play_file=self.play_file) 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_RTPStreamICENegotiationStateDidChange(self, notification): data = notification.data stype = notification.sender.type.title() if data.state == 'GATHERING': show_notice("Gathering %s ICE Candidates..." % stype) elif data.state == 'NEGOTIATION_START': show_notice("Probing remote %s ICE candidates..." % stype) elif data.state == 'NEGOTIATING': show_notice("%s ICE negotiating in progress..." % stype) elif data.state == 'GATHERING_COMPLETE': show_notice("%s ICE candidates gathering complete" % stype) elif data.state == 'RUNNING': show_notice("%s ICE negotiation succeeded" % stype) elif data.state == 'FAILED': show_notice("%s ICE negotiation failed" % stype, True) def _NH_RTPStreamICENegotiationDidFail(self, notification): stream = notification.sender show_notice('%s ICE negotiation failed: %s' % (notification.sender.type, notification.data.reason)) def _NH_RTPStreamICENegotiationDidSucceed(self, notification): stream = notification.sender show_notice('%s ICE negotiation succeeded' % stream.type) show_notice('%s RTP endpoints: %s:%d (%s) <-> %s:%d (%s)' % (stream.type, stream.local_rtp_address, stream.local_rtp_port, self.stream.local_rtp_candidate.type.lower(), stream.remote_rtp_address, stream.remote_rtp_port, stream.remote_rtp_candidate.type.lower())) if stream.local_rtp_candidate.type.lower() != 'relay' and stream.remote_rtp_candidate.type.lower() != 'relay': show_notice('%s stream is peer to peer' % stream.type) else: show_notice('%s stream is relayed by server' % stream.type) def handle_private_keys(self, password, key): account = key['account'] private_key_encrypted = key['private_key'] public_key = key['public_key'] try: pgpMessage = pgpy.PGPMessage.from_blob(private_key_encrypted.encode()) decryptedKeyPair = pgpMessage.decrypt(password) private_key = decryptedKeyPair.message private_key_path = "%s/%s.privkey" % (self.keys_path, account.id) fd = open(private_key_path, "wb+") fd.write(private_key.encode()) fd.close() show_notice("Private key saved to %s" % private_key_path) public_key_path = "%s/%s.pubkey" % (self.keys_path, account.id) fd = open(public_key_path, "wb+") fd.write(public_key.encode()) fd.close() show_notice("Public key saved to %s" % public_key_path) account.sms.private_key = private_key_path account.sms.public_key = public_key_path account.sms.public_key_checksum = hashlib.sha1(public_key.encode()).hexdigest() account.save() except Exception as e: show_notice("Import private key failed: %s" % str(e)) def get_public_key(self, uri): public_key = None try: public_key = self.public_keys[uri] except KeyError: public_key_path = "%s/%s.pubkey" % (self.keys_path, uri) if os.path.exists(public_key_path): try: public_key, _ = pgpy.PGPKey.from_file(public_key_path) except Exception as e: pass else: self.public_keys[uri] = public_key show_notice('PGP public key imported from %s' % public_key_path) return public_key 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.received_private_key: self.handle_private_keys(message_text, self.received_private_key) self.received_private_key = None return 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, account=None, route=None): account = account or self.account try: message_session = next((session for session in self.message_sessions if session.target == recipient and account == session.account)) except StopIteration: message_session = MessageSession(account, recipient, route) self.message_sessions.add(message_session) return message_session def _NH_SIPSessionTransferNewIncoming(self, notification): target = "%s@%s" % (notification.data.transfer_destination.user.decode(), notification.data.transfer_destination.host.decode()) show_notice('Session transfer requested by remote party to: %s' % target) self.question = Question("Call transfer request to %s, do you want to accept? (a)ccept/(r)eject" % target, 'arbi', bold=True) notification_center = NotificationCenter() notification_center.add_observer(self, sender=self.question) ui = UI() ui.add_question(self.question) 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': if self.active_session: ui.status = 'Accepting transfer...' self.active_session.accept_transfer() elif answer == 'r': if self.active_session: ui.status = 'Rejecting transfer...' self.active_session.accept_transfer() 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 notification.data.error == self.last_failure_reason: return self.last_failure_reason = notification.data.error 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): show_notice('%s Registered Bonjour contact %s' % (datetime.now().replace(microsecond=0), notification.data.name)) def _NH_BonjourAccountRegistrationDidFail(self, notification): if notification.data.reason == self.last_failure_reason: return self.last_failure_reason = notification.data.reason #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: instance_id = data.from_header.uri.parameters.get('instance_id', None).split(":")[2] except (IndexError, AttributeError): instance_id = None 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', IsComposingDocument.content_type, 'text/pgp-private-key', 'text/pgp-public-key'): return account = AccountManager().find_account(data.to_header.uri) if not account: return is_composing = False private_key = None try: private_key = self.private_keys[account.id] except KeyError: try: private_key, _ = pgpy.PGPKey.from_file(account.sms.private_key) except Exception as e: pass else: show_notice('PGP private key imported from %s' % account.sms.private_key) self.private_keys[account.id] = private_key from_header = FromHeader.new(data.from_header) identity = '%s@%s' % (from_header.uri.user.decode(), from_header.uri.host.decode()) remote_uri = identity if isinstance(account, BonjourAccount): via = data.headers.get('Via', Null)[0] route = Route(address=via.host, port=via.port, transport=via.transport.lower()) else: route = None if content_type not in ('text/pgp-private-key', 'text/pgp-public-key'): message_session = self.message_session(identity, account=account, route=route) parsed_payload = message_session.handle_incoming_message(data.from_header, content_type, data.body, call_id) if parsed_payload is None: return (content_type, content, identity, msg_id, encrypted) = parsed_payload else: content = data.body encrypted = False message_session = None if content_type not in ('text/pgp-private-key', 'text/pgp-public-key', IsComposingDocument.content_type): self.message_session_to = identity if content_type == IsComposingDocument.content_type: # 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 from_header.display_name: identity = '"%s" <%s>' % (from_header.display_name, identity) content = content.decode() if isinstance(content, bytes) else content content = content.strip() if is_composing: ui = UI() ui.status = "%s is composing a message" % identity reactor.callLater(4, setattr, ui, 'status', None) else: if encrypted: show_notice("%s %s wrote: %s (%s encrypted)" % (datetime.now().replace(microsecond=0), identity, content, msg_id)) else: if content_type == 'text/pgp-public-key': if content.startswith('-----BEGIN PGP PUBLIC KEY BLOCK-----') and content.endswith('-----END PGP PUBLIC KEY BLOCK-----'): public_key_path = "%s/%s.pubkey" % (self.keys_path, instance_id or remote_uri) fd = open(public_key_path, "wb+") fd.write(content.encode()) fd.close() self.get_public_key(instance_id or remote_uri) show_notice("Public key saved to %s" % public_key_path) return if content_type == 'text/pgp-private-key': public_key = '' private_key_encrypted = '' start_public = False start_private = False for l in content.split("\n"): if l == "-----BEGIN PGP PUBLIC KEY BLOCK-----": start_public = True if l == "-----BEGIN PGP MESSAGE-----": start_public = False start_private = True if start_public: public_key = public_key + l + "\n" if start_private: private_key_encrypted = private_key_encrypted + l + "\n" if private_key_encrypted and public_key: self.received_private_key = {'account': account, 'private_key': private_key_encrypted, 'public_key': public_key} self.question = Question("Private key received for account %s, do you want to import it? (a)ccept/(r)eject" % (account.id), 'ar', bold=True) notification_center = NotificationCenter() notification_center.add_observer(self, sender=self.question) ui = UI() ui.add_question(self.question) else: if content.startswith('-----BEGIN PGP MESSAGE-----') and content.endswith('-----END PGP MESSAGE-----') and not private_key: show_notice("%s %s wrote: %s (%s)" % (datetime.now().replace(microsecond=0), identity, 'Received a PGP message for which we have no private key', msg_id)) else: try: pgpMessage = pgpy.PGPMessage.from_blob(content.encode()) decrypted_message = private_key.decrypt(pgpMessage) except (pgpy.errors.PGPDecryptionError, pgpy.errors.PGPError) as e: show_notice("%s %s wrote: %s (%s)" % (datetime.now().replace(microsecond=0), identity, 'PGP decrypt error', msg_id)) return except ValueError as e: show_notice("%s %s wrote: %s (%s)" % (datetime.now().replace(microsecond=0), identity, content, msg_id)) else: content = bytes(decrypted_message.message, 'latin1').decode() show_notice("%s %s wrote: %s (%s)" % (datetime.now().replace(microsecond=0), identity, content, msg_id)) if not encrypted and message_session and message_session.encryption.active and content_type != IsComposingDocument.content_type: show_notice("%s %s stopped the encryption" % (datetime.now().replace(microsecond=0), identity)) message_session.encryption.stop() if content.startswith('?OTR:') and message_session: show_notice("%s %s encrypted message could not be read" % (datetime.now().replace(microsecond=0), identity)) message_session.encryption.stop() 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: for stream in notification.data.streams: if stream.type in ('audio', 'chat'): break else: 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('Session from %s rejected due to incompatible media' % remote_identity) session.reject(415) return 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.discard_observer(self, sender=notification.sender) if self.must_exit: self.stop() 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)] def _NH_SIPSessionDidEnd(self, notification): notification_center = NotificationCenter() session = notification.sender notification_center.discard_observer(self, sender=session) for stream in session.streams or session.proposed_streams or []: notification_center.discard_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 if self.must_exit: self.stop() 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' and stream.type == 'audio': peer_name = stream.encryption.zrtp.peer_name if stream.encryption.zrtp.peer_name else None sas = stream.encryption.zrtp.sas.decode() if isinstance(stream.encryption.zrtp.sas, bytes) else stream.encryption.zrtp.sas show_notice("ZRTP secret is %s" % 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.capitalize(), 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.encode()) self.smp_verifified_using_zrtp = True else: show_notice("OTR verification requested by remote, reply using /otr_answer") show_notice('Question: %s' % data.question.decode()) 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.encode(), 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 remote_fingerprint = stream.encryption.peer_fingerprint 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) ui = UI() if stream.encryption.verified: ui.status = "OTR remote fingerprint has been verified" else: ui.status = "OTR remote fingerprint has not yet been verified" reactor.callLater(15, setattr, ui, 'status', None) 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_MediaStreamDidNotInitialize(self, notification): if self.must_exit: self.stop() def _NH_SIPSessionTransferGotProgress(self, notification): show_notice('Session transfer progress: %s (%s)' % (notification.data.reason, notification.data.code)) def _NH_SIPSessionTransferDidFail(self, notification): show_notice('Session transfer failed: %s (%s)' % (notification.data.reason, notification.data.code)) def _NH_SIPSessionGotConferenceInfo(self, notification): info = notification.data.conference_info show_notice('Conference users: %s' % ", ".join(user.entity.split(":")[1] for user in info.users)) if info.conference_description.resources: for file in info.conference_description.resources.files: show_notice('Conference shared file: %s (%s bytes)' % (file.name, file.size)) def _NH_RTPStreamICENegotiationDidSucceed(self, notification): show_notice(" ") show_notice("ICE negotiation succeeded in %s seconds" % 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.decode()) # command handlers # def _CH_call(self, target, video_option=None): if video_option and video_option != '+video': 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, video=video_option=='+video') 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_m(self, target=None): self._CH_message(target) def _CH_message(self, target=None): if not target: show_notice('Usage: /message user@domain') route = None if isinstance(self.account, BonjourAccount): try: idx = int(target) neighbour = list(self.neighbours.values())[idx-1] show_notice('Using Bonjour neighbour "%s (%s)" <%s>' % (neighbour.display_name, neighbour.host, neighbour.uri)) target = '%s@%s' % (neighbour.uri.user, neighbour.uri.host) route = Route(address=neighbour.uri.host, port=neighbour.uri.port, transport=neighbour.uri.transport) except (IndexError, TypeError): i = 1 lines = ['Bonjour neighbours:'] for key, neighbour in self.neighbours.items(): lines.append(' "%s (%s)" <%s> (%i)' % (neighbour.display_name, neighbour.host, neighbour.uri, i)) i = i + 1 show_notice(lines) try: SIPURI.parse('sip:%s' % target) if self.message_session_to != target: message_session = self.message_session(target, route=route) 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 send_outgoing_file(self, target, filepath): if '~' in filepath: filepath = os.path.expanduser(filepath) filepath = os.path.abspath(filepath) if (os.path.exists(filepath)): transfer_handler = OutgoingTransferHandler(self.account, target, filepath) transfer_handler.start() else: show_notice('File %s does not exist' % filepath) def _CH_send(self, target, filepath): self.send_outgoing_file(target, filepath) 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_n(self): self._CH_neighbours() def _CH_neighbours(self): lines = ['Bonjour neighbours:'] 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_camera(self, device=None): engine = Engine() settings = SIPSimpleSettings() video_devices = [None] + sorted(engine.video_devices) if device is None: if settings.video.device in video_devices: old_device = settings.video.device else: old_device = None new_device = video_devices[(video_devices.index(old_device)+1) % len(video_devices)] settings.video.device = new_device settings.save() show_notice('Video device changed to %s' % new_device) else: device = device.decode(sys.getfilesystemencoding()) if device == 'None': device = None elif device not in video_devices: show_notice('Unknown video device %s. Type /devices to see a list of available devices' % device) return try: settings.video.device = new_device except SIPCoreError as e: show_notice('Failed to set video device to %s: %s' % (device, str(e))) else: show_notice('Video device changed to %s' % new_device) def _CH_input(self, device=None): engine = Engine() settings = SIPSimpleSettings() 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 as e: show_notice('Failed to set input device to %s: %s' % (new_input_device, str(e))) else: 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) settings.audio.input_device = new_input_device settings.save() else: device = device.decode(sys.getfilesystemencoding()) 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 as e: show_notice('Failed to set input device to %s: %s' % (device, str(e))) else: 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) settings.audio.input_device = new_input_device settings.save() def _CH_output(self, device=None): engine = Engine() settings = SIPSimpleSettings() 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 as e: show_notice('Failed to set output device to %s: %s' % (new_output_device, str(e))) else: 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) settings.audio.output_device = new_output_device settings.save() else: device = device.decode(sys.getfilesystemencoding()) 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 as e: show_notice('Failed to set output device to %s: %s' % (device, str(e))) else: 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) settings.audio.output_device = device settings.save() def _CH_alert(self, device=None): engine = Engine() settings = SIPSimpleSettings() 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 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 == '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) settings.audio.alert_device = new_output_device settings.save() else: device = device.decode(sys.getfilesystemencoding()) 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 as e: show_notice('Failed to set alert device to %s: %s' % (device, str(e))) else: 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) settings.audio.alert_device = device settings.save() def _CH_devices(self): engine = Engine() settings = SIPSimpleSettings() 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) if engine.video_devices: show_notice('Available cameras: %s' % ', '.join(sorted(engine.video_devices))) show_notice('Selected camera: %s' % settings.video.device) else: if self.enable_video: show_notice('No camera present, video is disabled') self.enable_video = 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 = 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.encode()) else: 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.encode()) def _CH_otr_question(self, *args): question = " ".join(args) if args else None if self.smp_secret is None: show_notice("OTR question can be asked only after secret has been set with /otr_secret") return 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 for message: %s" % question) message_session.encryption.verified = False message_session.encryption.smp_verify(self.smp_secret.encode(), question=question.encode()) 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.encode(), question=question.encode()) 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 show_notice("ZRTP peer is now %s" % ('verified' if audio_stream.encryption.zrtp.verified else 'not 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 show_notice("ZRTP peer name is now %s" % audio_stream.encryption.zrtp.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 = 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 if '@' not in uri: uri = '%s@%s' % (uri, self.account.id.domain) 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 = 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 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]} [+video]: call the specified user using audio and optionally video') lines.append(' /chat {user[@domain]} [+audio]: call the specified user using chat and possibly audio') lines.append(' /[message | m] {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 | n]: 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(' /camera [device]: change camera device (ctrl-x c)') 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): return 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] [filename]' 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('--playback-dir', type='string', dest='playback_dir', help='Directory with wav files to be played after calling. The destination SIP address is taken from the filename. After playback the call is hangup') 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('-p', '--enable_playback', action='store_true', dest='enable_playback', default=False, help='Enable polling playback directory for new wavs.') 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 filepath = args[1] if len(args) == 2 else None application = SIPSessionApplication() application.start(target, options, filepath) signal.signal(signal.SIGINT, signal.SIG_DFL) application.stopped_event.wait() sleep(0.1) diff --git a/va-recorder.py b/va-recorder.py index 3f3e87d..18981d9 100755 --- a/va-recorder.py +++ b/va-recorder.py @@ -1,349 +1,355 @@ #!/usr/bin/env python3 import pyaudio import wave from ctypes import * from optparse import OptionParser from pathlib import Path import math -import psutil import struct import wave import time import os import sys from datetime import datetime import functools print = functools.partial(print, flush=True) # remove useless alsa logging # From alsa-lib Git 3fd4ab9be0db7c7430ebd258f2717a976381715d # $ grep -rn snd_lib_error_handler_t # include/error.h:59:typedef void (*snd_lib_error_handler_t)(const char *file, int line, const char *function, int err, const char *fmt, ...) /* __attribute__ ((format (printf, 5, 6))) */; # Define our error handler type ERROR_HANDLER_FUNC = CFUNCTYPE(None, c_char_p, c_int, c_char_p, c_int, c_char_p) def py_error_handler(filename, line, function, err, fmt): pass #print('messages are yummy') c_error_handler = ERROR_HANDLER_FUNC(py_error_handler) asound = cdll.LoadLibrary('libasound.so') # Set error handler asound.snd_lib_error_set_handler(c_error_handler) # import sounddevice as sd #devices = sd.query_devices() #print(devices) f_name_directory = '%s/.sipclient/spool/playback' % Path.home() lock_file = '%s/.sipclient/spool/playback/playback.lock' % Path.home() -def checkIfProcessRunning(processName): - ''' - Check if there is any running process that contains the given name processName. - ''' - #Iterate over the all the running process - for proc in psutil.process_iter(): - try: - # Check if process name contains the given name string. - if processName.lower() in proc.name().lower(): - return True - except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess): - pass - return False; - - class Recorder: chunk = 1024 - FORMAT = pyaudio.paInt16 - CHANNELS = 1 + channels = 1 @staticmethod def rms(frame): SHORT_NORMALIZE = (1.0/32768.0) count = len(frame) / 2 format = "%dh" % (count) shorts = struct.unpack(format, frame) sum_squares = 0.0 for sample in shorts: n = sample * SHORT_NORMALIZE sum_squares += n * n rms = math.pow(sum_squares / count, 0.5) return rms * 1000 def __init__(self, target, options): self.p = pyaudio.PyAudio() info = self.p.get_host_api_info_by_index(0) numdevices = info.get('deviceCount') self.target = target self.timeout_length = options.timeout - self.RATE = options.rate + self.rate = options.rate self.device = options.device self.min_rec_time = options.min_rec_time self.max_rec_time = options.max_rec_time self.external_lock_file = options.external_lock_file - self.activate_by_file = options.activate_by_file - self.activate_by_level = options.activate_by_level > 0 - self.threshold = options.activate_by_level - + self.external_trigger_file = options.external_trigger_file + self.level_lock_file = options.level_lock_file + self.level_enable_file = options.level_enable_file self.quiet = options.quiet self.started_by_file = False self.started_by_level = False devices = {} - orig_device = self.device - devices_text = [] for i in range(0, numdevices): if (self.p.get_device_info_by_host_api_device_index(0, i).get('maxInputChannels')) > 0: devices[i] = self.p.get_device_info_by_host_api_device_index(0, i).get('name') if devices[i] == str(self.device): self.device = i elif str(i) == str(self.device): self.device = i devices_text.append("%s) %s" % (i, devices[i])) + print('Available devices: %s' % ", ".join(devices_text)) try: - print('Using audio device %s) %s at sample rate %d' % (self.device, devices[self.device], self.RATE)) + print('Using audio device %s) %s at sample rate %d' % (self.device, devices[self.device], self.rate)) except KeyError: - print('Non existent audio device %s' % orig_device) - print('Available devices: %s' % ", ".join(devices_text)) - sys.exit(0) + print('Non existent audio device') - self.stream = self.p.open(format=self.FORMAT, - channels=self.CHANNELS, - rate=self.RATE, + self.threshold = 0 if target == 'test' else options.threshold + self.stream = self.p.open(format=pyaudio.paInt16, + channels=self.channels, + rate=self.rate, input=True, output=True, input_device_index=self.device, frames_per_buffer=self.chunk) def listen(self): wait_print = False i = 0 lock_print = False external_lock_print = False - festival_print = False + level_lock_print = False + level_enable_print = False + while True: i = i + 1 input = self.stream.read(self.chunk, exception_on_overflow = False) rms_val = self.rms(input) now = datetime.now().strftime('%Y-%m-%d %H:%M:%S') if os.path.exists(lock_file): if not lock_print: print("%s - lock file %s present, listen paused" % (now, lock_file)) lock_print = True continue else: if lock_print: print("%s - lock file %s absent, listen resumed" % (now, lock_file)) lock_print = False if self.external_lock_file: if os.path.exists(self.external_lock_file): if not external_lock_print: print("%s - external lock file %s present, listen paused" % (now, self.external_lock_file)) external_lock_print = True continue else: if external_lock_print: print("%s - external lock file %s absent, listen resumed" % (now, self.external_lock_file)) external_lock_print = False - - if checkIfProcessRunning('festival'): - if not festival_print: - print("%s - festival running, listen paused" % now) - festival_print = True - continue - else: - if festival_print: - print("%s - festival stopped, listen resumed" % now) - festival_print = False + + if self.level_lock_file: + if os.path.exists(self.level_lock_file): + if not level_lock_print: + print("%s - level lock file %s present, listen paused" % (now, self.level_lock_file)) + + level_lock_print = True + continue + else: + if level_lock_print: + print("%s - level lock file %s absent, listen resumed" % (now, self.level_lock_file)) + level_lock_print = False + wait_print = False - if self.activate_by_file: - if os.path.exists(self.activate_by_file): + if self.external_trigger_file: + if os.path.exists(self.external_trigger_file): + if not self.started_by_file: + print('%s - recording by file %s' % (now, self.external_trigger_file)) self.started_by_file = True - print('%s - recording by file %s' % (now, self.activate_by_file)) - - if not self.started_by_file and self.activate_by_level and rms_val >= self.threshold: - print('%s - recording by level %3d > %d' % (now, rms_val, self.threshold)) - self.started_by_level = True + + if self.threshold and not self.started_by_file: + if self.level_enable_file: + if not os.path.exists(self.level_enable_file): + if not level_enable_print: + print("%s - level enable file %s missing, listen paused" % (now, self.level_enable_file)) + + level_enable_print = True + print("%s - not listening, level %3d" % (now, rms_val), end='\r') + continue + else: + if level_enable_print: + print("%s - level enable file %s present, listen resumed" % (now, self.level_enable_file)) + level_enable_print = False + + if rms_val >= self.threshold: + if not self.started_by_level: + print('%s - recording by level %3d > %d' % (now, rms_val, self.threshold)) + self.started_by_level = True if self.started_by_level or self.started_by_file: self.record() else: if not self.quiet: - if self.activate_by_file: + if self.external_trigger_file: print("%s - listening, level %3d" % (now, rms_val), end='\r') else: print("%s - listening, level %3d < %d" % (now, rms_val, self.threshold), end='\r') def record(self): rec = [] current = time.time() end = time.time() + self.timeout_length start_time = current recording = False i = 0 - #now = datetime.now().strftime('%Y-%m-%d %H:%M:%S') - #print('%s - now recording...' % now) + now = datetime.now().strftime('%Y-%m-%d %H:%M:%S') + print('%s - now recording...' % now) if self.started_by_level: while current <= end: i = i + 1 data = self.stream.read(self.chunk) rms_val = self.rms(data) now = datetime.now().strftime('%Y-%m-%d %H:%M:%S') if rms_val >= self.threshold: end = time.time() + self.timeout_length #if not recording: diff = time.time() - start_time if not self.quiet: print('%s - recording at level %3d for %.1f seconds' % (now, rms_val, diff), end='\r') recording = True current = time.time() rec.append(data) rec_time = time.time() - start_time if rec_time > self.max_rec_time: #print("%s - maximum recording time of %d seconds reached" % (now, rec_time)) break else: rec_time = time.time() - start_time - self.timeout_length if os.path.exists(lock_file): print("%s - lock file %s detected" % (now, lock_file)) break if rec_time > self.min_rec_time: self.write(b''.join(rec), rec_time) else: - if rec_time > 1: - print("%s - skip short recording of %.1f seconds" % (now, rec_time)) + if rec_time > 0: + print("%s - skip too short recording of %.1f seconds" % (now, rec_time)) else: print() if self.started_by_file: - while os.path.exists(self.activate_by_file): + while os.path.exists(self.external_trigger_file): i = i + 1 data = self.stream.read(self.chunk) rms_val = self.rms(data) now = datetime.now().strftime('%Y-%m-%d %H:%M:%S') if not self.quiet: print('%s - recording by file at level %3d' % (now, rms_val), end='\r') recording = True current = time.time() rec.append(data) rec_time = time.time() - start_time if rec_time > self.max_rec_time: break if os.path.exists(lock_file): print("%s - lock file %s detected" % (now, lock_file)) break print("%s - recorded %.1f seconds" % (now, rec_time)) self.write(b''.join(rec), rec_time) self.started_by_file = False self.started_by_level = False def play(self, file): p = pyaudio.PyAudio() wf = wave.open(file, 'rb') stream = p.open( format = p.get_format_from_width(wf.getsampwidth()), channels = wf.getnchannels(), rate = wf.getframerate(), output = True ) data = wf.readframes(1024) while data != b'': stream.write(data) data = wf.readframes(1024) stream.close() p.terminate() def write(self, recording, duration): now = datetime.now().strftime('%Y-%m-%d %H:%M:%S') if os.path.exists(lock_file): print("%s - lock file %s present, skip file" % (now, lock_file)) return if self.external_lock_file and os.path.exists(self.external_lock_file): print("%s - external lock file %s present, skip file" % (now, self.external_lock_file)) return + if self.level_lock_file and os.path.exists(self.level_lock_file): + print("%s - level lock file %s present, skip file" % (now, self.level_lock_file)) + return + n_files = len(os.listdir(f_name_directory)) tmp_filename = os.path.join(f_name_directory, '%s.tmp' % self.target) wf = wave.open(tmp_filename, 'wb') - wf.setnchannels(self.CHANNELS) - wf.setsampwidth(self.p.get_sample_size(self.FORMAT)) - wf.setframerate(self.RATE) + wf.setnchannels(self.channels) + wf.setsampwidth(self.p.get_sample_size(pyaudio.paInt16)) + wf.setframerate(self.rate) wf.writeframes(recording) wf.close() filename = os.path.join(f_name_directory, '%s.wav' % self.target) os.rename(tmp_filename, filename) print('%s - saved %d seconds audio to %s' % (now, duration, filename)) if self.target == 'test': self.play(filename) def end(self): # Reset to default error handler self.p.terminate() asound.snd_lib_error_set_handler(None) if __name__ == '__main__': description = 'This script is a voice activate recorder that saves individual recording in the folder %s that is polled by sip-session to initiate an outgoing call and playback the file. The filename is in the format user@domain.wav. Use test argument to test audio level.' % f_name_directory usage = '%prog [options] [user@domain]' parser = OptionParser(usage=usage, description=description) parser.print_usage = parser.print_help parser.add_option('-r', '--sample_rate', type='int', default='16000', dest='rate', help='Audio sample rate') parser.add_option('-d', '--device', type='string', default='pulse', dest='device', help='Use selected input audio device') - parser.add_option('-t', '--timeout', type='int', default=2, dest='timeout', help='Silence timeout to stop recording') - parser.add_option('-m', '--min_rec_time', type='int', default=2, dest='min_rec_time', help='Minimum recording time to save recording') - parser.add_option('-M', '--max_rec_time', type='int', default=15, dest='max_rec_time', help='Maximum recording time for each file') -# parser.add_option('-t', '--threshold', type='int', default=30, dest='threshold', help='Minimum signal level to start recording') + parser.add_option('-T', '--timeout', type='int', default=2, dest='timeout', help='Silence timeout to stop recording') + parser.add_option('-m', '--min_rec_time', type='int', default=1, dest='min_rec_time', help='Minimum recording time to save recording') + parser.add_option('-M', '--max_rec_time', type='int', default=5, dest='max_rec_time', help='Maximum recording time for each file') + parser.add_option('-t', '--threshold', type='int', default=10, dest='threshold', help='Minimum signal level to start recording') + parser.add_option('-l', '--level_lock_file', type='string', dest='level_lock_file', help='Skip level recording if file exists') + parser.add_option('-L', '--level_enable_file', type='string', dest='level_enable_file', help='Enable level recording only if file exists') parser.add_option('-e', '--external_lock_file', type='string', dest='external_lock_file', help='Skip recording if file exists') - parser.add_option('-f', '--activate_by_file', type='string', dest='activate_by_file', help='Start recording if file exists, regardless of level') - parser.add_option('-l', '--activate_by_level', type='int', default=30, dest='activate_by_level', help='Start recording if sound exceeds this threshold level') + parser.add_option('-E', '--external_trigger_file', type='string', dest='external_trigger_file', help='Start recording if file exists, regardless of level') parser.add_option('-q', '--quiet', action='store_true', dest='quiet', default=False, help='Minimize logging.') options, args = parser.parse_args() try: target = args[0] except IndexError: parser.print_help() sys.exit(1) try: a = Recorder(args[0], options) a.listen() except KeyboardInterrupt: a.end() print() sys.exit(0) except OSError as e: print("Error: %s" % str(e)) sys.exit(0)