Page MenuHomePhabricator

No OneTemporary

This file is larger than 256 KB, so syntax highlighting was skipped.
diff --git a/sip-audio-session3 b/sip-audio-session3
index 6ab630b..e12592c 100755
--- a/sip-audio-session3
+++ b/sip-audio-session3
@@ -1,1270 +1,1261 @@
#!/usr/bin/env python3
import atexit
import os
import platform
import select
import shutil
import signal
import sys
import termios
import uuid
from datetime import datetime
from itertools import chain
from optparse import OptionParser
from threading import Thread
from time import sleep
from 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 twisted.internet import reactor
from sipsimple.account import Account, AccountManager, BonjourAccount
from sipsimple.audio import WavePlayer
from sipsimple.application import SIPApplication
from sipsimple.configuration import ConfigurationError
from sipsimple.configuration.settings import SIPSimpleSettings
from sipsimple.core import Engine, SIPCoreError, SIPURI, ToHeader, 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
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
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.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')
if self.input:
self.input.start()
self.output.start()
log.level.current = log.level.WARNING # get rid of twisted messages
Account.register_extension(AccountExtension)
BonjourAccount.register_extension(AccountExtension)
SIPSimpleSettings.register_extension(SIPSimpleSettingsExtension)
self.config_directory = options.config_directory or config_directory
try:
self.output.put("Using config directory: %s\n" % self.config_directory)
SIPApplication.start(self, FileStorage(self.config_directory))
except ConfigurationError 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)
def print_help(self):
message = 'Available control keys:\n'
message += ' s: toggle SIP trace on the console\n'
message += ' j: toggle PJSIP trace on the console\n'
message += ' n: toggle notifications trace on the console\n'
message += ' p: toggle printing RTP statistics on the console\n'
message += ' h: hang-up the active session\n'
message += ' r: toggle audio recording\n'
message += ' m: mute the microphone\n'
message += ' i: change audio input device\n'
message += ' o: change audio output device\n'
message += ' a: change audio alert device\n'
message += ' SPACE: hold/unhold\n'
message += ' Ctrl-d: quit the program\n'
message += ' ?: display this help message\n'
self.output.put('\n'+message+'\n')
def _NH_SIPApplicationWillStart(self, notification):
account_manager = AccountManager()
notification_center = NotificationCenter()
settings = SIPSimpleSettings()
if 'armv7' in platform.platform() and settings.audio.echo_canceller.enabled:
self.output.put("Disable echo canceller on ARM architecture\n")
settings.audio.echo_canceller.enabled = False
settings.save()
for account in account_manager.iter_accounts():
if isinstance(account, Account):
account.sip.register = False
account.presence.enabled = False
account.xcap.enabled = False
account.message_summary.enabled = False
if self.options.account is None:
self.account = account_manager.default_account
else:
possible_accounts = [account for account in account_manager.iter_accounts() if self.options.account in account.id and account.enabled]
if len(possible_accounts) > 1:
self.output.put('More than one account exists which matches %s: %s\n' % (self.options.account, ', '.join(sorted(account.id for account in possible_accounts))))
self.output.stop()
self.stop()
self.end_cancel_thread()
return
elif len(possible_accounts) == 0:
self.output.put('No enabled account that matches %s was found. Available and enabled accounts: %s\n' % (self.options.account, ', '.join(sorted(account.id for account in account_manager.get_accounts() if account.enabled))))
self.output.stop()
self.stop()
self.end_cancel_thread()
return
else:
self.account = possible_accounts[0]
notification_center.add_observer(self, sender=self.account)
if isinstance(self.account, Account) and self.target is None:
self.account.sip.register = True
self.account.presence.enabled = False
self.account.xcap.enabled = False
self.account.message_summary.enabled = False
self.output.put('Using account %s\n' % self.account.id)
self.logger.start()
if settings.logs.trace_sip and self.logger._siptrace_filename is not None:
self.output.put('Logging SIP trace to file "%s"\n' % self.logger._siptrace_filename)
if settings.logs.trace_pjsip and self.logger._pjsiptrace_filename is not None:
self.output.put('Logging PJSIP trace to file "%s"\n' % self.logger._pjsiptrace_filename)
if settings.logs.trace_notifications and self.logger._notifications_filename is not None:
self.output.put('Logging notifications trace to file "%s"\n' % self.logger._notifications_filename)
if self.options.disable_sound:
settings.audio.input_device = None
settings.audio.output_device = None
settings.audio.alert_device = None
if self.options.enable_default_devices:
settings.audio.input_device = 'system_default'
settings.audio.output_device = 'system_default'
settings.audio.alert_device = 'system_default'
def _NH_SIPApplicationDidStart(self, notification):
engine = Engine()
settings = SIPSimpleSettings()
engine.trace_sip = self.logger.sip_to_stdout or settings.logs.trace_sip
engine.log_level = settings.logs.pjsip_level if (self.logger.pjsip_to_stdout or settings.logs.trace_pjsip) else 0
self.ip_address_monitor.start()
self.output.put('Available audio input devices: %s\n' % ', '.join(['None', 'system_default'] + sorted(engine.input_devices)))
self.output.put('Available audio output devices: %s\n' % ', '.join(['None', 'system_default'] + sorted(engine.output_devices)))
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)))
else:
if self.enable_video:
self.output.put('No camera present, video is disabled')
self.enable_video = False
if self.voice_audio_mixer.input_device == 'system_default':
self.output.put('Using audio input device: %s (system default device)\n' % self.voice_audio_mixer.real_input_device)
else:
self.output.put('Using audio input device: %s\n' % self.voice_audio_mixer.input_device)
if self.voice_audio_mixer.output_device == 'system_default':
self.output.put('Using audio output device: %s (system default device)\n' % self.voice_audio_mixer.real_output_device)
else:
self.output.put('Using audio output device: %s\n' % self.voice_audio_mixer.output_device)
if self.alert_audio_mixer.output_device == 'system_default':
self.output.put('Using audio alert device: %s (system default device)\n' % self.alert_audio_mixer.real_output_device)
else:
self.output.put('Using audio alert device: %s\n' % self.alert_audio_mixer.output_device)
if not self.batch_mode:
self.print_help()
inbound_ringtone = self.account.sounds.audio_inbound.sound_file if self.account.sounds.audio_inbound is not None else None
outbound_ringtone = settings.sounds.audio_outbound
if inbound_ringtone:
self.wave_inbound_ringtone = WavePlayer(self.alert_audio_mixer, inbound_ringtone.path.normalized, volume=inbound_ringtone.volume, loop_count=0, pause_time=2)
self.alert_audio_bridge.add(self.wave_inbound_ringtone)
if outbound_ringtone:
self.wave_outbound_ringtone = WavePlayer(self.voice_audio_mixer, outbound_ringtone.path.normalized, volume=outbound_ringtone.volume, loop_count=0, pause_time=2)
self.voice_audio_bridge.add(self.wave_outbound_ringtone)
self.tone_ringtone = WavePlayer(self.voice_audio_mixer, ResourcePath('sounds/ring_tone.wav').normalized, loop_count=0, pause_time=6)
self.voice_audio_bridge.add(self.tone_ringtone)
self.hold_tone = WavePlayer(self.voice_audio_mixer, ResourcePath('sounds/hold_tone.wav').normalized, loop_count=0, pause_time=30, volume=50)
self.voice_audio_bridge.add(self.hold_tone)
if 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.verify_server = True
settings.save()
- if isinstance(self.account, Account):
- self.account.tls.ca_list = os.path.join(config_directory, 'tls/ca.crt')
- self.account.tls.verify_server = True
- self.account.save()
-
- if self.account.tls.certificate is None and isinstance(self.account, BonjourAccount):
- self.account.tls.certificate = os.path.join(config_directory, 'tls/default.crt')
- self.account.save()
-
if self.target is not None:
if isinstance(self.account, BonjourAccount) and '@' not in self.target:
self.output.put('Bonjour mode requires a host in the destination address\n')
self.stop()
self.end_cancel_thread()
return
if '@' not in self.target:
self.target = '%s@%s' % (self.target, self.account.id.domain)
if not self.target.startswith('sip:') and not self.target.startswith('sips:'):
self.target = 'sip:' + self.target
try:
self.target = SIPURI.parse(self.target)
except SIPCoreError:
self.output.put('Illegal SIP URI: %s\n' % self.target)
self.stop()
else:
if '.' not in self.target.host.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)
if isinstance(self.account, Account) and self.account.sip.outbound_proxy is not None:
uri = SIPURI(host=self.account.sip.outbound_proxy.host, port=self.account.sip.outbound_proxy.port, parameters={'transport': self.account.sip.outbound_proxy.transport})
elif isinstance(self.account, Account) and self.account.sip.always_use_my_proxy:
uri = SIPURI(host=self.account.id.domain)
else:
uri = self.target
self.session_spool_dir = self.spool_dir + "/" + (self.options.external_id or str(uuid.uuid1()))
tls_name = self.account.sip.tls_name if isinstance(self.account, Account) else None
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
message = '%s Registered contact "%s" for sip:%s at %s:%d;transport=%s (expires in %d seconds).\n' % (datetime.now().replace(microsecond=0), contact_header.uri, self.account.id, registrar.address, registrar.port, registrar.transport, expires)
if len(contact_header_list) > 1:
message += 'Other registered contacts:\n%s\n' % '\n'.join([' %s (expires in %s seconds)' % (str(other_contact_header.uri), other_contact_header.expires) for other_contact_header in contact_header_list if other_contact_header.uri != notification.data.contact_header.uri])
self.output.put(message)
self.registration_succeeded = True
def _NH_SIPAccountRegistrationDidFail(self, notification):
self.output.put('%s Failed to register contact for sip:%s: %s (retrying in %.2f seconds)\n' % (datetime.now().replace(microsecond=0), self.account.id, notification.data.error, notification.data.retry_after))
self.registration_succeeded = False
def _NH_SIPAccountRegistrationDidEnd(self, notification):
self.output.put('%s Registration ended.\n' % datetime.now().replace(microsecond=0))
def _NH_BonjourAccountRegistrationDidSucceed(self, notification):
self.output.put('%s Registered Bonjour contact %s\n' % (datetime.now().replace(microsecond=0), notification.data.name))
def _NH_BonjourAccountRegistrationDidFail(self, notification):
self.output.put('%s Failed to register Bonjour contact: %s\n' % (datetime.now().replace(microsecond=0), notification.data.reason))
def _NH_BonjourAccountRegistrationDidEnd(self, notification):
self.output.put('%s Registration ended.\n' % datetime.now().replace(microsecond=0))
def _NH_BonjourAccountDidAddNeighbour(self, notification):
neighbour, record = notification.data.neighbour, notification.data.record
now = datetime.now().replace(microsecond=0)
self.output.put('%s Discovered Bonjour neighbour: %s (%s) <%s>\n' % (now, record.name, record.host, record.uri))
self.neighbours[neighbour] = BonjourNeighbour(neighbour, record.uri, record.name, record.host)
def _NH_BonjourAccountDidUpdateNeighbour(self, notification):
neighbour, record = notification.data.neighbour, notification.data.record
now = datetime.now().replace(microsecond=0)
try:
bonjour_neighbour = self.neighbours[neighbour]
except KeyError:
self.output.put('%s Discovered Bonjour neighbour: "%s (%s)" <%s>\n' % (now, record.name, record.host, record.uri))
self.neighbours[neighbour] = BonjourNeighbour(neighbour, record.uri, record.name, record.host)
else:
self.output.put('%s Updated Bonjour neighbour: "%s (%s)" <%s>\n' % (now, record.name, record.host, record.uri))
bonjour_neighbour.display_name = record.name
bonjour_neighbour.host = record.host
bonjour_neighbour.uri = record.uri
def _NH_BonjourAccountDidRemoveNeighbour(self, notification):
neighbour = notification.data.neighbour
now = datetime.now().replace(microsecond=0)
try:
bonjour_neighbour = self.neighbours.pop(neighbour)
except KeyError:
pass
else:
self.output.put('%s Bonjour neighbour left: "%s (%s)" <%s>\n' % (now, bonjour_neighbour.display_name, bonjour_neighbour.host, bonjour_neighbour.uri))
def _NH_DNSLookupDidSucceed(self, notification):
notification_center = NotificationCenter()
result_text = ', '.join(('%s:%s (%s)' % (result.address, result.port, result.transport.upper()) for result in notification.data.result))
self.output.put("\nDNS lookup for %s succeeded: %s\n" % (self.target.host.decode(), result_text))
if self.end_session_if_needed():
self.stop()
self.end_cancel_thread()
self.outgoing_session = session = Session(self.account)
notification_center.add_observer(self, sender=session)
streams = [MediaStreamRegistry.AudioStream(), MediaStreamRegistry.VideoStream()] if self.enable_video else [MediaStreamRegistry.AudioStream()]
session.connect(ToHeader(self.target), routes=notification.data.result, streams=streams)
def _NH_DNSLookupDidFail(self, notification):
self.output.put('DNS lookup failed: %s\n' % notification.data.error)
self.stop()
self.end_cancel_thread()
def auto_answer_allowed(self, uri):
if not self.options.auto_answer_uris:
self.output.put('Auto answer allowed for %s\n' % uri)
return True
uri = uri.split(":")[1]
auto_answer_uris = self.options.auto_answer_uris.split(",")
if uri in auto_answer_uris:
self.output.put('Auto answer allowed for %s\n' % uri)
return True
self.output.put('Auto answer denied for %s\n' % uri)
return False
def _NH_SIPSessionNewIncoming(self, notification):
session = notification.sender
for stream in notification.data.streams:
if stream.type 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:
session.accept([stream for stream in session.proposed_streams if stream.type in accepted_types])
return
else:
def auto_answer():
self.incoming_sessions.remove(session)
session.accept([stream for stream in session.proposed_streams if stream.type in accepted_types])
timer = reactor.callLater(self.options.auto_answer_interval, auto_answer)
self.answer_timers[id(session)] = timer
session.send_ring_indication()
self.incoming_sessions.append(session)
if len(self.incoming_sessions) == 1:
self._print_new_session()
if not self.started_sessions:
if self.wave_inbound_ringtone:
self.wave_inbound_ringtone.start()
else:
self.tone_ringtone.start()
def _NH_SIPSessionNewOutgoing(self, notification):
session = notification.sender
local_identity = str(session.local_identity.uri)
if session.local_identity.display_name:
local_identity = '"%s" <%s>' % (session.local_identity.display_name, local_identity)
remote_identity = str(session.remote_identity.uri)
if session.remote_identity.display_name:
remote_identity = '"%s" <%s>' % (session.remote_identity.display_name, remote_identity)
self.output.put("Initiating SIP %s session from %s to %s via %s...\n" % ('video' if self.enable_video else 'audio', local_identity, remote_identity, session.route))
def _NH_SIPSessionGotRingIndication(self, notification):
if self.wave_outbound_ringtone:
self.wave_outbound_ringtone.start()
def _NH_SIPSessionDidFail(self, notification):
session = notification.sender
if session.direction == 'incoming' and session in self.incoming_sessions:
if self.wave_inbound_ringtone:
self.wave_inbound_ringtone.stop()
self.tone_ringtone.stop()
elif session.direction == 'outgoing':
if self.wave_outbound_ringtone:
self.wave_outbound_ringtone.stop()
if notification.data.failure_reason == 'Call completed elsewhere' or notification.data.code == 487:
self.output.put('Session cancelled\n')
if session is self.outgoing_session:
self.stop()
self.end_cancel_thread()
if session in self.incoming_sessions:
self.incoming_sessions.remove(session)
elif notification.data.failure_reason == 'user request':
self.output.put('Session rejected by user (%d %s)\n' % (notification.data.code, notification.data.reason))
if notification.sender is self.outgoing_session:
self.stop()
self.end_cancel_thread()
else:
self.output.put('Session failed: %s\n' % notification.data.failure_reason)
if session is self.outgoing_session:
self.stop()
self.end_cancel_thread()
if id(session) in self.answer_timers:
timer = self.answer_timers[id(session)]
if timer.active():
timer.cancel()
del self.answer_timers[id(session)]
if self.incoming_sessions:
self._print_new_session()
elif session.direction == 'incoming':
if self.wave_inbound_ringtone:
self.wave_inbound_ringtone.stop()
self.tone_ringtone.stop()
self.success = False
def _NH_SIPSessionWillStart(self, notification):
session = notification.sender
if session.direction == 'incoming':
if self.wave_inbound_ringtone:
self.wave_inbound_ringtone.stop()
if not self.incoming_sessions:
self.tone_ringtone.stop()
else:
if self.wave_outbound_ringtone:
self.wave_outbound_ringtone.stop()
if id(session) in self.answer_timers:
timer = self.answer_timers[id(session)]
if timer.active():
timer.cancel()
del self.answer_timers[id(session)]
def _NH_SIPSessionDidStart(self, notification):
notification_center = NotificationCenter()
session = notification.sender
self.output.put('Session started with %d streams\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:
self.tone_ringtone.start()
self._print_new_session()
for stream in notification.data.streams:
notification_center.add_observer(self, sender=stream)
if self.options.auto_hangup_interval is not None:
if self.options.auto_hangup_interval == 0:
session.end()
else:
timer = reactor.callLater(self.options.auto_hangup_interval, session.end)
self.hangup_timers[id(session)] = timer
def _NH_SIPSessionWillEnd(self, notification):
notification_center = NotificationCenter()
session = notification.sender
if id(session) in self.hangup_timers:
timer = self.hangup_timers[id(session)]
if timer.active():
timer.cancel()
del self.hangup_timers[id(session)]
hangup_tone = WavePlayer(self.voice_audio_mixer, ResourcePath('sounds/hangup_tone.wav').normalized)
notification_center.add_observer(self, sender=hangup_tone)
self.voice_audio_bridge.add(hangup_tone)
hangup_tone.start()
def end_session_if_needed(self):
if not self.session_spool_dir:
return False
stop_call_file = self.session_spool_dir + "/stop"
if stop_call_file and os.path.exists(stop_call_file):
if self.active_session is not None:
self.output.put('Ending audio session...\n')
self.active_session.end()
elif self.outgoing_session is not None:
self.outgoing_session.end()
elif self.incoming_sessions:
session = self.incoming_sessions.pop(0)
session.reject()
return True
return False
def end_cancel_thread(self):
if self.stop_call_thread is not None:
self.stop_call_thread.stop()
self.stop_call_thread = None
if self.session_spool_dir:
try:
shutil.rmtree(self.session_spool_dir)
except OSError:
pass
self.session_spool_dir = None
def _NH_SIPSessionDidEnd(self, notification):
self.end_cancel_thread()
session = notification.sender
if session is not self.active_session:
identity = str(session.remote_identity.uri)
if session.remote_identity.display_name:
identity = '"%s" <%s>' % (session.remote_identity.display_name, identity)
else:
identity = '\b'
if notification.data.end_reason == 'user request':
self.output.put('Session %s ended by %s party\n' % (identity, notification.data.originator))
else:
self.output.put('Session %s ended due to error: %s\n' % (identity, notification.data.end_reason))
if session.end_time and session.start_time:
duration = session.end_time - session.start_time
seconds = duration.seconds if duration.microseconds < 500000 else duration.seconds+1
minutes, seconds = seconds / 60, seconds % 60
hours, minutes = minutes / 60, minutes % 60
hours += duration.days*24
if not minutes and not hours:
duration_text = '%d seconds' % seconds
elif not hours:
duration_text = '%02d:%02d' % (minutes, seconds)
else:
duration_text = '%02d:%02d:%02d' % (hours, minutes, seconds)
self.output.put('Session duration was %s\n' % duration_text)
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:
self.stop()
self.end_cancel_thread()
on_hold_streams = [stream for stream in chain(*(session.streams for session in self.started_sessions)) if stream.on_hold]
if not on_hold_streams and self.hold_tone.is_active:
self.hold_tone.stop()
self.success = True
def _NH_SIPSessionDidChangeHoldState(self, notification):
session = notification.sender
if notification.data.on_hold:
if notification.data.originator == 'remote':
if session is self.active_session:
self.output.put('Remote party has put the audio session on hold\n')
else:
identity = str(session.remote_identity.uri)
if session.remote_identity.display_name:
identity = '"%s" <%s>' % (session.remote_identity.display_name, identity)
self.output.put('%s has put the audio session on hold\n' % identity)
elif not self.ignore_local_hold:
if session is self.active_session:
self.output.put('Session is put on hold\n')
else:
identity = str(session.remote_identity.uri)
if session.remote_identity.display_name:
identity = '"%s" <%s>' % (session.remote_identity.display_name, identity)
self.output.put('Session %s is put on hold\n' % identity)
else:
self.ignore_local_hold = False
else:
if notification.data.originator == 'remote':
if session is self.active_session:
self.output.put('Remote party has taken the audio session out of hold\n')
else:
identity = str(session.remote_identity.uri)
if session.remote_identity.display_name:
identity = '"%s" <%s>' % (session.remote_identity.display_name, identity)
self.output.put('%s has taken the audio session out of hold\n' % identity)
elif not self.ignore_local_unhold:
if session is self.active_session:
self.output.put('Session is taken out of hold\n')
else:
identity = str(session.remote_identity.uri)
if session.remote_identity.display_name:
identity = '"%s" <%s>' % (session.remote_identity.display_name, identity)
self.output.put('Session %s is taken out of hold\n' % identity)
else:
self.ignore_local_unhold = False
def _NH_SIPSessionNewProposal(self, notification):
if notification.data.originator == 'remote':
session = notification.sender
accepted_types = ['video', 'audio', '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.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_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 not self.hold_tone.is_active:
self.hold_tone.start()
else:
on_hold_streams = [stream for stream in chain(*(session.streams for session in self.started_sessions)) if stream is not notification.sender and stream.on_hold]
if not on_hold_streams and self.hold_tone.is_active:
self.hold_tone.stop()
def _NH_RTPStreamDidChangeRTPParameters(self, notification):
stream = notification.sender
self.output.put('Audio RTP parameters changed:\n')
self.output.put('Audio stream using "%s" codec at %sHz\n' % (stream.codec, stream.sample_rate))
self.output.put('Audio RTP endpoints %s:%d <-> %s:%d\n' % (stream.local_rtp_address, stream.local_rtp_port, stream.remote_rtp_address, stream.remote_rtp_port))
if stream.encryption.active:
self.output.put('RTP audio stream is encrypted using %s (%s)\n' % (stream.encryption.type, stream.encryption.cipher))
def _NH_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_WavePlayerDidEnd(self, notification):
notification_center = NotificationCenter()
notification_center.remove_observer(self, sender=notification.sender)
def _NH_WavePlayerDidFail(self, notification):
notification_center = NotificationCenter()
notification_center.remove_observer(self, sender=notification.sender)
self.output.put('Failed to play %s: %s' % (notification.sender.filename, notification.data.error))
def _NH_DefaultAudioDeviceDidChange(self, notification):
SIPApplication._NH_DefaultAudioDeviceDidChange(self, notification)
if notification.data.changed_input and self.voice_audio_mixer.input_device=='system_default':
self.output.put('Switched default input device to: %s\n' % self.voice_audio_mixer.real_input_device)
if notification.data.changed_output and self.voice_audio_mixer.output_device=='system_default':
self.output.put('Switched default output device to: %s\n' % self.voice_audio_mixer.real_output_device)
if notification.data.changed_output and self.alert_audio_mixer.output_device=='system_default':
self.output.put('Switched alert device to: %s\n' % self.alert_audio_mixer.real_output_device)
def _NH_AudioDevicesDidChange(self, notification):
old_devices = set(notification.data.old_devices)
new_devices = set(notification.data.new_devices)
added_devices = new_devices - old_devices
removed_devices = old_devices - new_devices
changed_input_device = self.voice_audio_mixer.real_input_device in removed_devices
changed_output_device = self.voice_audio_mixer.real_output_device in removed_devices
changed_alert_device = self.alert_audio_mixer.real_output_device in removed_devices
SIPApplication._NH_AudioDevicesDidChange(self, notification)
if added_devices:
self.output.put('Added audio device(s): %s\n' % ', '.join(sorted(added_devices)))
if removed_devices:
self.output.put('Removed audio device(s): %s\n' % ', '.join(sorted(removed_devices)))
if changed_input_device:
self.output.put('Audio input device has been switched to: %s\n' % self.voice_audio_mixer.real_input_device)
if changed_output_device:
self.output.put('Audio output device has been switched to: %s\n' % self.voice_audio_mixer.real_output_device)
if changed_alert_device:
self.output.put('Audio alert device has been switched to: %s\n' % self.alert_audio_mixer.real_output_device)
self.output.put('Available audio input devices: %s\n' % ', '.join(['None', 'system_default'] + sorted(self.engine.input_devices)))
self.output.put('Available audio output devices: %s\n' % ', '.join(['None', 'system_default'] + sorted(self.engine.output_devices)))
def _NH_RTPStreamICENegotiationDidSucceed(self, notification):
self.output.put("ICE negotiation succeeded in %s\n" % notification.data.duration)
def _NH_RTPStreamICENegotiationDidFail(self, notification):
self.output.put("ICE negotiation failed: %s\n" % notification.data.reason.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('-s', '--trace-sip', action='store_true', dest='trace_sip', default=False, help='Dump the raw contents of incoming and outgoing SIP messages.')
parser.add_option('-j', '--trace-pjsip', action='store_true', dest='trace_pjsip', default=False, help='Print PJSIP logging output.')
parser.add_option('-n', '--trace-notifications', action='store_true', dest='trace_notifications', default=False, help='Print all notifications (disabled by default).')
parser.add_option('-S', '--disable-sound', action='store_true', dest='disable_sound', default=False, help='Disables initializing the sound card.')
parser.set_default('auto_answer_interval', None)
parser.add_option('--auto-answer', action='callback', callback=parse_handle_call_option, callback_args=('auto_answer_interval',), help='Interval after which to answer an incoming session (disabled by default). If the option is specified but the interval is not, it defaults to 0 (accept the session as soon as it starts ringing).', metavar='[INTERVAL]')
parser.add_option('-u', '--auto-answer-uris', type='string', dest='auto_answer_uris', default="", help='Optional list of SIP URIs for which auto-answer is allowed')
parser.add_option('-i', '--external-id', type='string', dest='external_id', help='id used for call control from external application')
parser.add_option('-v', '--spool-dir', type='string', dest='spool_dir', default=None, help='Spool dir for call control from external applications, default is /var/spool/sipclients/sessions')
parser.add_option('-t', '--enable-default-devices', action='store_true', dest='enable_default_devices', help='Use default audio devices')
parser.add_option('-V', '--enable-video', action='store_true', dest='enable_video', default=False, help='Enable video if camera is available')
parser.set_default('auto_hangup_interval', None)
parser.add_option('--auto-hangup', action='callback', callback=parse_handle_call_option, callback_args=('auto_hangup_interval',), help='Interval after which to hang up an established session (disabled by default). If the option is specified but the interval is not, it defaults to 0 (hangup the session as soon as it connects).', metavar='[INTERVAL]')
parser.add_option('-b', '--batch', action='store_true', dest='batch_mode', default=False, help='Run the program in batch mode: reading input from the console is disabled and the option --auto-answer is implied. This is particularly useful when running this script in a non-interactive environment.')
parser.add_option('-d', '--daemonize', action='store_true', dest='daemonize', default=False, help='Enable running this program as a deamon.')
options, args = parser.parse_args()
target = args[0] if args and not options.auto_answer_uris else None
application = SIPAudioApplication()
application.start(target, options)
signal.signal(signal.SIGINT, signal.SIG_DFL)
application.output.join()
sleep(0.1)
sys.exit(0 if application.success else 1)
diff --git a/sip-publish-presence3 b/sip-publish-presence3
index 4a5cf5a..9ca9c5a 100755
--- a/sip-publish-presence3
+++ b/sip-publish-presence3
@@ -1,840 +1,840 @@
#!/usr/bin/env python3
import hashlib
import os
import random
import select
import sys
import termios
import traceback
import uuid
from collections import deque
from datetime import datetime
from optparse import OptionParser
from threading import RLock, Thread
from time import time
from application import log
from application.notification import IObserver, NotificationCenter, NotificationData
from application.python.queue import EventQueue
from eventlib.twistedutil import join_reactor
from twisted.internet import reactor
from twisted.internet.error import ReactorNotRunning
from zope.interface import implementer
from sipsimple.core import Engine, FromHeader, Publication, PublicationError, RouteHeader, SIPCoreError, SIPURI, PJSIPError
from sipsimple.account import Account, AccountManager, BonjourAccount
from sipsimple.application import SIPApplication
from sipsimple.configuration import ConfigurationError, ConfigurationManager
from sipsimple.configuration.settings import SIPSimpleSettings
from sipsimple.lookup import DNSLookup
from sipsimple.payloads import BuilderError
from sipsimple.payloads.pidf import Contact, Device, DeviceInfo, DMNote, DeviceTimestamp, Person, PIDFNote, PersonTimestamp, PIDF, Service, ServiceTimestamp, Status
from sipsimple.payloads.rpid import Activities, ActivityRegistry, Mood, MoodRegistry, RPIDNote, TimeOffset
from sipsimple.storage import FileStorage
from sipsimple.threading import run_in_twisted_thread
from sipclient.configuration import config_directory
from sipclient.configuration.account import AccountExtension
from sipclient.configuration.settings import SIPSimpleSettingsExtension
from sipclient.log import Logger
class KeyBinding(object):
def __init__(self, description, handler):
self.description = description
self.handler = handler
class Prompt(str):
"""Used to make a distinction between normal strings written to the console and prompts."""
class Menu(object):
key_bindings = {}
def __init__(self, interface):
self.interface = interface
def print_prompt(self):
buf = ['Commands:']
for key, binding in list(self.key_bindings.items()):
buf.append(' %s: %s' % (key, binding.description))
self.interface.application.output.put('\n'+'\n'.join(buf)+'\n')
def _exit(self):
self.interface.exit_menu()
def _exit_program(self):
self.interface.application.stop()
# Mood manipulation menu
class MoodMenu(Menu):
def _show_moods(self):
person = self.interface.application.person
buf = ['Moods:']
if person.mood is not None:
for m in list(person.mood):
buf.append(' %s' % str(m))
self.interface.application.output.put('\n'.join(buf))
def _add_mood(self):
person = self.interface.application.person
buf = ['Possible moods:']
values = list(MoodRegistry.names)
values.sort()
max_len = max(len(s) for s in values)+2
format = ' %%02d) %%-%ds' % max_len
num_line = 80/(max_len+5)
i = 0
text = ''
for val in values:
text += format % (i+1, val)
i += 1
if i % num_line == 0:
buf.append(text)
text = ''
buf.append(text)
self.interface.application.output.put('\n'.join(buf)+'\n')
m = self.interface.getstring('Select mood to add (any non-number will return)')
try:
m = int(m)
if m not in range(len(values)):
raise ValueError
except ValueError:
self.interface.application.output.put('Invalid input')
else:
if person.mood is None:
person.mood = Mood()
person.mood.add(values[m-1])
person.timestamp = PersonTimestamp(datetime.now())
self.interface.application.publish()
self.interface.application.output.put('Mood added')
self.interface.show_top_level()
def _del_mood(self):
person = self.interface.application.person
if person.mood is None:
self.interface.application.output.put('There is no current mood set')
self.print_prompt()
return
buf = ['Current moods:']
values = list(person.mood)
values.sort()
max_len = max(len(s) for s in values)+2
format = " %%02d) %%-%ds" % max_len
num_line = 80/(max_len+5)
i = 0
text = ''
for val in values:
text += format % (i+1, val)
i += 1
if i % num_line == 0:
buf.append(text)
text = ''
buf.append(text)
self.interface.application.output.put('\n'.join(buf)+'\n')
m = self.interface.getstring('Select mood to delete (any non-number will return)')
try:
m = int(m)
except ValueError:
self.interface.application.output.put('Invalid input')
else:
person.mood.remove(values[m-1])
person.timestamp = PersonTimestamp(datetime.now())
self.interface.application.publish()
self.interface.application.output.put('Mood deleted')
self.interface.show_top_level()
def _clear_moods(self):
person = self.interface.application.person
if person.mood is None:
self.interface.application.output.put('There is no current mood set')
self.print_prompt()
return
person.mood = None
person.timestamp = PersonTimestamp(datetime.now())
self.interface.application.publish()
self.interface.application.output.put('Mood information cleared')
self.interface.show_top_level()
def _set_note(self):
person = self.interface.application.person
if person.mood is not None and person.mood.notes:
a_note = next(iter(person.mood.notes))
self.interface.application.output.put('Current note: %s' % a_note)
note = self.interface.getstring('Set note (press return to delete)')
if note == '':
person.mood.notes.clear()
self.interface.application.output.put('Note removed')
else:
if person.mood is None:
person.mood = Mood()
person.mood.notes.add(RPIDNote(note, lang='en'))
self.interface.application.output.put('Note set')
person.timestamp = PersonTimestamp(datetime.now())
self.interface.application.publish()
self.interface.show_top_level()
def _set_random(self):
person = self.interface.application.person
values = list(value for value in MoodRegistry.names if value != 'unknown')
random.shuffle(values)
if person.mood is None:
person.mood = Mood()
else:
person.mood.clear()
values = values[:3]
for mood in values:
person.mood.add(mood)
person.timestamp = PersonTimestamp(datetime.now())
self.interface.application.publish()
self.interface.application.output.put('You are now ' + ', '.join(values))
self.interface.show_top_level()
key_bindings = {'s': KeyBinding(description='show current moods', handler=_show_moods),
'a': KeyBinding(description='add a mood', handler=_add_mood),
'd': KeyBinding(description='delete a mood', handler=_del_mood),
'c': KeyBinding(description='clear all mood data', handler=_clear_moods),
'n': KeyBinding(description='set mood note', handler=_set_note),
'r': KeyBinding(description='set random mood', handler=_set_random),
'x': KeyBinding(description='exit to upper level menu', handler=Menu._exit),
'q': KeyBinding(description='quit program', handler=Menu._exit_program)}
# Activities manipulation menu
class ActivitiesMenu(Menu):
def _show_activity(self):
person = self.interface.application.person
buf = ["Activity:"]
if person.activities is not None:
for a in list(person.activities):
buf.append(" %s" % str(a))
self.interface.application.output.put('\n'.join(buf))
def _set_activity(self):
person = self.interface.application.person
buf = ["Possible activities:"]
values = list(ActivityRegistry.names)
values.sort()
max_len = max(len(s) for s in values)+2
format = " %%02d) %%-%ds" % max_len
num_line = 80/(max_len+5)
i = 0
text = ''
for val in values:
text += format % (i+1, val)
i += 1
if i % num_line == 0:
buf.append(text)
text = ''
self.interface.application.output.put('\n'.join(buf)+'\n')
a = self.interface.getstring('Select activity to add (any non-number will return)')
try:
a = int(a)
if a-1 not in range(len(values)):
raise ValueError
except ValueError:
self.interface.application.output.put('Invalid input')
else:
if person.activities is None:
person.activities = Activities()
else:
person.activities.clear()
person.activities.add(values[a-1])
person.timestamp = PersonTimestamp(datetime.now())
self.interface.application.publish()
self.interface.application.output.put('Activity set')
self.interface.show_top_level()
def _del_activity(self):
person = self.interface.application.person
if person.activities is None or len(person.activities.values) == 0:
self.interface.application.output.put('There is no current activity set')
return
person.activities.clear()
person.activities.add('unknown')
person.timestamp = PersonTimestamp(datetime.now())
self.interface.application.publish()
self.interface.application.output.put('Activity deleted')
self.interface.show_top_level()
def _clear_activity(self):
person = self.interface.application.person
if person.activities is None:
self.interface.application.output.put('There is no current activity set')
return
person.activities = None
person.timestamp = PersonTimestamp(datetime.now())
self.interface.application.publish()
self.interface.application.output.put('Activities information cleared')
self.interface.show_top_level()
def _set_note(self):
person = self.interface.application.person
if person.activities is not None and person.activities.notes:
a_note = next(iter(person.activities.notes))
self.interface.application.output.put('Current note: %s' % a_note)
note = self.interface.getstring('Set note (press return to delete)')
if note == '':
person.activities.notes.clear()
self.interface.application.output.put('Note deleted')
else:
if person.activities is None:
person.activities = Activities()
person.activities.add('unknown')
person.activities.notes.add(RPIDNote(note, lang='en'))
self.interface.application.output.put('Note set')
person.timestamp = PersonTimestamp(datetime.now())
self.interface.application.publish()
self.interface.show_top_level()
def _set_random(self):
person = self.interface.application.person
values = list(value for value in ActivityRegistry.names if value != 'unknown')
activity = random.choice(values)
if person.activities is None:
person.activities = Activities()
else:
person.activities.clear()
person.activities.add(activity)
person.timestamp = PersonTimestamp(datetime.now())
self.interface.application.publish()
self.interface.application.output.put('You are now %s' % activity)
self.interface.show_top_level()
key_bindings = {'s': KeyBinding(description='show current activity', handler=_show_activity),
'a': KeyBinding(description='set activity', handler=_set_activity),
'd': KeyBinding(description='delete activity', handler=_del_activity),
'c': KeyBinding(description='clear all acitivity data', handler=_clear_activity),
'n': KeyBinding(description='set activity note', handler=_set_note),
'r': KeyBinding(description='set random activity', handler=_set_random),
'x': KeyBinding(description='exit to upper level menu', handler=Menu._exit),
'q': KeyBinding(description='quit program', handler=Menu._exit_program)}
# Extended status manipulation menu
class ExtendedStatusMenu(Menu):
def _show_extended_status(self):
service = self.interface.application.service
buf = ["Extended status:"]
if service.status.extended is not None:
buf.append(" %s" % str(service.status.extended))
self.interface.application.output.put('\n'.join(buf))
def _set_extended_status(self):
service = self.interface.application.service
buf = ["Possible statuses:"]
values = ['available', 'away', 'extended-away', 'busy', 'offline']
max_len = max(len(s) for s in values)+2
format = " %%02d) %%-%ds" % max_len
num_line = 80/(max_len+5)
i = 0
text = ''
for val in values:
text += format % (i+1, val)
i += 1
if i % num_line == 0:
buf.append(text)
text = ''
buf.append(text)
self.interface.application.output.put('\n'.join(buf)+'\n')
a = self.interface.getstring('Select status (any non-number will return)')
try:
a = int(a)
if a-1 not in range(len(values)):
raise ValueError
except ValueError:
self.interface.application.output.put('Invalid input')
else:
status = values[a-1]
if status == 'offline':
service.status.basic = 'closed'
else:
service.status.basic = 'open'
service.status.extended = status
self.interface.application.publish()
self.interface.application.output.put('Extended status set')
self.interface.show_top_level()
def _del_extended_status(self):
self.interface.application.service.status.extended = None
self.interface.application.publish()
self.interface.application.output.put('Activity deleted')
self.interface.show_top_level()
key_bindings = {'s': KeyBinding(description='show current extended status', handler=_show_extended_status),
'e': KeyBinding(description='set extended status', handler=_set_extended_status),
'd': KeyBinding(description='delete extended status', handler=_del_extended_status),
'x': KeyBinding(description='exit to upper level menu', handler=Menu._exit),
'q': KeyBinding(description='quit program', handler=Menu._exit_program)}
class TopLevelMenu(Menu):
def _show_pidf(self):
try:
pidf = self.interface.application.pidf.toxml(pretty_print=True)
except BuilderError as e:
print("PIDF as currently defined is invalid: %s" % str(e))
except:
traceback.print_exc()
else:
self.interface.application.output.put(pidf)
self.print_prompt()
def _set_mood_info(self):
mood_menu = MoodMenu(self.interface)
self.interface.add_menu(mood_menu)
def _set_activity_info(self):
activities_menu = ActivitiesMenu(self.interface)
self.interface.add_menu(activities_menu)
def _toggle_basic(self):
service = self.interface.application.service
if service.status.basic == 'open':
service.status.basic = 'closed'
else:
service.status.basic = 'open'
service.timestamp = ServiceTimestamp(datetime.now())
self.interface.application.output.put("Your basic status is now '%s'" % service.status.basic)
self.interface.application.publish()
self.print_prompt()
def _set_extended_status(self):
extended_status_menu = ExtendedStatusMenu(self.interface)
self.interface.add_menu(extended_status_menu)
def _set_note(self):
service = self.interface.application.service
if service.notes:
a_note = next(iter(service.notes))
self.interface.application.output.put('Current note: %s' % a_note)
note = self.interface.getstring('Set note (press return to delete)')
if note == '':
if service.notes:
service.notes.clear()
self.interface.application.output.put('Note removed')
else:
service.notes.add(PIDFNote(note, lang='en'))
self.interface.application.output.put('Note added')
self.interface.application.publish()
self.print_prompt()
key_bindings = {'s': KeyBinding(description='show PIDF', handler=_show_pidf),
'm': KeyBinding(description='set mood information', handler=_set_mood_info),
'a': KeyBinding(description='set activities information', handler=_set_activity_info),
'b': KeyBinding(description='toggle basic status', handler=_toggle_basic),
'e': KeyBinding(description='set extended status', handler=_set_extended_status),
'n': KeyBinding(description='set note', handler=_set_note),
'q': KeyBinding(description='quit program', handler=Menu._exit_program)}
class UserInterface(Thread):
def __init__(self, application):
Thread.__init__(self)
self.application = application
self.daemon = True
self.menu_stack = deque([TopLevelMenu(self)])
self._old_terminal_settings = None
def run(self):
self.menu_stack[-1].print_prompt()
notification_center = NotificationCenter()
while True:
for char in self._getchars():
menu = self.menu_stack[-1]
if char == '\x04':
self.application.stop()
return
elif char in menu.key_bindings:
menu.key_bindings[char].handler(menu)
else:
notification_center.post_notification('SAInputWasReceived', sender=self, data=NotificationData(input=char))
def stop(self):
self._termios_restore()
def add_menu(self, menu):
self.menu_stack.append(menu)
menu.print_prompt()
def show_top_level(self):
main = self.menu_stack[0]
self.menu_stack.clear()
self.menu_stack.append(main)
main.print_prompt()
def exit_menu(self):
if len(self.menu_stack) > 1:
self.menu_stack.pop()
self.menu_stack[-1].print_prompt()
def getstring(self, prompt='selection'):
self.application.output.put(Prompt(prompt))
return sys.stdin.readline().strip()
def _termios_restore(self):
if self._old_terminal_settings is not None:
termios.tcsetattr(sys.stdin.fileno(), termios.TCSADRAIN, self._old_terminal_settings)
def _getchars(self):
fd = sys.stdin.fileno()
if os.isatty(fd):
self._old_terminal_settings = termios.tcgetattr(fd)
new = termios.tcgetattr(fd)
new[3] = new[3] & ~termios.ICANON & ~termios.ECHO
new[6][termios.VMIN] = b'\000'
try:
termios.tcsetattr(fd, termios.TCSADRAIN, new)
if select.select([fd], [], [], None)[0]:
return sys.stdin.read(4192)
finally:
self._termios_restore()
else:
return os.read(fd, 4192)
@implementer(IObserver)
class PublicationApplication(object):
def __init__(self, account_name, trace_sip, trace_pjsip, trace_notifications):
self.account_name = account_name
self.interface = UserInterface(self)
self.output = EventQueue(self._output_handler)
self.logger = Logger(sip_to_stdout=trace_sip, pjsip_to_stdout=trace_pjsip, notifications_to_stdout=trace_notifications)
self.lookup = DNSLookup()
self.publication_lock = RLock()
self.success = False
self.account = None
self.publication = None
self.pidf = None
self.service = None
self.person = None
self.device = None
self.stopping = False
self.publishing = False
self._publication_routes = None
self._publication_timeout = 0.0
self._publication_wait = 0.5
account_manager = AccountManager()
engine = Engine()
notification_center = NotificationCenter()
notification_center.add_observer(self, sender=account_manager)
notification_center.add_observer(self, sender=engine)
notification_center.add_observer(self, sender=self.interface)
notification_center.add_observer(self, sender=self.lookup)
log.level.current = log.level.WARNING
def run(self):
account_manager = AccountManager()
configuration = ConfigurationManager()
engine = Engine()
notification_center = NotificationCenter()
# start output thread
self.output.start()
# startup configuration
Account.register_extension(AccountExtension)
BonjourAccount.register_extension(AccountExtension)
SIPSimpleSettings.register_extension(SIPSimpleSettingsExtension)
SIPApplication.storage = FileStorage(config_directory)
try:
configuration.start()
except ConfigurationError as e:
raise RuntimeError("Failed to load sipclient's configuration: %s\nIf an old configuration file is in place, delete it or move it and recreate the configuration using the sip_settings script." % str(e))
account_manager.load()
if self.account_name is None:
self.account = account_manager.default_account
else:
possible_accounts = [account for account in account_manager.iter_accounts() if self.account_name in account.id and account.enabled]
if len(possible_accounts) > 1:
raise RuntimeError("More than one account exists which matches %s: %s" % (self.account_name, ", ".join(sorted(account.id for account in possible_accounts))))
if len(possible_accounts) == 0:
raise RuntimeError("No enabled account that matches %s was found. Available and enabled accounts: %s" % (self.account_name, ", ".join(sorted(account.id for account in account_manager.get_accounts() if account.enabled))))
self.account = possible_accounts[0]
if self.account is None:
raise RuntimeError("Unknown account %s. Available enabled accounts: %s" % (self.account_name, ', '.join(sorted(account.id for account in account_manager.iter_accounts() if account.enabled))))
elif self.account == BonjourAccount():
raise RuntimeError("Cannot use bonjour account to publish presence information")
elif not self.account.presence.enabled:
raise RuntimeError("Presence is not enabled for account %s" % self.account.id)
for account in account_manager.iter_accounts():
if account == self.account:
account.sip.register = False
else:
account.enabled = False
self.output.put('Using account %s' % self.account.id)
settings = SIPSimpleSettings()
# start logging
self.logger.start()
# start the engine
engine.start(
udp_port=settings.sip.udp_port if "udp" in settings.sip.transport_list else None,
tcp_port=settings.sip.tcp_port if "tcp" in settings.sip.transport_list else None,
tls_port=settings.sip.tls_port if "tls" in settings.sip.transport_list else None,
- tls_verify_server=self.account.tls.verify_server,
- tls_ca_file=os.path.expanduser(settings.tls.ca_list) if settings.tls.ca_list else None,
- tls_cert_file=os.path.expanduser(self.account.tls.certificate) if self.account.tls.certificate else None,
- tls_privkey_file=os.path.expanduser(self.account.tls.certificate) if self.account.tls.certificate else None,
+ tls_verify_server = settings.tls.verify_server,
+ tls_ca_file = os.path.expanduser(settings.tls.ca_list) if settings.tls.ca_list else None,
+ tls_cert_file = os.path.expanduser(settings.tls.certificate) if settings.tls.certificate else None,
+ tls_privkey_file=os.path.expanduser(settings.tls.certificate) if settings.tls.certificate else None,
user_agent=settings.user_agent,
sample_rate=settings.audio.sample_rate,
rtp_port_range=(settings.rtp.port_range.start, settings.rtp.port_range.end),
trace_sip=settings.logs.trace_sip or self.logger.sip_to_stdout,
log_level=settings.logs.pjsip_level if (settings.logs.trace_pjsip or self.logger.pjsip_to_stdout) else 0
)
# initialize pidf
self.pidf = PIDF(entity=self.account.id) # entity will be determined when account is selected
# initialize top level elements
device_id = settings.instance_id
self.service = Service("ID-"+str(uuid.UUID(hex=settings.instance_id)))
self.service.status = Status(basic='open')
self.service.status.extended = 'available'
contact = "sip:%s;gr=%s" % (self.account.id, device_id)
self.service.contact = Contact(contact)
self.service.contact.priority = 0
self.service.device_id = device_id
self.service.device_info = DeviceInfo(device_id)
self.service.timestamp = ServiceTimestamp(datetime.now())
self.pidf.add(self.service)
self.person = Person("ID-"+hashlib.md5(self.account.id.encode()).hexdigest())
self.person.time_offset = TimeOffset()
self.person.timestamp = PersonTimestamp(datetime.now())
self.pidf.add(self.person)
self.device = Device(''.join(chr(random.randint(97, 122)) for i in range(8)))
self.device.device_id = device_id
self.device.notes.add(DMNote('Powered by %s' % settings.user_agent, lang='en'))
self.device.timestamp = DeviceTimestamp(datetime.now())
self.pidf.add(self.device)
# start the interface thread
self.interface.start()
# initialize publication object
self.publication = Publication(FromHeader(self.account.uri, self.account.display_name), "presence", "application/pidf+xml",
credentials=self.account.credentials, duration=self.account.sip.publish_interval)
notification_center.add_observer(self, sender=self.publication)
reactor.callLater(0, self.publish)
# start twisted
try:
reactor.run()
finally:
self.interface.stop()
# stop the output
self.output.stop()
self.output.join()
self.logger.stop()
return 0 if self.success else 1
def stop(self):
self.stopping = True
if self.publication is not None:
self.service.status.basic = 'closed'
self.service.status.extended = 'offline'
self.publish()
else:
engine = Engine()
engine.stop()
def print_help(self):
message = 'Available control keys:\n'
message += ' t: toggle SIP trace on the console\n'
message += ' j: toggle PJSIP trace on the console\n'
message += ' n: toggle notifications trace on the console\n'
message += ' Ctrl-d: quit the program\n'
message += ' ?: display this help message\n'
self.output.put('\n'+message)
def publish(self):
with self.publication_lock:
if self.publishing:
return
settings = SIPSimpleSettings()
self._publication_timeout = time() + 30
if self.account.sip.outbound_proxy is not None:
uri = SIPURI(host=self.account.sip.outbound_proxy.host, port=self.account.sip.outbound_proxy.port, parameters={'transport': self.account.sip.outbound_proxy.transport})
else:
uri = SIPURI(host=self.account.id.domain)
self.lookup.lookup_sip_proxy(uri, settings.sip.transport_list, tls_name=self.account.sip.tls_name)
self.publishing = True
def unpublish(self):
try:
self.publication.end(timeout=1)
return
except PublicationError:
pass
engine = Engine()
engine.stop()
def handle_notification(self, notification):
handler = getattr(self, '_NH_%s' % notification.name, None)
if handler is not None:
handler(notification)
def _NH_SIPPublicationDidSucceed(self, notification):
with self.publication_lock:
self._publication_routes = None
self._publication_wait = 0.5
self.success = True
self.publishing = False
if self.stopping:
self.unpublish()
return
def _NH_SIPPublicationDidFail(self, notification):
with self.publication_lock:
self.success = False
self.output.put('Publishing failed: %d %s' % (notification.data.code, notification.data.reason))
if notification.data.code in (401, 403, 407):
self.publishing = False
self.stop()
else:
if self.stopping:
self.unpublish()
return
if not self._publication_routes or time() > self._publication_timeout:
self._publication_wait = min(self._publication_wait*2, 30)
timeout = random.uniform(self._publication_wait, 2*self._publication_wait)
reactor.callFromThread(reactor.callLater, timeout, self.publish)
self.publishing = False
else:
route = self._publication_routes.popleft()
self._do_publish(route)
def _NH_SIPPublicationWillExpire(self, notification):
# For now, just re-publish the whole document instead of sending a refresh
self.publish()
def _NH_SIPPublicationDidNotEnd(self, notification):
self.success = False
engine = Engine()
engine.stop()
def _NH_SIPPublicationDidEnd(self, notification):
if notification.data.expired:
self.output.put('Publication expired')
else:
self.output.put('Unpublished')
if self.stopping:
self.success = True
engine = Engine()
engine.stop()
else:
self.publish()
def _NH_DNSLookupDidSucceed(self, notification):
with self.publication_lock:
self._publication_routes = deque(notification.data.result)
route = self._publication_routes.popleft()
self._do_publish(route)
def _NH_DNSLookupDidFail(self, notification):
with self.publication_lock:
self.output.put('DNS lookup failed: %s' % notification.data.error)
timeout = random.uniform(1.0, 2.0)
reactor.callLater(timeout, self.publish)
self.publishing = False
def _NH_SAInputWasReceived(self, notification):
engine = Engine()
settings = SIPSimpleSettings()
key = notification.data.input
if key == 't':
self.logger.sip_to_stdout = not self.logger.sip_to_stdout
engine.trace_sip = self.logger.sip_to_stdout or settings.logs.trace_sip
self.output.put('SIP tracing to console is now %s.' % ('activated' if self.logger.sip_to_stdout else 'deactivated'))
elif key == 'j':
self.logger.pjsip_to_stdout = not self.logger.pjsip_to_stdout
engine.log_level = settings.logs.pjsip_level if (self.logger.pjsip_to_stdout or settings.logs.trace_pjsip) else 0
self.output.put('PJSIP tracing to console is now %s.' % ('activated' if self.logger.pjsip_to_stdout else 'deactivated'))
elif key == 'n':
self.logger.notifications_to_stdout = not self.logger.notifications_to_stdout
self.output.put('Notification tracing to console is now %s.' % ('activated' if self.logger.notifications_to_stdout else 'deactivated'))
elif key == '?':
self.print_help()
@run_in_twisted_thread
def _NH_SIPEngineDidEnd(self, notification):
self._stop_reactor()
@run_in_twisted_thread
def _NH_SIPEngineDidFail(self, notification):
self.output.put('Engine failed.')
self._stop_reactor()
def _NH_SIPEngineGotException(self, notification):
self.output.put('An exception occured within the SIP core:\n'+notification.data.traceback)
def _stop_reactor(self):
try:
reactor.stop()
except ReactorNotRunning:
pass
def _do_publish(self, route):
try:
route_header = RouteHeader(route.uri)
self.publication.publish(self.pidf.toxml(), route_header, timeout=5)
except BuilderError as e:
self.output.put("PIDF as currently defined is invalid: %s" % str(e))
self.publishing = False
except PJSIPError as e:
self.output.put("Publish failed: %s" % str(e))
except:
traceback.print_exc()
self.publishing = False
def _output_handler(self, event):
event = event.decode() if isinstance(event, bytes) else event
if isinstance(event, Prompt):
sys.stdout.write(event+'> ')
sys.stdout.flush()
else:
sys.stdout.write(event+'\n')
if __name__ == "__main__":
description = "This script publishes the presence event package to a SIP Presence Agent for the given SIP account, the presence information can be changed using a menu-driven interface."
usage = "%prog [options]"
parser = OptionParser(usage=usage, description=description)
parser.print_usage = parser.print_help
parser.add_option("-a", "--account-name", type="string", dest="account_name", help="The name of the account to use.")
parser.add_option("-s", "--trace-sip", action="store_true", dest="trace_sip", default=False, help="Dump the raw contents of incoming and outgoing SIP messages (disabled by default).")
parser.add_option("-j", "--trace-pjsip", action="store_true", dest="trace_pjsip", default=False, help="Print PJSIP logging output (disabled by default).")
parser.add_option("-n", "--trace-notifications", action="store_true", dest="trace_notifications", default=False, help="Print all notifications (disabled by default).")
options, args = parser.parse_args()
try:
application = PublicationApplication(options.account_name, options.trace_sip, options.trace_pjsip, options.trace_notifications)
return_code = application.run()
except RuntimeError as e:
print("Error: %s" % str(e))
sys.exit(1)
except SIPCoreError as e:
print("Error: %s" % str(e))
sys.exit(1)
else:
sys.exit(return_code)
diff --git a/sip-register3 b/sip-register3
index 2b2fe40..f457de3 100755
--- a/sip-register3
+++ b/sip-register3
@@ -1,390 +1,381 @@
#!/usr/bin/env python3
import atexit
import os
import select
import signal
import sys
import termios
from datetime import datetime
from optparse import OptionParser
from threading import Thread
from time import sleep
from application import log
from application.notification import NotificationCenter, NotificationData
from application.python.queue import EventQueue
from sipsimple.account import Account, AccountManager, BonjourAccount
from sipsimple.application import SIPApplication
from sipsimple.configuration import ConfigurationError
from sipsimple.configuration.settings import SIPSimpleSettings
from sipsimple.core import Engine
from sipsimple.storage import FileStorage
from sipclient.configuration import config_directory
from sipclient.configuration.account import AccountExtension
from sipclient.configuration.settings import SIPSimpleSettingsExtension
from sipclient.log import Logger
from sipclient.system import IPAddressMonitor, 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.daemon = True
self._old_terminal_settings = None
def start(self):
atexit.register(self._termios_restore)
Thread.start(self)
def run(self):
notification_center = NotificationCenter()
while True:
for char in self._getchars():
notification_center.post_notification('SIPApplicationGotInput', sender=self, data=NotificationData(input=char))
def stop(self):
self._termios_restore()
def _termios_restore(self):
if self._old_terminal_settings is not None:
termios.tcsetattr(sys.stdin.fileno(), termios.TCSADRAIN, self._old_terminal_settings)
def _getchars(self):
fd = sys.stdin.fileno()
if os.isatty(fd):
self._old_terminal_settings = termios.tcgetattr(fd)
new = termios.tcgetattr(fd)
new[3] = new[3] & ~termios.ICANON & ~termios.ECHO
new[6][termios.VMIN] = b'\000'
try:
termios.tcsetattr(fd, termios.TCSADRAIN, new)
if select.select([fd], [], [], None)[0]:
return sys.stdin.read(4192)
finally:
self._termios_restore()
else:
return os.read(fd, 4192)
class RegistrationApplication(SIPApplication):
def __init__(self):
self.account = None
self.options = None
self.input = None
self.output = None
self.ip_address_monitor = IPAddressMonitor()
self.logger = None
self.max_registers = None
self.neighbours = {}
self.success = False
def _write(self, message):
sys.stdout.write(message)
sys.stdout.flush()
def start(self, options):
notification_center = NotificationCenter()
self.options = options
self.max_registers = options.max_registers if options.max_registers > 0 else None
self.input = InputThread() if not options.batch_mode else None
self.output = EventQueue(self._write)
self.logger = Logger(sip_to_stdout=options.trace_sip, pjsip_to_stdout=options.trace_pjsip, notifications_to_stdout=options.trace_notifications)
notification_center.add_observer(self, sender=self)
notification_center.add_observer(self, sender=self.input)
notification_center.add_observer(self, name='SIPSessionNewIncoming')
notification_center.add_observer(self, name='DNSLookupDidSucceed')
if self.input:
self.input.start()
self.output.start()
log.level.current = log.level.WARNING # get rid of twisted messages
Account.register_extension(AccountExtension)
BonjourAccount.register_extension(AccountExtension)
SIPSimpleSettings.register_extension(SIPSimpleSettingsExtension)
try:
SIPApplication.start(self, FileStorage(options.config_directory or config_directory))
except ConfigurationError as e:
self.output.put("Failed to load sipclient's configuration: %s\n" % str(e))
self.output.put("If an old configuration file is in place, delete it or move it and recreate the configuration using the sip_settings script.\n")
self.output.stop()
def print_help(self):
message = 'Available control keys:\n'
message += ' s: toggle SIP trace on the console\n'
message += ' j: toggle PJSIP trace on the console\n'
message += ' n: toggle notifications trace on the console\n'
message += ' Ctrl-d: quit the program\n'
message += ' ?: display this help message\n'
self.output.put('\n'+message)
def _NH_SIPApplicationWillStart(self, notification):
account_manager = AccountManager()
notification_center = NotificationCenter()
settings = SIPSimpleSettings()
if self.options.account is None:
self.account = account_manager.default_account
else:
possible_accounts = [account for account in account_manager.iter_accounts() if self.options.account in account.id and account.enabled]
if len(possible_accounts) > 1:
self.output.put('More than one account exists which matches %s: %s\n' % (self.options.account, ', '.join(sorted(account.id for account in possible_accounts))))
self.output.stop()
self.stop()
return
elif len(possible_accounts) == 0:
self.output.put('No enabled account that matches %s was found. Available and enabled accounts: %s\n' % (self.options.account, ', '.join(sorted(account.id for account in account_manager.get_accounts() if account.enabled))))
self.output.stop()
self.stop()
return
else:
self.account = possible_accounts[0]
for account in account_manager.iter_accounts():
if account is self.account:
if isinstance(account, Account):
account.sip.register = True
account.message_summary.enabled = False
account.presence.enabled = False
account.xcap.enabled = False
account.enabled = True
else:
account.enabled = False
self.output.put('Using account %s\n' % self.account.id)
notification_center.add_observer(self, sender=self.account)
if self.account is BonjourAccount() and self.max_registers is not None:
if self.max_registers == 1:
- self.max_registers = len([transport for transport in settings.sip.transport_list if (transport!='tls' or self.account.tls.certificate is not None)])
+ self.max_registers = len([transport for transport in settings.sip.transport_list if (transport!='tls' or settings.tls.certificate is not None)])
else:
self.output.put('--max-registers option only accepts 0 or 1 if using Bonjour account\n')
self.output.stop()
self.stop()
return
# start logging
self.logger.start()
if settings.logs.trace_sip and self.logger._siptrace_filename is not None:
self.output.put('Logging SIP trace to file "%s"\n' % self.logger._siptrace_filename)
if settings.logs.trace_pjsip and self.logger._pjsiptrace_filename is not None:
self.output.put('Logging PJSIP trace to file "%s"\n' % self.logger._pjsiptrace_filename)
if settings.logs.trace_notifications and self.logger._notifications_filename is not None:
self.output.put('Logging notifications trace to file "%s"\n' % self.logger._notifications_filename)
def _NH_SIPApplicationDidStart(self, notification):
engine = Engine()
settings = SIPSimpleSettings()
engine.trace_sip = self.logger.sip_to_stdout or settings.logs.trace_sip
engine.log_level = settings.logs.pjsip_level if (self.logger.pjsip_to_stdout or settings.logs.trace_pjsip) else 0
if settings.tls.ca_list is None:
self.output.put('Initializing default TLS certificates and settings')
copy_default_certificates()
settings.tls.ca_list = os.path.join(config_directory, 'tls/ca.crt')
settings.tls.verify_server = True
settings.save()
- if isinstance(self.account, Account):
- self.account.tls.ca_list = os.path.join(config_directory, 'tls/ca.crt')
- self.account.tls.verify_server = True
- self.account.save()
-
- if self.account.tls.certificate is None and isinstance(self.account, BonjourAccount):
- self.account.tls.certificate = os.path.join(config_directory, 'tls/default.crt')
- self.account.save()
-
self.ip_address_monitor.start()
if self.account is not BonjourAccount() and self.max_registers != 1 and not self.options.batch_mode:
self.print_help()
elif self.account is BonjourAccount() and self.max_registers is None:
self.print_help()
def _NH_SIPApplicationWillEnd(self, notification):
self.ip_address_monitor.stop()
def _NH_SIPApplicationDidEnd(self, notification):
if self.input:
self.input.stop()
self.output.stop()
self.output.join()
def _NH_DNSLookupDidSucceed(self, notification):
notification_center = NotificationCenter()
targets = {}
for result in notification.data.result:
try:
ports = targets[result.address]
except KeyError:
targets[result.address] = set()
port = '%s-%s' % (result.port, result.transport.upper())
targets[result.address].add(port)
result_text = ''
i = 1
for t in list(targets.keys()):
result_text = result_text + ' ' + str(i) + ') ' + t + ': '
result_text = result_text + ' '.join(sorted(list(targets[t])))
i = i + 1
self.output.put("\n%s DNS lookup for %s succeeded:%s\n" % (datetime.now().replace(microsecond=0), self.account.id.domain, result_text))
def _NH_SIPApplicationGotInput(self, notification):
engine = Engine()
settings = SIPSimpleSettings()
key = notification.data.input
if key == '\x04':
self.stop()
elif key == 's':
self.logger.sip_to_stdout = not self.logger.sip_to_stdout
engine.trace_sip = self.logger.sip_to_stdout or settings.logs.trace_sip
self.output.put('SIP tracing to console is now %s.\n' % ('activated' if self.logger.sip_to_stdout else 'deactivated'))
elif key == 'j':
self.logger.pjsip_to_stdout = not self.logger.pjsip_to_stdout
engine.log_level = settings.logs.pjsip_level if (self.logger.pjsip_to_stdout or settings.logs.trace_pjsip) else 0
self.output.put('PJSIP tracing to console is now %s.\n' % ('activated' if self.logger.pjsip_to_stdout else 'deactivated'))
elif key == 'n':
self.logger.notifications_to_stdout = not self.logger.notifications_to_stdout
self.output.put('Notification tracing to console is now %s.\n' % ('activated' if self.logger.notifications_to_stdout else 'deactivated'))
elif key == '?':
self.print_help()
def _NH_SIPAccountRegistrationDidSucceed(self, notification):
contact_header = notification.data.contact_header
contact_header_list = notification.data.contact_header_list
expires = notification.data.expires
registrar = notification.data.registrar
if not self.success:
message = '%s Registered contact "%s" at %s:%d;transport=%s for %d seconds\n' % (datetime.now().replace(microsecond=0), contact_header.uri, registrar.address, registrar.port, registrar.transport, expires)
if notification.sender.contact.public_gruu:
message += '%s Public GRUU: "%s"\n' % (datetime.now().replace(microsecond=0), notification.sender.contact.public_gruu)
if notification.sender.contact.temporary_gruu:
message += '%s Temporary GRUU: "%s"\n' % (datetime.now().replace(microsecond=0), notification.sender.contact.temporary_gruu)
if len(contact_header_list) > 1:
message += 'Other registered contacts:\n%s\n' % '\n'.join([' %s (expires in %s seconds)' % (str(other_contact_header.uri), other_contact_header.expires) for other_contact_header in contact_header_list if other_contact_header.uri != notification.data.contact_header.uri])
self.output.put(message)
self.success = True
else:
self.output.put('%s Refreshed contact "%s" at %s:%d;transport=%s for %d seconds\n' % (datetime.now().replace(microsecond=0), contact_header.uri, registrar.address, registrar.port, registrar.transport, expires))
if self.max_registers is not None:
self.max_registers -= 1
if self.max_registers == 0:
self.stop()
def _NH_SIPAccountRegistrationGotAnswer(self, notification):
if notification.data.code >= 300:
registrar = notification.data.registrar
code = notification.data.code
reason = notification.data.reason.decode() if isinstance(notification.data.reason, bytes) else notification.data.reason
self.output.put('%s Registration failed at %s:%d;transport=%s: %d %s\n' % (datetime.now().replace(microsecond=0), registrar.address, registrar.port, registrar.transport, code, reason))
def _NH_SIPAccountRegistrationDidFail(self, notification):
reason = notification.data.error.decode() if isinstance(notification.data.error, bytes) else notification.data.error
self.success = False
if self.max_registers is not None:
self.max_registers -= 1
if self.max_registers == 0:
self.output.put('%s Failed to register contact for sip:%s: %s\n' % (datetime.now().replace(microsecond=0), self.account.id, reason))
self.stop()
else:
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, reason, notification.data.retry_after))
else:
self.output.put('%s Failed to register contact for sip:%s: %s\n' % (datetime.now().replace(microsecond=0), self.account.id, reason))
self.stop()
def _NH_SIPAccountRegistrationDidEnd(self, notification):
self.output.put('%s Registration ended.\n' % datetime.now().replace(microsecond=0))
def _NH_BonjourAccountRegistrationDidSucceed(self, notification):
self.output.put('%s Registered Bonjour contact "%s"\n' % (datetime.now().replace(microsecond=0), notification.data.name))
if self.max_registers is not None:
self.max_registers -= 1
if self.max_registers == 0:
self.stop()
def _NH_BonjourAccountRegistrationDidFail(self, notification):
self.output.put('%s Failed to register Bonjour contact: %s\n' % (datetime.now().replace(microsecond=0), notification.data.reason))
if self.max_registers is not None:
self.max_registers -= 1
if self.max_registers == 0:
self.stop()
def _NH_BonjourAccountRegistrationDidEnd(self, notification):
self.output.put('%s Registration ended.\n' % datetime.now().replace(microsecond=0))
def _NH_BonjourAccountDidAddNeighbour(self, notification):
neighbour, record = notification.data.neighbour, notification.data.record
now = datetime.now().replace(microsecond=0)
self.output.put('%s Discovered Bonjour neighbour: "%s (%s)" <%s>\n' % (now, record.name, record.host, record.uri))
self.neighbours[neighbour] = BonjourNeighbour(neighbour, record.uri, record.name, record.host)
def _NH_BonjourAccountDidUpdateNeighbour(self, notification):
neighbour, record = notification.data.neighbour, notification.data.record
now = datetime.now().replace(microsecond=0)
try:
bonjour_neighbour = self.neighbours[neighbour]
except KeyError:
self.output.put('%s Discovered Bonjour neighbour: "%s (%s)" <%s>\n' % (now, record.name, record.host, record.uri))
self.neighbours[neighbour] = BonjourNeighbour(neighbour, record.uri, record.name, record.host)
else:
self.output.put('%s Updated Bonjour neighbour: "%s (%s)" <%s>\n' % (now, record.name, record.host, record.uri))
bonjour_neighbour.display_name = record.name
bonjour_neighbour.host = record.host
bonjour_neighbour.uri = record.uri
def _NH_BonjourAccountDidRemoveNeighbour(self, notification):
neighbour = notification.data.neighbour
now = datetime.now().replace(microsecond=0)
try:
bonjour_neighbour = self.neighbours.pop(neighbour)
except KeyError:
pass
else:
self.output.put('%s Bonjour neighbour left: "%s (%s)" <%s>\n' % (now, bonjour_neighbour.display_name, bonjour_neighbour.host, bonjour_neighbour.uri))
def _NH_SIPEngineGotException(self, notification):
self.output.put('%s An exception occured within the SIP core:\n%s\n' % (datetime.now().replace(microsecond=0), notification.data.traceback))
if __name__ == "__main__":
description = 'This script registers the contact address of the given SIP account to the SIP registrar and refresh it while the program is running. When Ctrl+D is pressed it will unregister.'
usage = '%prog [options]'
parser = OptionParser(usage=usage, description=description)
parser.print_usage = parser.print_help
parser.add_option('-a', '--account', type='string', dest='account', help='The name of the account to use. If not supplied, the default account will be used.', metavar='NAME')
parser.add_option('-c', '--config-directory', type='string', dest='config_directory', help='The configuration directory to use. This overrides the default location.')
parser.add_option('-s', '--trace-sip', action='store_true', dest='trace_sip', default=False, help='Dump the raw contents of incoming and outgoing SIP messages (disabled by default).')
parser.add_option('-j', '--trace-pjsip', action='store_true', dest='trace_pjsip', default=False, help='Print PJSIP logging output (disabled by default).')
parser.add_option('-n', '--trace-notifications', action='store_true', dest='trace_notifications', default=False, help='Print all notifications (disabled by default).')
parser.add_option('-r', '--max-registers', type='int', dest='max_registers', default=1, help='Max number of REGISTERs sent (default 1, set to 0 for infinite).')
parser.add_option('-b', '--batch', action='store_true', dest='batch_mode', default=False, help='Run the program in batch mode: reading input from the console is disabled. This is particularly useful when running this script in a non-interactive environment.')
options, args = parser.parse_args()
application = RegistrationApplication()
application.start(options)
signal.signal(signal.SIGINT, signal.SIG_DFL)
application.output.join()
sleep(0.1)
sys.exit(0 if application.success else 1)
diff --git a/sip-session3 b/sip-session3
index addb482..1200a76 100755
--- a/sip-session3
+++ b/sip-session3
@@ -1,3136 +1,3127 @@
#!/usr/bin/env python3
import os
import re
import signal
import sys
import urllib.request, urllib.parse, urllib.error
import random
import requests
from collections import defaultdict
from datetime import datetime, timedelta
from dateutil.tz import tzlocal
from itertools import chain
from lxml import html
from optparse import OptionParser
from threading import Event, Thread
from time import sleep
from application import log
from application.notification import IObserver, NotificationCenter
from application.python.queue import EventQueue
from application.python import Null
from eventlib import api
from twisted.internet import reactor
from zope.interface import 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
from sipsimple.session import IllegalStateError, Session
from sipsimple.streams import MediaStreamRegistry
from sipsimple.streams.msrp.filetransfer import FileSelector
from sipsimple.streams.msrp.chat import CPIMPayload, SimplePayload, CPIMParserError, ChatIdentity, OTREncryption
from sipsimple.storage import FileStorage
from sipsimple.threading.green import run_in_green_thread
from sipsimple.util import ISOTimestamp
from sipclient.configuration import config_directory
from sipclient.configuration.account import AccountExtension
from sipclient.configuration.datatypes import ResourcePath
from sipclient.configuration.settings import SIPSimpleSettingsExtension
from sipclient.log import Logger
from sipclient.system import IPAddressMonitor, 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, use_cpim=False):
self.account = account
self.target = target
self.routes = None
self.use_cpim = use_cpim
self.msg_id = 0
self.started = False
self.msg_map = {}
self.route = None
self.ended = False
self.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})
elif isinstance(self.account, Account) and self.account.sip.always_use_my_proxy:
uri = SIPURI(host=self.account.id.domain)
else:
uri = self.target_uri
lookup.lookup_sip_proxy(uri, settings.sip.transport_list, tls_name=self.account.sip.tls_name if self.account is not BonjourAccount() else None)
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:
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()
def send_message(self, message):
if not self.routes:
self.start()
self.msg_id = self.msg_id + 1
if not isinstance(message, OTRInternalMessage):
messageObject = QueuedMessage(self.msg_id , message)
self.message_queue.put(messageObject)
else:
self.message_queue.put(message)
return self.msg_id
def _send_message(self, message):
if not self.routes:
return
if not self.route:
self.route = self.routes.pop(0)
identity = str(self.account.uri)
if self.account.display_name:
identity = '"%s" <%s>' % (self.account.display_name, identity)
if isinstance(message.content, str):
charset = 'utf8'
message.content = message.content.encode(charset)
else:
charset = None
what_type = None
if not isinstance(message, OTRInternalMessage):
try:
message.content = self.encryption.otr_session.handle_output(message.content, message.content_type)
except OTRError as e:
if 'has ended the private conversation' in str(e):
show_notice('Encryption has been disabled by remote party, please resend the message again')
self.encryption.stop()
else:
show_notice('Failed to encrypt outgoing message: %s' % str(e))
return
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()
if self.use_cpim:
content_type = 'message/cpim'
payload = CPIMPayload(message.content,
'text/plain',
charset='utf-8',
sender=ChatIdentity(self.account.uri, self.account.display_name),
recipients=[ChatIdentity(self.remote_uri, None)])
else:
payload = SimplePayload(message.content, message.content_type, charset)
content, content_type = payload.encode()
message_request = Message(FromHeader(self.account.uri, self.account.display_name),
ToHeader(self.target_uri),
RouteHeader(self.route.uri),
content_type,
content,
credentials=self.account.credentials)
self.notification_center.add_observer(self, sender=message_request)
self.msg_map[message_request] = message
message_request.send(15)
call_id = message_request._request.call_id.decode()
message.call_id = call_id
if message.id != 'OTR':
if '?OTR:' in content.decode():
show_notice('Encrypted message %s sent to %s' % (call_id, self.route.uri))
message.encrypted = True
else:
show_notice('Message %s sent to %s' % (call_id, self.route.uri))
else:
show_notice('OTR message %s sent' % call_id)
def handle_incoming(self, from_header, content_type, data, call_id):
self.msg_id = self.msg_id + 1
if content_type == 'message/cpim':
try:
payload = CPIMPayload.decode(data)
except CPIMParserError as e:
show_notice('CPIM parse error: %s' % str(e))
return None
else:
if payload.sender:
identity = '%s@%s' % (payload.sender.uri.user.decode(), payload.sender.uri.host.decode())
if from_header.uri == cpim_message.sender.uri:
if payload.sender.display_name:
identity = '"%s" <%s>' % (payload.sender.display_name, identity)
elif from_header.display_name:
identity = '"%s" <%s>' % (from_header.display_name, identity)
else:
identity = from_header
else:
payload = SimplePayload.decode(data, content_type)
from_header = FromHeader.new(from_header)
identity = '%s@%s' % (from_header.uri.user.decode(), from_header.uri.host.decode())
content = payload.content
content_type = payload.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[notification.sender]
except KeyError:
message = None
else:
del(self.msg_map[notification.sender])
if not message:
return
if message.id in (None, 'OTR'):
return
if notification.data.code == 202:
show_notice('%s Message %s to %s will be delivered later by the server' % (datetime.now().replace(microsecond=0), message.id, self.remote_uri))
elif notification.data.code == 200:
show_notice('%s Message %s received by %s' % (datetime.now().replace(microsecond=0), message.id, self.remote_uri))
else:
show_notice('%s Message %s received by %s (code %d)' % (datetime.now().replace(microsecond=0), message.id, self.remote_uri, notification.data.code ))
def _NH_SIPMessageDidFail(self, notification):
self.notification_center.remove_observer(self, sender=notification.sender)
try:
message = self.msg_map[notification.sender]
except KeyError:
message = None
else:
del(self.msg_map[notification.sender])
if not message:
return
if message.id in (None, 'OTR'):
return
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):
self.account = account
self.target = target
self.streams = []
if video:
self.streams.append(MediaStreamRegistry.VideoStream())
if audio:
self.streams.append(MediaStreamRegistry.AudioStream())
if chat:
self.streams.append(MediaStreamRegistry.ChatStream())
self.wave_ringtone = None
def start(self):
if isinstance(self.account, BonjourAccount) and '@' not in self.target:
show_notice('Bonjour mode requires a host in the destination address')
return
if '@' not in self.target:
self.target = '%s@%s' % (self.target, self.account.id.domain)
if not self.target.startswith('sip:') and not self.target.startswith('sips:'):
self.target = 'sip:' + self.target
try:
self.target = SIPURI.parse(self.target)
except SIPCoreError:
show_notice('Illegal SIP URI: %s' % self.target)
else:
if '.' not in self.target.host.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})
elif isinstance(self.account, Account) and self.account.sip.always_use_my_proxy:
uri = SIPURI(host=self.account.id.domain)
else:
uri = self.target
show_notice('DNS lookup for %s' % uri)
lookup.lookup_sip_proxy(uri, settings.sip.transport_list, tls_name=self.account.sip.tls_name if self.account is not BonjourAccount() else None)
def handle_notification(self, notification):
handler = getattr(self, '_NH_%s' % notification.name, Null)
handler(notification)
def _NH_DNSLookupDidSucceed(self, notification):
notification_center = NotificationCenter()
notification_center.remove_observer(self, sender=notification.sender)
session = Session(self.account)
notification_center.add_observer(self, sender=session)
session.connect(ToHeader(self.target), routes=notification.data.result, streams=self.streams)
application = SIPSessionApplication()
application.outgoing_session = session
def _NH_DNSLookupDidFail(self, notification):
show_notice('Call to %s failed: DNS lookup error: %s' % (self.target, notification.data.error))
notification_center = NotificationCenter()
notification_center.remove_observer(self, sender=notification.sender)
def _NH_SIPSessionNewOutgoing(self, notification):
session = notification.sender
local_identity = str(session.local_identity.uri)
if session.local_identity.display_name:
local_identity = '"%s" <%s>' % (session.local_identity.display_name, local_identity)
remote_identity = str(session.remote_identity.uri)
if session.remote_identity.display_name:
remote_identity = '"%s" <%s>' % (session.remote_identity.display_name, remote_identity)
show_notice("Initiating SIP session from %s to %s via %s..." % (local_identity, remote_identity, session.route))
self.message_session_to = None
def _NH_SIPSessionGotRingIndication(self, notification):
settings = SIPSimpleSettings()
ui = UI()
ringtone = settings.sounds.audio_outbound
if ringtone and self.wave_ringtone is None:
self.wave_ringtone = WavePlayer(SIPApplication.voice_audio_mixer, ringtone.path.normalized, volume=ringtone.volume, loop_count=0, pause_time=2)
SIPApplication.voice_audio_bridge.add(self.wave_ringtone)
self.wave_ringtone.start()
ui.status = 'Ringing...'
def _NH_SIPSessionWillStart(self, notification):
ui = UI()
if self.wave_ringtone:
self.wave_ringtone.stop()
SIPApplication.voice_audio_bridge.remove(self.wave_ringtone)
self.wave_ringtone = None
ui.status = 'Connecting...'
def _NH_SIPSessionDidStart(self, notification):
notification_center = NotificationCenter()
ui = UI()
session = notification.sender
notification_center.remove_observer(self, sender=session)
ui.status = 'Connected'
reactor.callLater(2, setattr, ui, 'status', None)
application = SIPSessionApplication()
application.outgoing_session = None
for stream in notification.data.streams:
if stream.type 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_SIPSessionDidFail(self, notification):
notification_center = NotificationCenter()
session = notification.sender
notification_center.remove_observer(self, sender=session)
ui = UI()
ui.status = None
application = SIPSessionApplication()
application.outgoing_session = None
if self.wave_ringtone:
self.wave_ringtone.stop()
SIPApplication.voice_audio_bridge.remove(self.wave_ringtone)
self.wave_ringtone = None
if notification.data.failure_reason == 'user request' and notification.data.code == 487:
show_notice('SIP session cancelled')
elif notification.data.failure_reason == 'user request':
show_notice('SIP session rejected by user (%d %s)' % (notification.data.code, notification.data.reason))
else:
show_notice('SIP session failed: %s' % notification.data.failure_reason)
@implementer(IObserver)
class IncomingCallInitializer(object):
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()
notification_center.remove_observer(self, sender=notification.sender)
answer = notification.data.answer
self.question = None
if answer == 'a':
self.session.accept(self.session.proposed_streams)
ui.status = 'Accepting...'
elif answer == 'r':
self.session.reject()
ui.status = 'Rejecting...'
elif answer == 'b':
self.session.reject(486)
ui.status = 'Sending Busy Here...'
if self.wave_ringtone:
self.wave_ringtone.stop()
self.wave_ringtone = None
if IncomingCallInitializer.sessions > 1:
if IncomingCallInitializer.tone_ringtone is None:
IncomingCallInitializer.tone_ringtone = WavePlayer(SIPApplication.voice_audio_mixer, ResourcePath('sounds/ring_tone.wav').normalized, loop_count=0, pause_time=6)
SIPApplication.voice_audio_bridge.add(IncomingCallInitializer.tone_ringtone)
IncomingCallInitializer.tone_ringtone.start()
elif IncomingCallInitializer.tone_ringtone:
IncomingCallInitializer.tone_ringtone.stop()
IncomingCallInitializer.tone_ringtone = None
if self.answer_timer is not None and self.answer_timer.active():
self.answer_timer.cancel()
def _NH_SIPSessionWillStart(self, notification):
ui = UI()
if self.question is not None:
notification_center = NotificationCenter()
notification_center.remove_observer(self, sender=self.question)
ui.remove_question(self.question)
self.question = None
ui.status = 'Connecting...'
def _NH_SIPSessionDidStart(self, notification):
notification_center = NotificationCenter()
session = notification.sender
notification_center.remove_observer(self, sender=session)
IncomingCallInitializer.sessions -= 1
ui = UI()
ui.status = 'Connected'
reactor.callLater(2, setattr, ui, 'status', None)
identity = str(session.remote_identity.uri)
if session.remote_identity.display_name:
identity = '"%s" <%s>' % (session.remote_identity.display_name, identity)
show_notice("SIP session with %s established" % identity)
for stream in notification.data.streams:
if stream.type 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})
elif isinstance(self.account, Account) and self.account.sip.always_use_my_proxy:
uri = SIPURI(host=self.account.id.domain)
else:
uri = self.target
lookup.lookup_sip_proxy(uri, settings.sip.transport_list, tls_name=self.account.sip.tls_name if self.account is not BonjourAccount() else None)
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 = 'Rejecting...'
if IncomingTransferHandler.sessions == 1:
if self.wave_ringtone:
self.wave_ringtone.stop()
self.wave_ringtone = None
if IncomingTransferHandler.tone_ringtone:
IncomingTransferHandler.tone_ringtone.stop()
IncomingTransferHandler.tone_ringtone = None
if self.answer_timer is not None and self.answer_timer.active():
self.answer_timer.cancel()
def _NH_SIPSessionWillStart(self, notification):
ui = UI()
if self.question is not None:
notification_center = NotificationCenter()
notification_center.remove_observer(self, sender=self.question)
ui.remove_question(self.question)
self.question = None
ui.status = 'Connecting...'
notification_center = NotificationCenter()
notification_center.add_observer(self, sender=notification.sender.proposed_streams[0])
def _NH_SIPSessionDidStart(self, notification):
session = notification.sender
IncomingCallInitializer.sessions -= 1
ui = UI()
ui.status = 'File transfer connected'
identity = str(session.remote_identity.uri)
if session.remote_identity.display_name:
identity = '"%s" <%s>' % (session.remote_identity.display_name, identity)
show_notice("File transfer for %s with %s started" % (os.path.basename(self.filename), identity))
if IncomingTransferHandler.sessions == 1:
if self.wave_ringtone:
self.wave_ringtone.stop()
self.wave_ringtone = None
if IncomingTransferHandler.tone_ringtone:
IncomingTransferHandler.tone_ringtone.stop()
IncomingTransferHandler.tone_ringtone = None
def _NH_SIPSessionDidFail(self, notification):
IncomingTransferHandler.sessions -= 1
if self.wave_ringtone:
self.wave_ringtone.stop()
self.wave_ringtone = None
if IncomingTransferHandler.sessions == 0 and IncomingTransferHandler.tone_ringtone is not None:
IncomingTransferHandler.tone_ringtone.stop()
IncomingTransferHandler.tone_ringtone = None
def _NH_MediaStreamDidNotInitialize(self, notification):
self._terminate(failure_reason=notification.data.reason)
def _NH_FileTransferHandlerDidInitialize(self, notification):
self.filename = self.stream.file_selector.name
def _NH_FileTransferHandlerProgress(self, notification):
ui = UI()
ui.status = '%s: %s%%' % (os.path.basename(self.filename), notification.data.transferred_bytes*100//notification.data.total_bytes)
def _NH_FileTransferHandlerDidEnd(self, notification):
reactor.callFromThread(reactor.callLater, 0, self.session.end)
self._terminate(failure_reason=notification.data.reason)
class SIPSessionApplication(SIPApplication):
# public methods
#
def __init__(self):
self.account = None
self.options = None
self.target = None
self.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.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
def start(self, target, options, filepath=None):
notification_center = NotificationCenter()
ui = UI()
history_file = os.path.join(config_directory, 'input.history')
ui.input.add_history(history_file)
self.options = options
self.target = target
self.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')
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(AccountExtension)
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()
for account in account_manager.iter_accounts():
if isinstance(account, Account):
account.sip.register = False
account.presence.enabled = False
account.xcap.enabled = False
account.message_summary.enabled = False
notification_center.add_observer(self, sender=account)
if self.options.account is None:
self.account = account_manager.default_account
else:
possible_accounts = [account for account in account_manager.iter_accounts() if self.options.account in account.id and account.enabled]
if len(possible_accounts) > 1:
show_notice('More than one account exists which matches %s: %s' % (self.options.account, ', '.join(sorted(account.id for account in possible_accounts))), bold=False)
self.stop()
return
elif len(possible_accounts) == 0:
show_notice('No enabled account which matches %s was found. Available and enabled accounts: %s' % (self.options.account, ', '.join(sorted(account.id for account in account_manager.get_accounts() if account.enabled))), bold=False)
self.stop()
return
else:
self.account = possible_accounts[0]
if isinstance(self.account, Account):
self.account.sip.register = True
show_notice('Using account %s' % self.account.id, bold=False)
ui.prompt = Prompt(self.account.id, foreground='default')
self.logger.start()
if settings.logs.trace_sip and self.logger._siptrace_filename is not None:
show_notice('Logging SIP trace to file "%s"' % self.logger._siptrace_filename, bold=False)
if settings.logs.trace_msrp and self.logger._msrptrace_filename is not None:
show_notice('Logging MSRP trace to file "%s"' % self.logger._msrptrace_filename, bold=False)
if settings.logs.trace_pjsip and self.logger._pjsiptrace_filename is not None:
show_notice('Logging PJSIP trace to file "%s"' % self.logger._pjsiptrace_filename, bold=False)
if settings.logs.trace_notifications and self.logger._notifications_filename is not None:
show_notice('Logging notifications trace to file "%s"' % self.logger._notifications_filename, bold=False)
if self.options.disable_sound:
settings.audio.input_device = None
settings.audio.output_device = None
settings.audio.alert_device = None
def _NH_SIPApplicationDidStart(self, notification):
settings = SIPSimpleSettings()
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()
# 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.verify_server = True
settings.save()
- if isinstance(self.account, Account):
- self.account.tls.ca_list = os.path.join(config_directory, 'tls/ca.crt')
- self.account.tls.verify_server = True
- self.account.save()
-
- if self.account.tls.certificate is None and isinstance(self.account, BonjourAccount):
- self.account.tls.certificate = os.path.join(config_directory, 'tls/default.crt')
- self.account.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 _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 _NH_UIInputGotText(self, notification):
msrp_chat = None
message_text = notification.data.text
local_identity = '%s@%s' % (self.account.id.username, self.account.id.domain)
encrypted = False
if self.active_session is not None:
msrp_chat = next((stream for stream in self.active_session.streams if stream.type == 'chat'), None)
if msrp_chat:
msrp_chat.send_message(message_text)
if msrp_chat.local_identity.display_name:
local_identity = msrp_chat.local_identity.display_name
else:
local_identity = str(msrp_chat.local_identity.uri)
else:
# compose SMS message
pass
elif self.message_session_to:
# compose SMS message
message_session = self.message_session(self.message_session_to)
msg_id = message_session.send_message(message_text)
message_text = message_text + ' (%s)' % msg_id
if message_session.encryption.active:
local_identity = '%s encrypted text sent to %s' % (local_identity, self.message_session_to)
else:
local_identity = '%s clear text sent to %s' % (local_identity, self.message_session_to)
else:
show_notice('No active chat or message session')
return
ui = UI()
ui.write(RichText('%s> ' % local_identity, foreground='darkred') + message_text)
def message_session(self, recipient, 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:
self.received_message_ids.remove(call_id)
except KeyError:
self.received_message_ids.add(call_id)
else:
# drop duplicate message received
return
content_type = data.content_type
if content_type not in ('text/plain', 'text/html', 'message/cpim', 'application/im-iscomposing+xml'):
return
account = AccountManager().find_account(data.request_uri)
is_composing = False
from_header = FromHeader.new(data.from_header)
identity = '%s@%s' % (from_header.uri.user.decode(), from_header.uri.host.decode())
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
message_session = self.message_session(identity, account=account, route=route)
parsed_payload = message_session.handle_incoming(data.from_header, content_type, data.body, call_id)
if parsed_payload is None:
return
(content_type, content, identity, msg_id, encrypted) = parsed_payload
if content_type == 'application/im-iscomposing+xml':
# body must not be utf-8 decoded
msg = IsComposingMessage.parse(content)
state = msg.state.value
refresh = msg.refresh.value if msg.refresh is not None else None
content_type = msg.content_type.value if msg.content_type is not None else None
last_active = msg.last_active.value if msg.last_active is not None else None
sender_identity = data.from_header
#show_notice('Is composing state=%s, refresh=%s, last_active=%s' % (state, refresh, last_active))
if state == 'active':
if refresh is None:
refresh = 120
if last_active is not None and (last_active - ISOTimestamp.now() > timedelta(seconds=refresh)):
# message is old, discard it
return
elif state == 'idle':
return
is_composing = True
if content_type != 'application/im-iscomposing+xml':
self.message_session_to = identity
if from_header.display_name:
identity = '"%s" <%s>' % (from_header.display_name, identity)
content = content.decode()
if is_composing:
ui = UI()
ui.status = "%s is composing a message" % identity
reactor.callLater(2, 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:
show_notice("%s %s wrote: %s (%s)" % (datetime.now().replace(microsecond=0), identity, content, msg_id))
if not encrypted and message_session.encryption.active and content_type != 'application/im-iscomposing+xml':
show_notice("%s %s stopped the encryption" % (datetime.now().replace(microsecond=0), identity))
message_session.encryption.stop()
if content.startswith('?OTR:'):
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)]
hangup_tone = WavePlayer(self.voice_audio_mixer, ResourcePath('sounds/hangup_tone.wav').normalized)
notification_center.add_observer(self, sender=hangup_tone)
self.voice_audio_bridge.add(hangup_tone)
hangup_tone.start()
def _NH_SIPSessionDidEnd(self, notification):
notification_center = NotificationCenter()
session = notification.sender
notification_center.discard_observer(self, sender=session)
for stream in session.streams or session.proposed_streams or []:
notification_center.remove_observer(self, sender=stream)
ui = UI()
ui.status = None
identity = str(session.remote_identity.uri)
if session.remote_identity.display_name:
identity = '"%s" <%s>' % (session.remote_identity.display_name, identity)
if notification.data.end_reason == 'user request':
show_notice('SIP session with %s ended by %s party' % (identity, notification.data.originator))
else:
show_notice('SIP session with %s ended due to error: %s' % (identity, notification.data.end_reason))
if session.start_time:
duration = session.end_time - session.start_time
seconds = duration.seconds if duration.microseconds < 500000 else duration.seconds+1
minutes, seconds = seconds / 60, seconds % 60
hours, minutes = minutes / 60, minutes % 60
hours += duration.days*24
if not minutes and not hours:
duration_text = '%d seconds' % seconds
elif not hours:
duration_text = '%02d:%02d' % (minutes, seconds)
else:
duration_text = '%02d:%02d:%02d' % (hours, minutes, seconds)
else:
duration_text = '0s'
show_notice('Session duration was %s' % duration_text)
if session in self.connected_sessions:
self.connected_sessions.remove(session)
if session is self.active_session:
if self.connected_sessions:
self.active_session = self.connected_sessions[0]
self.message_session_to = None
self.active_session.unhold()
self.ignore_local_unhold = True
identity = str(self.active_session.remote_identity.uri)
if self.active_session.remote_identity.display_name:
identity = '"%s" <%s>' % (self.active_session.remote_identity.display_name, identity)
show_notice('Active SIP session: "%s" (%d/%d)' % (identity, self.connected_sessions.index(self.active_session)+1, len(self.connected_sessions)))
else:
self.active_session = None
self._update_prompt()
if session is self.outgoing_session:
self.outgoing_session = None
on_hold_streams = [stream for stream in chain(*(session.streams for session in self.connected_sessions)) if stream.on_hold]
if not on_hold_streams and self.hold_tone is not None:
self.hold_tone.stop()
self.hold_tone = None
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 ('+', '-')]
if 'sip' in add_types or ('sip' in toggle_types and not self.logger.sip_to_stdout):
self.logger.sip_to_stdout = 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
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
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
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
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
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
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
show_notice('Notification tracing to console is now deactivated')
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)
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)
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)
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)
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)
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)
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 audio input 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('-s', '--trace-sip', action='store_true', dest='trace_sip', default=False, help='Dump the raw contents of incoming and outgoing SIP messages.')
parser.add_option('-m', '--trace-msrp', action='store_true', dest='trace_msrp', default=False, help='Dump msrp logging information and the raw contents of incoming and outgoing MSRP messages.')
parser.add_option('-j', '--trace-pjsip', action='store_true', dest='trace_pjsip', default=False, help='Print PJSIP logging output.')
parser.add_option('-n', '--trace-notifications', action='store_true', dest='trace_notifications', default=False, help='Print all notifications (disabled by default).')
parser.add_option('-S', '--disable-sound', action='store_true', dest='disable_sound', default=False, help='Disables initializing the sound card.')
parser.set_default('auto_answer_interval', None)
parser.add_option('--auto-answer', action='callback', callback=parse_handle_call_option, callback_args=('auto_answer_interval',), help='Interval after which to answer an incoming session (disabled by default). If the option is specified but the interval is not, it defaults to 0 (accept the session as soon as it starts ringing).', metavar='[INTERVAL]')
parser.set_default('auto_hangup_interval', None)
parser.add_option('--auto-hangup', action='callback', callback=parse_handle_call_option, callback_args=('auto_hangup_interval',), help='Interval after which to hang up an established session (disabled by default). If the option is specified but the interval is not, it defaults to 0 (hangup the session as soon as it connects).', metavar='[INTERVAL]')
options, args = parser.parse_args()
target = args[0] if args else None
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/sip-subscribe-conference3 b/sip-subscribe-conference3
index 776f191..a0decfc 100755
--- a/sip-subscribe-conference3
+++ b/sip-subscribe-conference3
@@ -1,632 +1,632 @@
#!/usr/bin/env python3
import datetime
import os
import random
import select
import sys
import termios
import urllib.request, urllib.parse, urllib.error
from itertools import chain
from collections import deque
from optparse import OptionParser
from threading import Thread
from time import time
from application import log
from application.notification import IObserver, NotificationCenter, NotificationData
from application.python.queue import EventQueue
from eventlib.twistedutil import join_reactor
from twisted.internet import reactor
from twisted.internet.error import ReactorNotRunning
from zope.interface import implementer
from sipsimple.account import Account, AccountManager, BonjourAccount
from sipsimple.application import SIPApplication
from sipsimple.lookup import DNSLookup, DNSManager
from sipsimple.configuration import ConfigurationError, ConfigurationManager
from sipsimple.configuration.settings import SIPSimpleSettings
from sipsimple.core import ContactHeader, Engine, FromHeader, RouteHeader, SIPCoreError, SIPURI, Subscription, ToHeader, Route, PJSIPError
from sipsimple.payloads import ParserError
from sipsimple.payloads.conference import ConferenceDocument
from sipsimple.storage import FileStorage
from sipsimple.threading import run_in_twisted_thread
from sipclient.configuration import config_directory
from sipclient.configuration.account import AccountExtension
from sipclient.configuration.settings import SIPSimpleSettingsExtension
from sipclient.log import Logger
class InputThread(Thread):
def __init__(self, application):
Thread.__init__(self)
self.application = application
self.daemon = True
self._old_terminal_settings = None
def run(self):
notification_center = NotificationCenter()
while True:
for char in self._getchars():
if char == "\x04":
self.application.stop()
return
else:
notification_center.post_notification('SAInputWasReceived', sender=self, data=NotificationData(input=char))
def stop(self):
self._termios_restore()
def _termios_restore(self):
if self._old_terminal_settings is not None:
termios.tcsetattr(sys.stdin.fileno(), termios.TCSADRAIN, self._old_terminal_settings)
def _getchars(self):
fd = sys.stdin.fileno()
if os.isatty(fd):
self._old_terminal_settings = termios.tcgetattr(fd)
new = termios.tcgetattr(fd)
new[3] = new[3] & ~termios.ICANON & ~termios.ECHO
new[6][termios.VMIN] = b'\000'
try:
termios.tcsetattr(fd, termios.TCSADRAIN, new)
if select.select([fd], [], [], None)[0]:
return sys.stdin.read(4192)
finally:
self._termios_restore()
else:
return os.read(fd, 4192)
@implementer(IObserver)
class SubscriptionApplication(object):
def __init__(self, account_name, target, trace_sip, trace_pjsip, trace_notifications):
self.account_name = account_name
self.target = target
self.input = InputThread(self)
self.output = EventQueue(self._write)
self.logger = Logger(sip_to_stdout=trace_sip, pjsip_to_stdout=trace_pjsip, notifications_to_stdout=trace_notifications)
self.success = False
self.account = None
self.subscription = None
self.stopping = False
self._subscription_routes = None
self._subscription_timeout = 0.0
self._subscription_wait = 0.5
account_manager = AccountManager()
engine = Engine()
notification_center = NotificationCenter()
notification_center.add_observer(self, sender=account_manager)
notification_center.add_observer(self, sender=engine)
notification_center.add_observer(self, sender=self.input)
log.level.current = log.level.WARNING
def _write(self, message):
if isinstance(message, str):
message = message.encode(sys.getfilesystemencoding())
sys.stdout.write(message.decode()+'\n')
def run(self):
account_manager = AccountManager()
configuration = ConfigurationManager()
self.engine = Engine()
# start output thread
self.output.start()
# startup configuration
Account.register_extension(AccountExtension)
BonjourAccount.register_extension(AccountExtension)
SIPSimpleSettings.register_extension(SIPSimpleSettingsExtension)
SIPApplication.storage = FileStorage(config_directory)
try:
configuration.start()
except ConfigurationError as e:
raise RuntimeError("failed to load sipclient's configuration: %s\nIf an old configuration file is in place, delete it or move it and recreate the configuration using the sip_settings script." % str(e))
account_manager.load()
if self.account_name is None:
self.account = account_manager.default_account
else:
possible_accounts = [account for account in account_manager.iter_accounts() if self.account_name in account.id and account.enabled]
if len(possible_accounts) > 1:
raise RuntimeError("More than one account exists which matches %s: %s" % (self.account_name, ", ".join(sorted(account.id for account in possible_accounts))))
if len(possible_accounts) == 0:
raise RuntimeError("No enabled account that matches %s was found. Available and enabled accounts: %s" % (self.account_name, ", ".join(sorted(account.id for account in account_manager.get_accounts() if account.enabled))))
self.account = possible_accounts[0]
if self.account is None:
raise RuntimeError("Unknown account %s. Available accounts: %s" % (self.account_name, ', '.join(account.id for account in account_manager.iter_accounts())))
for account in account_manager.iter_accounts():
if account == self.account:
account.sip.register = False
else:
account.enabled = False
self.output.put('Using account %s' % self.account.id)
settings = SIPSimpleSettings()
# start logging
self.logger.start()
# start the engine
self.engine.start(
auto_sound=False,
events={"conference": ["application/conference-info+xml"],},
udp_port=settings.sip.udp_port if "udp" in settings.sip.transport_list else None,
tcp_port=settings.sip.tcp_port if "tcp" in settings.sip.transport_list else None,
tls_port=settings.sip.tls_port if "tls" in settings.sip.transport_list else None,
- tls_verify_server=self.account.tls.verify_server,
+ tls_verify_server=settings.tls.verify_server,
tls_ca_file=os.path.expanduser(settings.tls.ca_list) if settings.tls.ca_list else None,
- tls_cert_file=os.path.expanduser(self.account.tls.certificate) if self.account.tls.certificate else None,
- tls_privkey_file=os.path.expanduser(self.account.tls.certificate) if self.account.tls.certificate else None,
+ tls_cert_file=os.path.expanduser(settings.tls.certificate) if settings.tls.certificate else None,
+ tls_privkey_file=os.path.expanduser(settings.tls.certificate) if settings.tls.certificate else None,
user_agent=settings.user_agent,
sample_rate=settings.audio.sample_rate,
rtp_port_range=(settings.rtp.port_range.start, settings.rtp.port_range.end),
trace_sip=settings.logs.trace_sip or self.logger.sip_to_stdout,
log_level=settings.logs.pjsip_level if (settings.logs.trace_pjsip or self.logger.pjsip_to_stdout) else 0
)
if self.target is None:
self.target = ToHeader(SIPURI(user=self.account.id.username, host=self.account.id.domain))
else:
if '@' not in self.target:
self.target = '%s@%s' % (self.target, self.account.id.domain)
if not self.target.startswith('sip:') and not self.target.startswith('sips:'):
self.target = 'sip:' + self.target
try:
self.target = ToHeader(SIPURI.parse(self.target))
except SIPCoreError:
self.output.put('Illegal SIP URI: %s' % self.target)
self.engine.stop()
return 1
self.output.put('Subscribing to %s for the conference event' % self.target.uri)
# start the input thread
self.input.start()
# start twisted
try:
reactor.run()
finally:
self.input.stop()
# stop the output
self.output.stop()
self.output.join()
self.logger.stop()
return 0 if self.success else 1
def stop(self):
self.stopping = True
if self.subscription is not None and self.subscription.state.lower() in ('accepted', 'pending', 'active'):
self.subscription.end(timeout=1)
else:
self.engine.stop()
def print_help(self):
message = 'Available control keys:\n'
message += ' t: toggle SIP trace on the console\n'
message += ' j: toggle PJSIP trace on the console\n'
message += ' n: toggle notifications trace on the console\n'
message += ' Ctrl-d: quit the program\n'
message += ' ?: display this help message\n'
self.output.put('\n'+message)
def handle_notification(self, notification):
handler = getattr(self, '_NH_%s' % notification.name, None)
if handler is not None:
handler(notification)
def _NH_SIPEngineDidStart(self, notification):
dns_manager = DNSManager()
self.engine.set_nameservers(dns_manager.nameservers)
self._subscribe()
def _NH_SIPSubscriptionDidStart(self, notification):
route = Route(notification.sender.route_header.uri.host, notification.sender.route_header.uri.port, notification.sender.route_header.uri.parameters.get('transport', 'udp'))
self._subscription_routes = None
self._subscription_wait = 0.5
self.output.put('Subscription succeeded at %s:%d;transport=%s' % (route.address, route.port, route.transport))
self.success = True
def _NH_SIPSubscriptionChangedState(self, notification):
route = Route(notification.sender.route_header.uri.host, notification.sender.route_header.uri.port, notification.sender.route_header.uri.parameters.get('transport', 'udp'))
if notification.data.state.lower() == "pending":
self.output.put('Subscription pending at %s:%d;transport=%s' % (route.address, route.port, route.transport))
elif notification.data.state.lower() == "active":
self.output.put('Subscription active at %s:%d;transport=%s' % (route.address, route.port, route.transport))
def _NH_SIPSubscriptionDidEnd(self, notification):
notification_center = NotificationCenter()
notification_center.remove_observer(self, sender=notification.sender)
self.subscription = None
route = Route(notification.sender.route_header.uri.host, notification.sender.route_header.uri.port, notification.sender.route_header.uri.parameters.get('transport', 'udp'))
self.output.put('Unsubscribed from %s:%d;transport=%s' % (route.address, route.port, route.transport))
self.stop()
def _NH_SIPSubscriptionDidFail(self, notification):
notification_center = NotificationCenter()
notification_center.remove_observer(self, sender=notification.sender)
self.subscription = None
route = Route(notification.sender.route_header.uri.host, notification.sender.route_header.uri.port, notification.sender.route_header.uri.parameters.get('transport', 'udp'))
if notification.data.code:
status = ': %d %s' % (notification.data.code, notification.data.reason)
else:
status = ': %s' % notification.data.reason
self.output.put('Subscription failed at %s:%d;transport=%s%s' % (route.address, route.port, route.transport, status))
if self.stopping or notification.data.code in (401, 403, 407) or self.success:
self.success = False
self.stop()
else:
if not self._subscription_routes or time() > self._subscription_timeout:
self._subscription_wait = min(self._subscription_wait*2, 30)
timeout = random.uniform(self._subscription_wait, 2*self._subscription_wait)
reactor.callFromThread(reactor.callLater, timeout, self._subscribe)
else:
route = self._subscription_routes.popleft()
route_header = RouteHeader(route.uri)
self.subscription = Subscription(self.target.uri,
FromHeader(self.account.uri, self.account.display_name),
self.target,
ContactHeader(self.account.contact[route]),
b"conference",
route_header,
credentials=self.account.credentials,
refresh=self.account.sip.subscribe_interval)
notification_center.add_observer(self, sender=self.subscription)
self.subscription.subscribe(timeout=5)
def _NH_SIPSubscriptionGotNotify(self, notification):
self.output.put('Received NOTIFY')
if notification.data.event == 'conference' and notification.data.body:
try:
conference_info = ConferenceDocument.parse(notification.data.body)
except ParserError as e:
self.output.put('Cannot parse body: %s' % str(e))
else:
conf_desc = conference_info.conference_description
print('Conference description: %s' % str(conf_desc.display_text))
print('Conference organizer: %s' % str(conference_info.host_info.web_page.value))
print('Conference participants:')
i = 1
for user in conference_info.users:
media = list(str(media.media_type) for media in chain(*user))
print("%d. %s <%s> with %s" % (i, user.display_text.value, user.entity.split(":")[1], ", ".join(media)))
i += 1
def _NH_DNSLookupDidSucceed(self, notification):
# create subscription and register to get notifications from it
self._subscription_routes = deque(notification.data.result)
route = self._subscription_routes.popleft()
route_header = RouteHeader(route.uri)
self.subscription = Subscription(self.target.uri,
FromHeader(self.account.uri, self.account.display_name),
self.target,
ContactHeader(self.account.contact[route]),
b"conference",
route_header,
credentials=self.account.credentials,
refresh=self.account.sip.subscribe_interval)
notification_center = NotificationCenter()
notification_center.add_observer(self, sender=self.subscription)
try:
self.subscription.subscribe(timeout=5)
except PJSIPError as e:
self.output.put('Subscription failed: %s' % str(e))
def _NH_DNSLookupDidFail(self, notification):
self.output.put('DNS lookup failed: %s' % notification.data.error)
timeout = random.uniform(1.0, 2.0)
reactor.callLater(timeout, self._subscribe)
def _NH_SAInputWasReceived(self, notification):
settings = SIPSimpleSettings()
key = notification.data.input
if key == 't':
self.logger.sip_to_stdout = not self.logger.sip_to_stdout
self.engine.trace_sip = self.logger.sip_to_stdout or settings.logs.trace_sip
self.output.put('SIP tracing to console is now %s.' % ('activated' if self.logger.sip_to_stdout else 'deactivated'))
elif key == 'j':
self.logger.pjsip_to_stdout = not self.logger.pjsip_to_stdout
self.engine.log_level = settings.logs.pjsip_level if (self.logger.pjsip_to_stdout or settings.logs.trace_pjsip) else 0
self.output.put('PJSIP tracing to console is now %s.' % ('activated' if self.logger.pjsip_to_stdout else 'deactivated'))
elif key == 'n':
self.logger.notifications_to_stdout = not self.logger.notifications_to_stdout
self.output.put('Notification tracing to console is now %s.' % ('activated' if self.logger.notifications_to_stdout else 'deactivated'))
elif key == '?':
self.print_help()
@run_in_twisted_thread
def _NH_SIPEngineDidEnd(self, notification):
self._stop_reactor()
@run_in_twisted_thread
def _NH_SIPEngineDidFail(self, notification):
self.output.put('Engine failed.')
self._stop_reactor()
def _NH_SIPEngineGotException(self, notification):
self.output.put('An exception occured within the SIP core:\n'+notification.data.traceback)
def _stop_reactor(self):
try:
reactor.stop()
except ReactorNotRunning:
pass
def _subscribe(self):
settings = SIPSimpleSettings()
self._subscription_timeout = time()+30
lookup = DNSLookup()
notification_center = NotificationCenter()
notification_center.add_observer(self, sender=lookup)
if self.account.sip.outbound_proxy is not None:
uri = SIPURI(host=self.account.sip.outbound_proxy.host, port=self.account.sip.outbound_proxy.port, parameters={'transport': self.account.sip.outbound_proxy.transport})
elif self.account.sip.always_use_my_proxy:
uri = SIPURI(host=self.account.id.domain)
else:
uri = self.target.uri
lookup.lookup_sip_proxy(uri, settings.sip.transport_list, tls_name=self.account.sip.tls_name)
def _format_note(self, note):
text = "Note"
if hasattr(note, 'lang') and note.lang is not None:
text += "(%s)" % note.lang
else:
text += ": %s" % note
return text
def _format_person(self, person, pidf):
buf = []
# display class
if person.rpid_class is not None:
buf.append(" Class: %s" % person.rpid_class)
# display timestamp
if person.timestamp is not None:
buf.append(" Timestamp: %s" % person.timestamp)
# display notes
if person.notes:
for note in person.notes:
buf.append(" %s" % self._format_note(note))
elif pidf.notes:
for note in pidf.notes:
buf.append(" %s" % self._format_note(note))
# display activities
if person.activities is not None:
activities = list(person.activities)
if len(activities) > 0:
text = " Activities"
if person.activities.since is not None or person.activities.until is not None:
text += " valid"
if person.activities.since is not None:
text += " from %s" % person.activities.since
if person.activities.until is not None:
text += " until %s" % person.activities.until
text += ": %s" % ', '.join(str(activity) for activity in activities)
buf.append(text)
if len(person.activities.notes) > 0:
for note in person.activities.notes:
buf.append(" %s" % self._format_note(note))
elif len(person.activities.notes) > 0:
buf.append(" Activities")
for note in person.activities.notes:
buf.append(" %s" % self._format_note(note))
# display mood
if person.mood is not None:
moods = list(person.mood)
if len(moods) > 0:
text = " Mood"
if person.mood.since is not None or person.mood.until is not None:
text += " valid"
if person.mood.since is not None:
text += " from %s" % person.mood.since
if person.mood.until is not None:
text += " until %s" % person.mood.until
text += ": %s" % ', '.join(str(mood) for mood in moods)
buf.append(text)
if len(person.mood.notes) > 0:
for note in person.mood.notes:
buf.append(" %s" % self._format_note(note))
# display place is
if person.place_is is not None:
place_info = ', '.join('%s %s' % (key.capitalize(), getattr(person.place_is, key).value) for key in ('audio', 'video', 'text') if getattr(person.place_is, key) and getattr(person.place_is, key).value)
if place_info != '':
buf.append(" Place information: " + place_info)
# display privacy
if person.privacy is not None:
text = " Private conversation possible with: "
private = []
if person.privacy.audio:
private.append("Audio")
if person.privacy.video:
private.append("Video")
if person.privacy.text:
private.append("Text")
if len(private) > 0:
text += ", ".join(private)
else:
text += "None"
buf.append(text)
# display sphere
if person.sphere is not None:
timeinfo = []
if person.sphere.since is not None:
timeinfo.append('from %s' % str(person.sphere.since))
if person.sphere.until is not None:
timeinfo.append('until %s' % str(person.sphere.until))
if len(timeinfo) != 0:
timeinfo = ' (' + ', '.join(timeinfo) + ')'
else:
timeinfo = ''
buf.append(" Current sphere%s: %s" % (timeinfo, person.sphere.value))
# display status icon
if person.status_icon is not None:
buf.append(" Status icon: %s" % person.status_icon)
# display time and time offset
if person.time_offset is not None:
ctime = datetime.datetime.utcnow() + datetime.timedelta(minutes=int(person.time_offset))
time_offset = int(person.time_offset)/60.0
if time_offset == int(time_offset):
offset_info = '(UTC+%d%s)' % (time_offset, (person.time_offset.description is not None and (' (%s)' % person.time_offset.description) or ''))
else:
offset_info = '(UTC+%.1f%s)' % (time_offset, (person.time_offset.description is not None and (' (%s)' % person.time_offset.description) or ''))
buf.append(" Current user time: %s %s" % (ctime.strftime("%H:%M"), offset_info))
# display user input
if person.user_input is not None:
buf.append(" User is %s" % person.user_input)
if person.user_input.last_input:
buf.append(" Last input at: %s" % person.user_input.last_input)
if person.user_input.idle_threshold:
buf.append(" Idle threshold: %s seconds" % person.user_input.idle_threshold)
return buf
def _format_service(self, service, pidf):
buf = []
# display class
if service.rpid_class is not None:
buf.append(" Class: %s" % service.rpid_class)
# display timestamp
if service.timestamp is not None:
buf.append(" Timestamp: %s" % service.timestamp)
# display notes
for note in service.notes:
buf.append(" %s" % self._format_note(note))
# display status
if service.status is not None:
if service.status.basic is not None:
buf.append(" Basic status: %s" % service.status.basic)
if service.status.extended is not None:
buf.append(" Extended status: %s" % service.status.extended)
# display contact
if service.contact is not None:
buf.append(" Contact%s: %s" % ((service.contact.priority is not None) and (' priority %s' % service.contact.priority) or '', urllib.parse.unquote(service.contact.value)))
# display device ID
if service.device_info is not None:
description = " (%s)" % urllib.parse.unquote(service.device_info.description.value).decode('utf-8') if service.device_info.description else ""
buf.append(" Service offered by device: %s%s" % (service.device_info.id, description))
# display relationship
if service.relationship is not None:
buf.append(" Relationship: %s" % service.relationship.value)
# display service-class
if service.service_class is not None:
buf.append(" Service class: %s" % service.service_class.value)
# display status icon
if service.status_icon is not None:
buf.append(" Status icon: %s" % service.status_icon)
# display user input
if service.user_input is not None:
buf.append(" Service is %s" % service.user_input)
if service.user_input.last_input:
buf.append(" Last input at: %s" % service.user_input.last_input)
if service.user_input.idle_threshold:
buf.append(" Idle threshold: %s seconds" % service.user_input.idle_threshold)
return buf
def _format_device(self, device, pidf):
buf = []
# display device ID
if device.device_id is not None:
buf.append(" Device id: %s" % device.device_id)
# display class
if device.rpid_class is not None:
buf.append(" Class: %s" % device.rpid_class)
# display timestamp
if device.timestamp is not None:
buf.append(" Timestamp: %s" % device.timestamp)
# display notes
for note in device.notes:
buf.append(" %s" % self._format_note(note))
# display user input
if device.user_input is not None:
buf.append(" Device is %s" % device.user_input)
if device.user_input.last_input:
buf.append(" Last input at: %s" % device.user_input.last_input)
if device.user_input.idle_threshold:
buf.append(" Idle threshold: %s seconds" % device.user_input.idle_threshold)
return buf
def _display_pidf(self, pidf):
buf = ["-"*16]
buf.append("Presence for %s:" % urllib.parse.unquote(pidf.entity))
persons = {}
devices = {}
services = {}
printed_sep = True
for child in pidf:
if isinstance(child, Person):
persons[child.id] = child
elif isinstance(child, Device):
devices[child.id] = child
elif isinstance(child, Service):
services[child.id] = child
# handle person information
if len(persons) == 0:
if list(pidf.notes):
buf.append(" Person information:")
for note in pidf.notes:
buf.append(" %s" % self._format_note(note))
printed_sep = False
else:
for person in list(persons.values()):
buf.append(" Person: %s" % person.id)
buf.extend(self._format_person(person, pidf))
printed_sep = False
# handle services informaation
if len(services) > 0:
if not printed_sep:
buf.append(" " + "-"*3)
for service in list(services.values()):
buf.append(" Service: %s" % service.id)
buf.extend(self._format_service(service, pidf))
# handle devices informaation
if len(devices) > 0:
if not printed_sep:
buf.append(" " + "-"*3)
for device in list(devices.values()):
buf.append(" Device: %s" % device.id)
buf.extend(self._format_device(device, pidf))
buf.append("-"*16)
# push the data
self.output.put('\n'.join(buf))
if __name__ == "__main__":
description = "This script subscribes to the conference event package published by the specified SIP target. If a SIP target is not specified, it will subscribe to its own address. It will then interprete PIDF bodies contained in NOTIFYs and display their meaning. The program will un-SUBSCRIBE and quit when CTRL+D is pressed."
usage = "%prog [options] [target-user@target-domain.com]"
parser = OptionParser(usage=usage, description=description)
parser.print_usage = parser.print_help
parser.add_option("-a", "--account-name", type="string", dest="account_name", help="The name of the account to use.")
parser.add_option("-s", "--trace-sip", action="store_true", dest="trace_sip", default=False, help="Dump the raw contents of incoming and outgoing SIP messages (disabled by default).")
parser.add_option("-j", "--trace-pjsip", action="store_true", dest="trace_pjsip", default=False, help="Print PJSIP logging output (disabled by default).")
parser.add_option("-n", "--trace-notifications", action="store_true", dest="trace_notifications", default=False, help="Print all notifications (disabled by default).")
options, args = parser.parse_args()
try:
application = SubscriptionApplication(options.account_name, args[0] if args else None, options.trace_sip, options.trace_pjsip, options.trace_notifications)
return_code = application.run()
except RuntimeError as e:
print("Error: %s" % str(e))
sys.exit(1)
except SIPCoreError as e:
print("Error: %s" % str(e))
sys.exit(1)
else:
sys.exit(return_code)
diff --git a/sip-subscribe-mwi3 b/sip-subscribe-mwi3
index 2a898fb..e2697d4 100755
--- a/sip-subscribe-mwi3
+++ b/sip-subscribe-mwi3
@@ -1,381 +1,381 @@
#!/usr/bin/env python3
import os
import random
import select
import sys
import termios
from collections import deque
from optparse import OptionParser
from threading import Thread
from time import time
from application import log
from application.notification import IObserver, NotificationCenter, NotificationData
from application.python.queue import EventQueue
from eventlib.twistedutil import join_reactor
from twisted.internet import reactor
from twisted.internet.error import ReactorNotRunning
from zope.interface import implementer
from sipsimple.account import Account, AccountManager, BonjourAccount
from sipsimple.application import SIPApplication
from sipsimple.configuration import ConfigurationError, ConfigurationManager
from sipsimple.configuration.settings import SIPSimpleSettings
from sipsimple.core import ContactHeader, Engine, FromHeader, Header, Route, RouteHeader, SIPCoreError, SIPURI, Subscription, ToHeader
from sipsimple.lookup import DNSLookup
from sipsimple.payloads.messagesummary import MessageSummary
from sipsimple.storage import FileStorage
from sipsimple.threading import run_in_twisted_thread
from sipclient.configuration import config_directory
from sipclient.configuration.account import AccountExtension
from sipclient.configuration.settings import SIPSimpleSettingsExtension
from sipclient.log import Logger
class InputThread(Thread):
def __init__(self, application):
Thread.__init__(self)
self.application = application
self.daemon = True
self._old_terminal_settings = None
def run(self):
notification_center = NotificationCenter()
while True:
for char in self._getchars():
if char == "\x04":
self.application.stop()
return
else:
notification_center.post_notification('SAInputWasReceived', sender=self, data=NotificationData(input=char))
def stop(self):
self._termios_restore()
def _termios_restore(self):
if self._old_terminal_settings is not None:
termios.tcsetattr(sys.stdin.fileno(), termios.TCSADRAIN, self._old_terminal_settings)
def _getchars(self):
fd = sys.stdin.fileno()
if os.isatty(fd):
self._old_terminal_settings = termios.tcgetattr(fd)
new = termios.tcgetattr(fd)
new[3] = new[3] & ~termios.ICANON & ~termios.ECHO
new[6][termios.VMIN] = b'\000'
try:
termios.tcsetattr(fd, termios.TCSADRAIN, new)
if select.select([fd], [], [], None)[0]:
return sys.stdin.read(4192)
finally:
self._termios_restore()
else:
return os.read(fd, 4192)
@implementer(IObserver)
class SubscriptionApplication(object):
def __init__(self, account_name, target, trace_sip, trace_pjsip, trace_notifications):
self.account_name = account_name
self.target = target
self.input = InputThread(self)
self.output = EventQueue(lambda event: sys.stdout.write(event+'\n'))
self.logger = Logger(sip_to_stdout=trace_sip, pjsip_to_stdout=trace_pjsip, notifications_to_stdout=trace_notifications)
self.success = False
self.account = None
self.subscription = None
self.stopping = False
self._subscription_routes = None
self._subscription_timeout = 0.0
self._subscription_wait = 0.5
account_manager = AccountManager()
engine = Engine()
notification_center = NotificationCenter()
notification_center.add_observer(self, sender=account_manager)
notification_center.add_observer(self, sender=engine)
notification_center.add_observer(self, sender=self.input)
log.level.current = log.level.WARNING
def run(self):
account_manager = AccountManager()
configuration = ConfigurationManager()
engine = Engine()
# start output thread
self.output.start()
# startup configuration
Account.register_extension(AccountExtension)
BonjourAccount.register_extension(AccountExtension)
SIPSimpleSettings.register_extension(SIPSimpleSettingsExtension)
SIPApplication.storage = FileStorage(config_directory)
try:
configuration.start()
except ConfigurationError as e:
raise RuntimeError("Failed to load sipclient's configuration: %s\nIf an old configuration file is in place, delete it or move it and recreate the configuration using the sip_settings script." % str(e))
account_manager.load()
if self.account_name is None:
self.account = account_manager.default_account
else:
possible_accounts = [account for account in account_manager.iter_accounts() if self.account_name in account.id and account.enabled]
if len(possible_accounts) > 1:
raise RuntimeError("More than one account exists which matches %s: %s" % (self.account_name, ", ".join(sorted(account.id for account in possible_accounts))))
if len(possible_accounts) == 0:
raise RuntimeError("No enabled account that matches %s was found. Available and enabled accounts: %s" % (self.account_name, ", ".join(sorted(account.id for account in account_manager.get_accounts() if account.enabled))))
self.account = possible_accounts[0]
if self.account is None:
raise RuntimeError("Unknown account %s. Available accounts: %s" % (self.account_name, ', '.join(account.id for account in account_manager.iter_accounts())))
elif self.account == BonjourAccount():
raise RuntimeError("Cannot use bonjour account for message summary subscription")
elif not self.account.message_summary.enabled:
raise RuntimeError("Message summary is not enabled for account %s" % self.account.id)
for account in account_manager.iter_accounts():
if account == self.account:
account.sip.register = False
else:
account.enabled = False
self.output.put('Using account %s' % self.account.id)
settings = SIPSimpleSettings()
# start logging
self.logger.start()
# start the engine
engine.start(
auto_sound=False,
events={'message-summary': ['application/simple-message-summary']},
udp_port=settings.sip.udp_port if "udp" in settings.sip.transport_list else None,
tcp_port=settings.sip.tcp_port if "tcp" in settings.sip.transport_list else None,
tls_port=settings.sip.tls_port if "tls" in settings.sip.transport_list else None,
- tls_verify_server=self.account.tls.verify_server,
+ tls_verify_server=settings.tls.verify_server,
tls_ca_file=os.path.expanduser(settings.tls.ca_list) if settings.tls.ca_list else None,
- tls_cert_file=os.path.expanduser(self.account.tls.certificate) if self.account.tls.certificate else None,
- tls_privkey_file=os.path.expanduser(self.account.tls.certificate) if self.account.tls.certificate else None,
+ tls_cert_file=os.path.expanduser(settings.tls.certificate) if settings.tls.certificate else None,
+ tls_privkey_file=os.path.expanduser(settings.tls.certificate) if settings.tls.certificate else None,
user_agent=settings.user_agent,
sample_rate=settings.audio.sample_rate,
rtp_port_range=(settings.rtp.port_range.start, settings.rtp.port_range.end),
trace_sip=settings.logs.trace_sip or self.logger.sip_to_stdout,
log_level=settings.logs.pjsip_level if (settings.logs.trace_pjsip or self.logger.pjsip_to_stdout) else 0
)
if self.target is None:
self.target = ToHeader(SIPURI(user=self.account.id.username, host=self.account.id.domain))
else:
if '@' not in self.target:
self.target = '%s@%s' % (self.target, self.account.id.domain)
if not self.target.startswith('sip:') and not self.target.startswith('sips:'):
self.target = 'sip:' + self.target
try:
self.target = ToHeader(SIPURI.parse(self.target))
except SIPCoreError:
self.output.put('Illegal SIP URI: %s' % self.target)
engine.stop()
return 1
self.output.put('Subscribing to %s for the message-summary event' % self.target.uri)
# start the input thread
self.input.start()
# start twisted
try:
reactor.run()
finally:
self.input.stop()
# stop the output
self.output.stop()
self.output.join()
self.logger.stop()
return 0 if self.success else 1
def stop(self):
self.stopping = True
if self.subscription is not None and self.subscription.state.lower() in ('accepted', 'pending', 'active'):
self.subscription.end(timeout=1)
else:
engine = Engine()
engine.stop()
def print_help(self):
message = 'Available control keys:\n'
message += ' t: toggle SIP trace on the console\n'
message += ' j: toggle PJSIP trace on the console\n'
message += ' n: toggle notifications trace on the console\n'
message += ' Ctrl-d: quit the program\n'
message += ' ?: display this help message\n'
self.output.put('\n'+message)
def handle_notification(self, notification):
handler = getattr(self, '_NH_%s' % notification.name, None)
if handler is not None:
handler(notification)
def _NH_SIPSubscriptionDidStart(self, notification):
route = Route(notification.sender.route_header.uri.host, notification.sender.route_header.uri.port, notification.sender.route_header.uri.parameters.get('transport', 'udp'))
self._subscription_routes = None
self._subscription_wait = 0.5
self.output.put('Subscription succeeded at %s:%d;transport=%s' % (route.address, route.port, route.transport))
self.success = True
def _NH_SIPSubscriptionChangedState(self, notification):
route = Route(notification.sender.route_header.uri.host, notification.sender.route_header.uri.port, notification.sender.route_header.uri.parameters.get('transport', 'udp'))
if notification.data.state.lower() == "pending":
self.output.put('Subscription pending at %s:%d;transport=%s' % (route.address, route.port, route.transport))
elif notification.data.state.lower() == "active":
self.output.put('Subscription active at %s:%d;transport=%s' % (route.address, route.port, route.transport))
def _NH_SIPSubscriptionDidEnd(self, notification):
notification_center = NotificationCenter()
notification_center.remove_observer(self, sender=notification.sender)
self.subscription = None
route = Route(notification.sender.route_header.uri.host, notification.sender.route_header.uri.port, notification.sender.route_header.uri.parameters.get('transport', 'udp'))
self.output.put('Unsubscribed from %s:%d;transport=%s' % (route.address, route.port, route.transport))
self.stop()
def _NH_SIPSubscriptionDidFail(self, notification):
notification_center = NotificationCenter()
notification_center.remove_observer(self, sender=notification.sender)
self.subscription = None
route = Route(notification.sender.route_header.uri.host, notification.sender.route_header.uri.port, notification.sender.route_header.uri.parameters.get('transport', 'udp'))
if notification.data.code:
status = ': %d %s' % (notification.data.code, notification.data.reason)
else:
status = ': %s' % notification.data.reason
self.output.put('Subscription failed at %s:%d;transport=%s%s' % (route.address, route.port, route.transport, status))
if self.stopping or notification.data.code in (401, 403, 407) or self.success:
self.success = False
self.stop()
else:
if not self._subscription_routes or time() > self._subscription_timeout:
self._subscription_wait = min(self._subscription_wait*2, 30)
timeout = random.uniform(self._subscription_wait, 2*self._subscription_wait)
reactor.callFromThread(reactor.callLater, timeout, self._subscribe)
else:
route = self._subscription_routes.popleft()
route_header = RouteHeader(route.uri)
self.subscription = Subscription(self.target.uri,
FromHeader(self.account.uri, self.account.display_name),
self.target,
ContactHeader(self.account.contact[route]),
b"message-summary",
route_header,
credentials=self.account.credentials,
refresh=self.account.sip.subscribe_interval)
notification_center.add_observer(self, sender=self.subscription)
self.subscription.subscribe(extra_headers=[Header('Supported', 'eventlist')], timeout=5)
def _NH_SIPSubscriptionGotNotify(self, notification):
if notification.data.body:
ms = MessageSummary.parse(notification.data.body)
self.output.put('\nReceived NOTIFY:\n' + ms.to_string())
self.print_help()
def _NH_DNSLookupDidSucceed(self, notification):
# create subscription and register to get notifications from it
self._subscription_routes = deque(notification.data.result)
route = self._subscription_routes.popleft()
route_header = RouteHeader(route.uri)
self.subscription = Subscription(self.target.uri,
FromHeader(self.account.uri, self.account.display_name),
self.target,
ContactHeader(self.account.contact[route]),
b"message-summary",
route_header,
credentials=self.account.credentials,
refresh=self.account.sip.subscribe_interval)
notification_center = NotificationCenter()
notification_center.add_observer(self, sender=self.subscription)
self.subscription.subscribe(extra_headers=[Header('Supported', 'eventlist')], timeout=5)
def _NH_SIPEngineDidStart(self, notification):
self._subscribe()
def _NH_DNSLookupDidFail(self, notification):
self.output.put('DNS lookup failed: %s' % notification.data.error)
timeout = random.uniform(1.0, 2.0)
reactor.callLater(timeout, self._subscribe)
def _NH_SAInputWasReceived(self, notification):
engine = Engine()
settings = SIPSimpleSettings()
key = notification.data.input
if key == 't':
self.logger.sip_to_stdout = not self.logger.sip_to_stdout
engine.trace_sip = self.logger.sip_to_stdout or settings.logs.trace_sip
self.output.put('SIP tracing to console is now %s.' % ('activated' if self.logger.sip_to_stdout else 'deactivated'))
elif key == 'j':
self.logger.pjsip_to_stdout = not self.logger.pjsip_to_stdout
engine.log_level = settings.logs.pjsip_level if (self.logger.pjsip_to_stdout or settings.logs.trace_pjsip) else 0
self.output.put('PJSIP tracing to console is now %s.' % ('activated' if self.logger.pjsip_to_stdout else 'deactivated'))
elif key == 'n':
self.logger.notifications_to_stdout = not self.logger.notifications_to_stdout
self.output.put('Notification tracing to console is now %s.' % ('activated' if self.logger.notifications_to_stdout else 'deactivated'))
elif key == '?':
self.print_help()
@run_in_twisted_thread
def _NH_SIPEngineDidEnd(self, notification):
self._stop_reactor()
@run_in_twisted_thread
def _NH_SIPEngineDidFail(self, notification):
self.output.put('Engine failed.')
self._stop_reactor()
def _NH_SIPEngineGotException(self, notification):
self.output.put('An exception occured within the SIP core:\n'+notification.data.traceback)
def _stop_reactor(self):
try:
reactor.stop()
except ReactorNotRunning:
pass
def _subscribe(self):
settings = SIPSimpleSettings()
self._subscription_timeout = time()+30
lookup = DNSLookup()
notification_center = NotificationCenter()
notification_center.add_observer(self, sender=lookup)
if self.account.sip.outbound_proxy is not None:
uri = SIPURI(host=self.account.sip.outbound_proxy.host, port=self.account.sip.outbound_proxy.port, parameters={'transport': self.account.sip.outbound_proxy.transport})
elif self.account.sip.always_use_my_proxy:
uri = SIPURI(host=self.account.id.domain)
else:
uri = self.target.uri
lookup.lookup_sip_proxy(uri, settings.sip.transport_list, tls_name=self.account.sip.tls_name)
if __name__ == "__main__":
description = "This script subscribes to the message summary event package for the specified SIP target. When a NOTIFY is received with the message summary information it will be displayed. The program will un-SUBSCRIBE and quit when CTRL+D is pressed."
usage = "%prog [options] [target-user@target-domain.com]"
parser = OptionParser(usage=usage, description=description)
parser.print_usage = parser.print_help
parser.add_option("-a", "--account-name", type="string", dest="account_name", help="The name of the account to use.")
parser.add_option("-s", "--trace-sip", action="store_true", dest="trace_sip", default=False, help="Dump the raw contents of incoming and outgoing SIP messages (disabled by default).")
parser.add_option("-j", "--trace-pjsip", action="store_true", dest="trace_pjsip", default=False, help="Print PJSIP logging output (disabled by default).")
parser.add_option("-n", "--trace-notifications", action="store_true", dest="trace_notifications", default=False, help="Print all notifications (disabled by default).")
options, args = parser.parse_args()
try:
application = SubscriptionApplication(options.account_name, args[0] if args else None, options.trace_sip, options.trace_pjsip, options.trace_notifications)
return_code = application.run()
except RuntimeError as e:
print("Error: %s" % str(e))
sys.exit(1)
except SIPCoreError as e:
print("Error: %s" % str(e))
sys.exit(1)
else:
sys.exit(return_code)
diff --git a/sip-subscribe-presence3 b/sip-subscribe-presence3
index cddc626..045edbb 100755
--- a/sip-subscribe-presence3
+++ b/sip-subscribe-presence3
@@ -1,623 +1,623 @@
#!/usr/bin/env python3
import datetime
import os
import random
import select
import sys
import termios
import urllib.request, urllib.parse, urllib.error
from collections import deque
from optparse import OptionParser
from threading import Thread
from time import time
from application import log
from application.notification import IObserver, NotificationCenter, NotificationData
from application.python.queue import EventQueue
from eventlib.twistedutil import join_reactor
from twisted.internet import reactor
from twisted.internet.error import ReactorNotRunning
from zope.interface import implementer
from sipsimple.account import Account, AccountManager, BonjourAccount
from sipsimple.application import SIPApplication
from sipsimple.lookup import DNSLookup, DNSManager
from sipsimple.configuration import ConfigurationError, ConfigurationManager
from sipsimple.configuration.settings import SIPSimpleSettings
from sipsimple.core import ContactHeader, Engine, FromHeader, RouteHeader, SIPCoreError, SIPURI, Subscription, ToHeader, Route, PJSIPError
from sipsimple.payloads import ParserError
from sipsimple.payloads import rpid # needed to register RPID extensions
from sipsimple.payloads.pidf import Device, Person, Service, PIDF, PIDFDocument
from sipsimple.storage import FileStorage
from sipsimple.threading import run_in_twisted_thread
from sipclient.configuration import config_directory
from sipclient.configuration.account import AccountExtension
from sipclient.configuration.settings import SIPSimpleSettingsExtension
from sipclient.log import Logger
class InputThread(Thread):
def __init__(self, application):
Thread.__init__(self)
self.application = application
self.daemon = True
self._old_terminal_settings = None
def run(self):
notification_center = NotificationCenter()
while True:
for char in self._getchars():
if char == "\x04":
self.application.stop()
return
else:
notification_center.post_notification('SAInputWasReceived', sender=self, data=NotificationData(input=char))
def stop(self):
self._termios_restore()
def _termios_restore(self):
if self._old_terminal_settings is not None:
termios.tcsetattr(sys.stdin.fileno(), termios.TCSADRAIN, self._old_terminal_settings)
def _getchars(self):
fd = sys.stdin.fileno()
if os.isatty(fd):
self._old_terminal_settings = termios.tcgetattr(fd)
new = termios.tcgetattr(fd)
new[3] = new[3] & ~termios.ICANON & ~termios.ECHO
new[6][termios.VMIN] = b'\000'
try:
termios.tcsetattr(fd, termios.TCSADRAIN, new)
if select.select([fd], [], [], None)[0]:
return sys.stdin.read(4192)
finally:
self._termios_restore()
else:
return os.read(fd, 4192)
@implementer(IObserver)
class SubscriptionApplication(object):
def __init__(self, account_name, target, trace_sip, trace_pjsip, trace_notifications):
self.account_name = account_name
self.target = target
self.input = InputThread(self)
self.output = EventQueue(self._write)
self.logger = Logger(sip_to_stdout=trace_sip, pjsip_to_stdout=trace_pjsip, notifications_to_stdout=trace_notifications)
self.success = False
self.account = None
self.subscription = None
self.stopping = False
self._subscription_routes = None
self._subscription_timeout = 0.0
self._subscription_wait = 0.5
account_manager = AccountManager()
engine = Engine()
notification_center = NotificationCenter()
notification_center.add_observer(self, sender=account_manager)
notification_center.add_observer(self, sender=engine)
notification_center.add_observer(self, sender=self.input)
log.level.current = log.level.WARNING
def _write(self, message):
if isinstance(message, str):
message = message.encode(sys.getfilesystemencoding())
sys.stdout.write(message.decode()+'\n')
def run(self):
account_manager = AccountManager()
configuration = ConfigurationManager()
self.engine = Engine()
# start output thread
self.output.start()
# startup configuration
Account.register_extension(AccountExtension)
BonjourAccount.register_extension(AccountExtension)
SIPSimpleSettings.register_extension(SIPSimpleSettingsExtension)
SIPApplication.storage = FileStorage(config_directory)
try:
configuration.start()
except ConfigurationError as e:
raise RuntimeError("failed to load sipclient's configuration: %s\nIf an old configuration file is in place, delete it or move it and recreate the configuration using the sip_settings script." % str(e))
account_manager.load()
if self.account_name is None:
self.account = account_manager.default_account
else:
possible_accounts = [account for account in account_manager.iter_accounts() if self.account_name in account.id and account.enabled]
if len(possible_accounts) > 1:
raise RuntimeError("More than one account exists which matches %s: %s" % (self.account_name, ", ".join(sorted(account.id for account in possible_accounts))))
if len(possible_accounts) == 0:
raise RuntimeError("No enabled account that matches %s was found. Available and enabled accounts: %s" % (self.account_name, ", ".join(sorted(account.id for account in account_manager.get_accounts() if account.enabled))))
self.account = possible_accounts[0]
if self.account is None:
raise RuntimeError("Unknown account %s. Available accounts: %s" % (self.account_name, ', '.join(account.id for account in account_manager.iter_accounts())))
elif self.account == BonjourAccount():
raise RuntimeError("Cannot use bonjour account for presence subscription")
elif not self.account.presence.enabled:
raise RuntimeError("Presence is not enabled for account %s" % self.account.id)
for account in account_manager.iter_accounts():
if account == self.account:
account.sip.register = False
else:
account.enabled = False
self.output.put('Using account %s' % self.account.id)
settings = SIPSimpleSettings()
# start logging
self.logger.start()
# start the engine
self.engine.start(
auto_sound=False,
events={"presence": [PIDFDocument.content_type.encode()]},
udp_port=settings.sip.udp_port if "udp" in settings.sip.transport_list else None,
tcp_port=settings.sip.tcp_port if "tcp" in settings.sip.transport_list else None,
tls_port=settings.sip.tls_port if "tls" in settings.sip.transport_list else None,
- tls_verify_server=self.account.tls.verify_server,
+ tls_verify_server=settings.tls.verify_server,
tls_ca_file=os.path.expanduser(settings.tls.ca_list) if settings.tls.ca_list else None,
- tls_cert_file=os.path.expanduser(self.account.tls.certificate) if self.account.tls.certificate else None,
- tls_privkey_file=os.path.expanduser(self.account.tls.certificate) if self.account.tls.certificate else None,
+ tls_cert_file=os.path.expanduser(settings.tls.certificate) if settings.tls.certificate else None,
+ tls_privkey_file=os.path.expanduser(settings.tls.certificate) if settings.tls.certificate else None,
user_agent=settings.user_agent,
sample_rate=settings.audio.sample_rate,
rtp_port_range=(settings.rtp.port_range.start, settings.rtp.port_range.end),
trace_sip=settings.logs.trace_sip or self.logger.sip_to_stdout,
log_level=settings.logs.pjsip_level if (settings.logs.trace_pjsip or self.logger.pjsip_to_stdout) else 0
)
if self.target is None:
self.target = ToHeader(SIPURI(user=self.account.id.username, host=self.account.id.domain))
else:
if '@' not in self.target:
self.target = '%s@%s' % (self.target, self.account.id.domain)
if not self.target.startswith('sip:') and not self.target.startswith('sips:'):
self.target = 'sip:' + self.target
try:
self.target = ToHeader(SIPURI.parse(self.target))
except SIPCoreError:
self.output.put('Illegal SIP URI: %s' % self.target)
self.engine.stop()
return 1
self.output.put('Subscribing to %s for the presence event' % self.target.uri)
# start the input thread
self.input.start()
# start twisted
try:
reactor.run()
finally:
self.input.stop()
# stop the output
self.output.stop()
self.output.join()
self.logger.stop()
return 0 if self.success else 1
def stop(self):
self.stopping = True
if self.subscription is not None and self.subscription.state.lower() in ('accepted', 'pending', 'active'):
self.subscription.end(timeout=1)
else:
self.engine.stop()
def print_help(self):
message = 'Available control keys:\n'
message += ' t: toggle SIP trace on the console\n'
message += ' j: toggle PJSIP trace on the console\n'
message += ' n: toggle notifications trace on the console\n'
message += ' Ctrl-d: quit the program\n'
message += ' ?: display this help message\n'
self.output.put('\n'+message)
def handle_notification(self, notification):
handler = getattr(self, '_NH_%s' % notification.name, None)
if handler is not None:
handler(notification)
def _NH_SIPEngineDidStart(self, notification):
self._subscribe()
def _NH_SIPSubscriptionDidStart(self, notification):
route = Route(notification.sender.route_header.uri.host, notification.sender.route_header.uri.port, notification.sender.route_header.uri.parameters.get('transport', 'udp'))
self._subscription_routes = None
self._subscription_wait = 0.5
self.output.put('Subscription succeeded at %s:%d;transport=%s' % (route.address, route.port, route.transport))
self.success = True
def _NH_SIPSubscriptionChangedState(self, notification):
route = Route(notification.sender.route_header.uri.host, notification.sender.route_header.uri.port, notification.sender.route_header.uri.parameters.get('transport', 'udp'))
if notification.data.state.lower() == "pending":
self.output.put('Subscription pending at %s:%d;transport=%s' % (route.address, route.port, route.transport))
elif notification.data.state.lower() == "active":
self.output.put('Subscription active at %s:%d;transport=%s' % (route.address, route.port, route.transport))
def _NH_SIPSubscriptionDidEnd(self, notification):
notification_center = NotificationCenter()
notification_center.remove_observer(self, sender=notification.sender)
self.subscription = None
route = Route(notification.sender.route_header.uri.host, notification.sender.route_header.uri.port, notification.sender.route_header.uri.parameters.get('transport', 'udp'))
self.output.put('Unsubscribed from %s:%d;transport=%s' % (route.address, route.port, route.transport))
self.stop()
def _NH_SIPSubscriptionDidFail(self, notification):
notification_center = NotificationCenter()
notification_center.remove_observer(self, sender=notification.sender)
self.subscription = None
route = Route(notification.sender.route_header.uri.host, notification.sender.route_header.uri.port, notification.sender.route_header.uri.parameters.get('transport', 'udp'))
if notification.data.code:
status = ': %d %s' % (notification.data.code, notification.data.reason)
else:
status = ': %s' % notification.data.reason
self.output.put('Subscription failed at %s:%d;transport=%s%s' % (route.address, route.port, route.transport, status))
if self.stopping or notification.data.code in (401, 403, 407) or self.success:
self.success = False
self.stop()
else:
if not self._subscription_routes or time() > self._subscription_timeout:
self._subscription_wait = min(self._subscription_wait*2, 30)
timeout = random.uniform(self._subscription_wait, 2*self._subscription_wait)
reactor.callFromThread(reactor.callLater, timeout, self._subscribe)
else:
route = self._subscription_routes.popleft()
route_header = RouteHeader(route.uri)
self.subscription = Subscription(self.target.uri,
FromHeader(self.account.uri, self.account.display_name),
self.target,
ContactHeader(self.account.contact[route]),
b"presence",
route_header,
credentials=self.account.credentials,
refresh=self.account.sip.subscribe_interval)
notification_center.add_observer(self, sender=self.subscription)
self.subscription.subscribe(timeout=5)
def _NH_SIPSubscriptionGotNotify(self, notification):
if notification.data.content_type == PIDFDocument.content_type:
self.output.put('Received NOTIFY:')
try:
pidf = PIDF.parse(notification.data.body)
except ParserError as e:
self.output.put('Got illegal PIDF document: %s\n%s' % (str(e), notification.data.body))
else:
self._display_pidf(pidf)
self.print_help()
def _NH_DNSLookupDidSucceed(self, notification):
# create subscription and register to get notifications from it
self._subscription_routes = deque(notification.data.result)
route = self._subscription_routes.popleft()
route_header = RouteHeader(route.uri)
self.subscription = Subscription(self.target.uri,
FromHeader(self.account.uri, self.account.display_name),
self.target,
ContactHeader(self.account.contact[route]),
b"presence",
route_header,
credentials=self.account.credentials,
refresh=self.account.sip.subscribe_interval)
notification_center = NotificationCenter()
notification_center.add_observer(self, sender=self.subscription)
try:
self.subscription.subscribe(timeout=5)
except PJSIPError as e:
self.output.put('Subscription failed: %s' % str(e))
def _NH_DNSLookupDidFail(self, notification):
self.output.put('DNS lookup failed: %s' % notification.data.error)
timeout = random.uniform(1.0, 2.0)
reactor.callLater(timeout, self._subscribe)
def _NH_SAInputWasReceived(self, notification):
settings = SIPSimpleSettings()
key = notification.data.input
if key == 't':
self.logger.sip_to_stdout = not self.logger.sip_to_stdout
self.engine.trace_sip = self.logger.sip_to_stdout or settings.logs.trace_sip
self.output.put('SIP tracing to console is now %s.' % ('activated' if self.logger.sip_to_stdout else 'deactivated'))
elif key == 'j':
self.logger.pjsip_to_stdout = not self.logger.pjsip_to_stdout
self.engine.log_level = settings.logs.pjsip_level if (self.logger.pjsip_to_stdout or settings.logs.trace_pjsip) else 0
self.output.put('PJSIP tracing to console is now %s.' % ('activated' if self.logger.pjsip_to_stdout else 'deactivated'))
elif key == 'n':
self.logger.notifications_to_stdout = not self.logger.notifications_to_stdout
self.output.put('Notification tracing to console is now %s.' % ('activated' if self.logger.notifications_to_stdout else 'deactivated'))
elif key == '?':
self.print_help()
@run_in_twisted_thread
def _NH_SIPEngineDidEnd(self, notification):
self._stop_reactor()
@run_in_twisted_thread
def _NH_SIPEngineDidFail(self, notification):
self.output.put('Engine failed.')
self._stop_reactor()
def _NH_SIPEngineGotException(self, notification):
self.output.put('An exception occured within the SIP core:\n'+notification.data.traceback)
def _stop_reactor(self):
try:
reactor.stop()
except ReactorNotRunning:
pass
def _subscribe(self):
settings = SIPSimpleSettings()
self._subscription_timeout = time()+30
lookup = DNSLookup()
notification_center = NotificationCenter()
notification_center.add_observer(self, sender=lookup)
if self.account.sip.outbound_proxy is not None:
uri = SIPURI(host=self.account.sip.outbound_proxy.host, port=self.account.sip.outbound_proxy.port, parameters={'transport': self.account.sip.outbound_proxy.transport})
elif self.account.sip.always_use_my_proxy:
uri = SIPURI(host=self.account.id.domain)
else:
uri = self.target.uri
lookup.lookup_sip_proxy(uri, settings.sip.transport_list, tls_name=self.account.sip.tls_name)
def _format_note(self, note):
text = "Note"
if hasattr(note, 'lang') and note.lang is not None:
text += "(%s)" % note.lang
else:
text += ": %s" % note
return text
def _format_person(self, person, pidf):
buf = []
# display class
if person.rpid_class is not None:
buf.append(" Class: %s" % person.rpid_class)
# display timestamp
if person.timestamp is not None:
buf.append(" Timestamp: %s" % person.timestamp)
# display notes
if person.notes:
for note in person.notes:
buf.append(" %s" % self._format_note(note))
elif pidf.notes:
for note in pidf.notes:
buf.append(" %s" % self._format_note(note))
# display activities
if person.activities is not None:
activities = list(person.activities)
if len(activities) > 0:
text = " Activities"
if person.activities.since is not None or person.activities.until is not None:
text += " valid"
if person.activities.since is not None:
text += " from %s" % person.activities.since
if person.activities.until is not None:
text += " until %s" % person.activities.until
text += ": %s" % ', '.join(str(activity) for activity in activities)
buf.append(text)
if len(person.activities.notes) > 0:
for note in person.activities.notes:
buf.append(" %s" % self._format_note(note))
elif len(person.activities.notes) > 0:
buf.append(" Activities")
for note in person.activities.notes:
buf.append(" %s" % self._format_note(note))
# display mood
if person.mood is not None:
moods = list(person.mood)
if len(moods) > 0:
text = " Mood"
if person.mood.since is not None or person.mood.until is not None:
text += " valid"
if person.mood.since is not None:
text += " from %s" % person.mood.since
if person.mood.until is not None:
text += " until %s" % person.mood.until
text += ": %s" % ', '.join(str(mood) for mood in moods)
buf.append(text)
if len(person.mood.notes) > 0:
for note in person.mood.notes:
buf.append(" %s" % self._format_note(note))
# display place is
if person.place_is is not None:
place_info = ', '.join('%s %s' % (key.capitalize(), getattr(person.place_is, key).value) for key in ('audio', 'video', 'text') if getattr(person.place_is, key) and getattr(person.place_is, key).value)
if place_info != '':
buf.append(" Place information: " + place_info)
# display privacy
if person.privacy is not None:
text = " Private conversation possible with: "
private = []
if person.privacy.audio:
private.append("Audio")
if person.privacy.video:
private.append("Video")
if person.privacy.text:
private.append("Text")
if len(private) > 0:
text += ", ".join(private)
else:
text += "None"
buf.append(text)
# display sphere
if person.sphere is not None:
timeinfo = []
if person.sphere.since is not None:
timeinfo.append('from %s' % str(person.sphere.since))
if person.sphere.until is not None:
timeinfo.append('until %s' % str(person.sphere.until))
if len(timeinfo) != 0:
timeinfo = ' (' + ', '.join(timeinfo) + ')'
else:
timeinfo = ''
buf.append(" Current sphere%s: %s" % (timeinfo, person.sphere.value))
# display status icon
if person.status_icon is not None:
buf.append(" Status icon: %s" % person.status_icon)
# display time and time offset
if person.time_offset is not None:
ctime = datetime.datetime.utcnow() + datetime.timedelta(minutes=int(person.time_offset))
time_offset = int(person.time_offset)/60.0
if time_offset == int(time_offset):
offset_info = '(UTC+%d%s)' % (time_offset, (person.time_offset.description is not None and (' (%s)' % person.time_offset.description) or ''))
else:
offset_info = '(UTC+%.1f%s)' % (time_offset, (person.time_offset.description is not None and (' (%s)' % person.time_offset.description) or ''))
buf.append(" Current user time: %s %s" % (ctime.strftime("%H:%M"), offset_info))
# display user input
if person.user_input is not None:
buf.append(" User is %s" % person.user_input)
if person.user_input.last_input:
buf.append(" Last input at: %s" % person.user_input.last_input)
if person.user_input.idle_threshold:
buf.append(" Idle threshold: %s seconds" % person.user_input.idle_threshold)
return buf
def _format_service(self, service, pidf):
buf = []
# display class
if service.rpid_class is not None:
buf.append(" Class: %s" % service.rpid_class)
# display timestamp
if service.timestamp is not None:
buf.append(" Timestamp: %s" % service.timestamp)
# display notes
for note in service.notes:
buf.append(" %s" % self._format_note(note))
# display status
if service.status is not None:
if service.status.basic is not None:
buf.append(" Basic status: %s" % service.status.basic)
if service.status.extended is not None:
buf.append(" Extended status: %s" % service.status.extended)
# display contact
if service.contact is not None:
buf.append(" Contact%s: %s" % ((service.contact.priority is not None) and (' priority %s' % service.contact.priority) or '', urllib.parse.unquote(service.contact.value)))
# display device ID
if service.device_info is not None:
description = " (%s)" % urllib.parse.unquote(service.device_info.description.value).decode('utf-8') if service.device_info.description else ""
buf.append(" Service offered by device: %s%s" % (service.device_info.id, description))
# display relationship
if service.relationship is not None:
buf.append(" Relationship: %s" % service.relationship.value)
# display service-class
if service.service_class is not None:
buf.append(" Service class: %s" % service.service_class.value)
# display status icon
if service.status_icon is not None:
buf.append(" Status icon: %s" % service.status_icon)
# display user input
if service.user_input is not None:
buf.append(" Service is %s" % service.user_input)
if service.user_input.last_input:
buf.append(" Last input at: %s" % service.user_input.last_input)
if service.user_input.idle_threshold:
buf.append(" Idle threshold: %s seconds" % service.user_input.idle_threshold)
return buf
def _format_device(self, device, pidf):
buf = []
# display device ID
if device.device_id is not None:
buf.append(" Device id: %s" % device.device_id)
# display class
if device.rpid_class is not None:
buf.append(" Class: %s" % device.rpid_class)
# display timestamp
if device.timestamp is not None:
buf.append(" Timestamp: %s" % device.timestamp)
# display notes
for note in device.notes:
buf.append(" %s" % self._format_note(note))
# display user input
if device.user_input is not None:
buf.append(" Device is %s" % device.user_input)
if device.user_input.last_input:
buf.append(" Last input at: %s" % device.user_input.last_input)
if device.user_input.idle_threshold:
buf.append(" Idle threshold: %s seconds" % device.user_input.idle_threshold)
return buf
def _display_pidf(self, pidf):
buf = ["-"*16]
buf.append("Presence for %s:" % urllib.parse.unquote(pidf.entity))
persons = {}
devices = {}
services = {}
printed_sep = True
for child in pidf:
if isinstance(child, Person):
persons[child.id] = child
elif isinstance(child, Device):
devices[child.id] = child
elif isinstance(child, Service):
services[child.id] = child
# handle person information
if len(persons) == 0:
if list(pidf.notes):
buf.append(" Person information:")
for note in pidf.notes:
buf.append(" %s" % self._format_note(note))
printed_sep = False
else:
for person in list(persons.values()):
buf.append(" Person: %s" % person.id)
buf.extend(self._format_person(person, pidf))
printed_sep = False
# handle services informaation
if len(services) > 0:
if not printed_sep:
buf.append(" " + "-"*3)
for service in list(services.values()):
buf.append(" Service: %s" % service.id)
buf.extend(self._format_service(service, pidf))
# handle devices informaation
if len(devices) > 0:
if not printed_sep:
buf.append(" " + "-"*3)
for device in list(devices.values()):
buf.append(" Device: %s" % device.id)
buf.extend(self._format_device(device, pidf))
buf.append("-"*16)
# push the data
self.output.put('\n'.join(buf))
if __name__ == "__main__":
description = "This script subscribes to the presence event package published by the specified SIP target. If a SIP target is not specified, it will subscribe to its own address. It will then interprete PIDF bodies contained in NOTIFYs and display their meaning. The program will un-SUBSCRIBE and quit when CTRL+D is pressed."
usage = "%prog [options] [target-user@target-domain.com]"
parser = OptionParser(usage=usage, description=description)
parser.print_usage = parser.print_help
parser.add_option("-a", "--account-name", type="string", dest="account_name", help="The name of the account to use.")
parser.add_option("-s", "--trace-sip", action="store_true", dest="trace_sip", default=False, help="Dump the raw contents of incoming and outgoing SIP messages (disabled by default).")
parser.add_option("-j", "--trace-pjsip", action="store_true", dest="trace_pjsip", default=False, help="Print PJSIP logging output (disabled by default).")
parser.add_option("-n", "--trace-notifications", action="store_true", dest="trace_notifications", default=False, help="Print all notifications (disabled by default).")
options, args = parser.parse_args()
try:
application = SubscriptionApplication(options.account_name, args[0] if args else None, options.trace_sip, options.trace_pjsip, options.trace_notifications)
return_code = application.run()
except RuntimeError as e:
print("Error: %s" % str(e))
sys.exit(1)
except SIPCoreError as e:
print("Error: %s" % str(e))
sys.exit(1)
else:
sys.exit(return_code)
diff --git a/sip-subscribe-rls3 b/sip-subscribe-rls3
index b385e37..85ed517 100755
--- a/sip-subscribe-rls3
+++ b/sip-subscribe-rls3
@@ -1,377 +1,377 @@
#!/usr/bin/env python3
import os
import random
import select
import sys
import termios
from collections import deque
from optparse import OptionParser
from threading import Thread
from time import time
from application import log
from application.notification import IObserver, NotificationCenter, NotificationData
from application.python.queue import EventQueue
from eventlib.twistedutil import join_reactor
from twisted.internet import reactor
from twisted.internet.error import ReactorNotRunning
from zope.interface import implementer
from sipsimple.account import Account, AccountManager, BonjourAccount
from sipsimple.application import SIPApplication
from sipsimple.configuration import ConfigurationError, ConfigurationManager
from sipsimple.configuration.settings import SIPSimpleSettings
from sipsimple.core import ContactHeader, Engine, FromHeader, Header, RouteHeader, SIPCoreError, SIPURI, Subscription, ToHeader, Route
from sipsimple.lookup import DNSLookup
from sipsimple.storage import FileStorage
from sipsimple.threading import run_in_twisted_thread
from sipclient.configuration import config_directory
from sipclient.configuration.account import AccountExtension
from sipclient.configuration.settings import SIPSimpleSettingsExtension
from sipclient.log import Logger
class InputThread(Thread):
def __init__(self, application):
Thread.__init__(self)
self.application = application
self.daemon = True
self._old_terminal_settings = None
def run(self):
notification_center = NotificationCenter()
while True:
for char in self._getchars():
if char == "\x04":
self.application.stop()
return
else:
notification_center.post_notification('SAInputWasReceived', sender=self, data=NotificationData(input=char))
def stop(self):
self._termios_restore()
def _termios_restore(self):
if self._old_terminal_settings is not None:
termios.tcsetattr(sys.stdin.fileno(), termios.TCSADRAIN, self._old_terminal_settings)
def _getchars(self):
fd = sys.stdin.fileno()
if os.isatty(fd):
self._old_terminal_settings = termios.tcgetattr(fd)
new = termios.tcgetattr(fd)
new[3] = new[3] & ~termios.ICANON & ~termios.ECHO
new[6][termios.VMIN] = b'\000'
try:
termios.tcsetattr(fd, termios.TCSADRAIN, new)
if select.select([fd], [], [], None)[0]:
return sys.stdin.read(4192)
finally:
self._termios_restore()
else:
return os.read(fd, 4192)
@implementer(IObserver)
class SubscriptionApplication(object):
def __init__(self, account_name, target, trace_sip, trace_pjsip, trace_notifications):
self.account_name = account_name
self.target = target
self.input = InputThread(self)
self.output = EventQueue(lambda event: sys.stdout.write(event+'\n'))
self.logger = Logger(sip_to_stdout=trace_sip, pjsip_to_stdout=trace_pjsip, notifications_to_stdout=trace_notifications)
self.success = False
self.account = None
self.subscription = None
self.stopping = False
self._subscription_routes = None
self._subscription_timeout = 0.0
self._subscription_wait = 0.5
account_manager = AccountManager()
engine = Engine()
notification_center = NotificationCenter()
notification_center.add_observer(self, sender=account_manager)
notification_center.add_observer(self, sender=engine)
notification_center.add_observer(self, sender=self.input)
log.level.current = log.level.WARNING
def run(self):
account_manager = AccountManager()
configuration = ConfigurationManager()
engine = Engine()
# start output thread
self.output.start()
# startup configuration
Account.register_extension(AccountExtension)
BonjourAccount.register_extension(AccountExtension)
SIPSimpleSettings.register_extension(SIPSimpleSettingsExtension)
SIPApplication.storage = FileStorage(config_directory)
try:
configuration.start()
except ConfigurationError as e:
raise RuntimeError("Failed to load sipclient's configuration: %s\nIf an old configuration file is in place, delete it or move it and recreate the configuration using the sip_settings script." % str(e))
account_manager.load()
if self.account_name is None:
self.account = account_manager.default_account
else:
possible_accounts = [account for account in account_manager.iter_accounts() if self.account_name in account.id and account.enabled]
if len(possible_accounts) > 1:
raise RuntimeError("More than one account exists which matches %s: %s" % (self.account_name, ", ".join(sorted(account.id for account in possible_accounts))))
if len(possible_accounts) == 0:
raise RuntimeError("No enabled account that matches %s was found. Available and enabled accounts: %s" % (self.account_name, ", ".join(sorted(account.id for account in account_manager.get_accounts() if account.enabled))))
self.account = possible_accounts[0]
if self.account is None:
raise RuntimeError("Unknown account %s. Available accounts: %s" % (self.account_name, ', '.join(account.id for account in account_manager.iter_accounts())))
elif self.account == BonjourAccount():
raise RuntimeError("Cannot use bonjour account for presence subscription")
elif not self.account.presence.enabled:
raise RuntimeError("Presence is not enabled for account %s" % self.account.id)
for account in account_manager.iter_accounts():
if account == self.account:
account.sip.register = False
else:
account.enabled = False
self.output.put('Using account %s' % self.account.id)
settings = SIPSimpleSettings()
# start logging
self.logger.start()
# start the engine
engine.start(
auto_sound=False,
events={'presence': ['multipart/related', 'application/rlmi+xml', 'application/pidf+xml']},
udp_port=settings.sip.udp_port if "udp" in settings.sip.transport_list else None,
tcp_port=settings.sip.tcp_port if "tcp" in settings.sip.transport_list else None,
tls_port=settings.sip.tls_port if "tls" in settings.sip.transport_list else None,
- tls_verify_server=self.account.tls.verify_server,
+ tls_verify_server=settings.tls.verify_server,
tls_ca_file=os.path.expanduser(settings.tls.ca_list) if settings.tls.ca_list else None,
- tls_cert_file=os.path.expanduser(self.account.tls.certificate) if self.account.tls.certificate else None,
- tls_privkey_file=os.path.expanduser(self.account.tls.certificate) if self.account.tls.certificate else None,
+ tls_cert_file=os.path.expanduser(settings.tls.certificate) if settings.tls.certificate else None,
+ tls_privkey_file=os.path.expanduser(settings.tls.certificate) if settings.tls.certificate else None,
user_agent=settings.user_agent,
sample_rate=settings.audio.sample_rate,
rtp_port_range=(settings.rtp.port_range.start, settings.rtp.port_range.end),
trace_sip=settings.logs.trace_sip or self.logger.sip_to_stdout,
log_level=settings.logs.pjsip_level if (settings.logs.trace_pjsip or self.logger.pjsip_to_stdout) else 0
)
if self.target is None:
self.target = ToHeader(SIPURI(user='%s+presence' % self.account.id.username, host=self.account.id.domain))
else:
if '@' not in self.target:
self.target = '%s@%s' % (self.target, self.account.id.domain)
if not self.target.startswith('sip:') and not self.target.startswith('sips:'):
self.target = 'sip:' + self.target
try:
self.target = ToHeader(SIPURI.parse(self.target))
except SIPCoreError:
self.output.put('Illegal SIP URI: %s' % self.target)
engine.stop()
return 1
self.output.put('Subscribing to %s for the presence event' % self.target.uri)
# start the input thread
self.input.start()
# start twisted
try:
reactor.run()
finally:
self.input.stop()
# stop the output
self.output.stop()
self.output.join()
self.logger.stop()
return 0 if self.success else 1
def stop(self):
self.stopping = True
if self.subscription is not None and self.subscription.state.lower() in ('accepted', 'pending', 'active'):
self.subscription.end(timeout=1)
else:
engine = Engine()
engine.stop()
def print_help(self):
message = 'Available control keys:\n'
message += ' t: toggle SIP trace on the console\n'
message += ' j: toggle PJSIP trace on the console\n'
message += ' n: toggle notifications trace on the console\n'
message += ' Ctrl-d: quit the program\n'
message += ' ?: display this help message\n'
self.output.put('\n'+message)
def handle_notification(self, notification):
handler = getattr(self, '_NH_%s' % notification.name, None)
if handler is not None:
handler(notification)
def _NH_SIPSubscriptionDidStart(self, notification):
route = Route(notification.sender.route_header.uri.host, notification.sender.route_header.uri.port, notification.sender.route_header.uri.parameters.get('transport', 'udp'))
self._subscription_routes = None
self._subscription_wait = 0.5
self.output.put('Subscription succeeded at %s:%d;transport=%s' % (route.address, route.port, route.transport))
self.success = True
def _NH_SIPSubscriptionChangedState(self, notification):
route = Route(notification.sender.route_header.uri.host, notification.sender.route_header.uri.port, notification.sender.route_header.uri.parameters.get('transport', 'udp'))
if notification.data.state.lower() == "pending":
self.output.put('Subscription pending at %s:%d;transport=%s' % (route.address, route.port, route.transport))
elif notification.data.state.lower() == "active":
self.output.put('Subscription active at %s:%d;transport=%s' % (route.address, route.port, route.transport))
def _NH_SIPSubscriptionDidEnd(self, notification):
notification_center = NotificationCenter()
notification_center.remove_observer(self, sender=notification.sender)
self.subscription = None
route = Route(notification.sender.route_header.uri.host, notification.sender.route_header.uri.port, notification.sender.route_header.uri.parameters.get('transport', 'udp'))
self.output.put('Unsubscribed from %s:%d;transport=%s' % (route.address, route.port, route.transport))
self.stop()
def _NH_SIPSubscriptionDidFail(self, notification):
notification_center = NotificationCenter()
notification_center.remove_observer(self, sender=notification.sender)
self.subscription = None
route = Route(notification.sender.route_header.uri.host, notification.sender.route_header.uri.port, notification.sender.route_header.uri.parameters.get('transport', 'udp'))
if notification.data.code:
status = ': %d %s' % (notification.data.code, notification.data.reason)
else:
status = ': %s' % notification.data.reason
self.output.put('Subscription failed at %s:%d;transport=%s%s' % (route.address, route.port, route.transport, status))
if self.stopping or notification.data.code in (401, 403, 407) or self.success:
self.success = False
self.stop()
else:
if not self._subscription_routes or time() > self._subscription_timeout:
self._subscription_wait = min(self._subscription_wait*2, 30)
timeout = random.uniform(self._subscription_wait, 2*self._subscription_wait)
reactor.callFromThread(reactor.callLater, timeout, self._subscribe)
else:
route = self._subscription_routes.popleft()
route_header = RouteHeader(route.uri)
self.subscription = Subscription(self.target.uri,
FromHeader(self.account.uri, self.account.display_name),
self.target,
ContactHeader(self.account.contact[route]),
b"presence",
route_header,
credentials=self.account.credentials,
refresh=self.account.sip.subscribe_interval)
notification_center.add_observer(self, sender=self.subscription)
self.subscription.subscribe(extra_headers=[Header('Supported', 'eventlist')], timeout=5)
def _NH_SIPSubscriptionGotNotify(self, notification):
if notification.data.body:
self.output.put('Received NOTIFY:\n' + notification.data.body.decode())
self.print_help()
def _NH_DNSLookupDidSucceed(self, notification):
# create subscription and register to get notifications from it
self._subscription_routes = deque(notification.data.result)
route = self._subscription_routes.popleft()
route_header = RouteHeader(route.uri)
self.subscription = Subscription(self.target.uri,
FromHeader(self.account.uri, self.account.display_name),
self.target,
ContactHeader(self.account.contact[route]),
b"presence",
route_header,
credentials=self.account.credentials,
refresh=self.account.sip.subscribe_interval)
notification_center = NotificationCenter()
notification_center.add_observer(self, sender=self.subscription)
self.subscription.subscribe(extra_headers=[Header('Supported', 'eventlist')], timeout=5)
def _NH_DNSLookupDidFail(self, notification):
self.output.put('DNS lookup failed: %s' % notification.data.error)
timeout = random.uniform(1.0, 2.0)
reactor.callLater(timeout, self._subscribe)
def _NH_SAInputWasReceived(self, notification):
engine = Engine()
settings = SIPSimpleSettings()
key = notification.data.input
if key == 't':
self.logger.sip_to_stdout = not self.logger.sip_to_stdout
engine.trace_sip = self.logger.sip_to_stdout or settings.logs.trace_sip
self.output.put('SIP tracing to console is now %s.' % ('activated' if self.logger.sip_to_stdout else 'deactivated'))
elif key == 'j':
self.logger.pjsip_to_stdout = not self.logger.pjsip_to_stdout
engine.log_level = settings.logs.pjsip_level if (self.logger.pjsip_to_stdout or settings.logs.trace_pjsip) else 0
self.output.put('PJSIP tracing to console is now %s.' % ('activated' if self.logger.pjsip_to_stdout else 'deactivated'))
elif key == 'n':
self.logger.notifications_to_stdout = not self.logger.notifications_to_stdout
self.output.put('Notification tracing to console is now %s.' % ('activated' if self.logger.notifications_to_stdout else 'deactivated'))
elif key == '?':
self.print_help()
def _NH_SIPEngineDidStart(self, notification):
self._subscribe()
@run_in_twisted_thread
def _NH_SIPEngineDidEnd(self, notification):
self._stop_reactor()
@run_in_twisted_thread
def _NH_SIPEngineDidFail(self, notification):
self.output.put('Engine failed.')
self._stop_reactor()
def _NH_SIPEngineGotException(self, notification):
self.output.put('An exception occured within the SIP core:\n'+notification.data.traceback)
def _stop_reactor(self):
try:
reactor.stop()
except ReactorNotRunning:
pass
def _subscribe(self):
settings = SIPSimpleSettings()
self._subscription_timeout = time()+30
lookup = DNSLookup()
notification_center = NotificationCenter()
notification_center.add_observer(self, sender=lookup)
if self.account.sip.outbound_proxy is not None:
uri = SIPURI(host=self.account.sip.outbound_proxy.host, port=self.account.sip.outbound_proxy.port, parameters={'transport': self.account.sip.outbound_proxy.transport})
else:
uri = self.target.uri
lookup.lookup_sip_proxy(uri, settings.sip.transport_list, tls_name=self.account.sip.tls_name)
if __name__ == "__main__":
description = "This script subscribes to the presence event package published by the specified SIP target assuming it is a resource list handled by a RLS server. The RLS server will then SUBSCRIBE in behalf of the account, collect NOTIFYs with the presence information of the recipients and provide periodically aggregated NOTIFYs back to the subscriber. If a target address is not specified, it will subscribe to the address 'username-buddies@domain.com', where username and domain are taken from the account's SIP address. It will then interprete PIDF bodies contained in NOTIFYs and display their meaning. The program will un-SUBSCRIBE and quit when CTRL+D is pressed."
usage = "%prog [options] [target-user@target-domain.com]"
parser = OptionParser(usage=usage, description=description)
parser.print_usage = parser.print_help
parser.add_option("-a", "--account-name", type="string", dest="account_name", help="The name of the account to use.")
parser.add_option("-s", "--trace-sip", action="store_true", dest="trace_sip", default=False, help="Dump the raw contents of incoming and outgoing SIP messages (disabled by default).")
parser.add_option("-j", "--trace-pjsip", action="store_true", dest="trace_pjsip", default=False, help="Print PJSIP logging output (disabled by default).")
parser.add_option("-n", "--trace-notifications", action="store_true", dest="trace_notifications", default=False, help="Print all notifications (disabled by default).")
options, args = parser.parse_args()
try:
application = SubscriptionApplication(options.account_name, args[0] if args else None, options.trace_sip, options.trace_pjsip, options.trace_notifications)
return_code = application.run()
except RuntimeError as e:
print("Error: %s" % str(e))
sys.exit(1)
except SIPCoreError as e:
print("Error: %s" % str(e))
sys.exit(1)
else:
sys.exit(return_code)
diff --git a/sip-subscribe-winfo3 b/sip-subscribe-winfo3
index 4ff521d..ebc13a1 100755
--- a/sip-subscribe-winfo3
+++ b/sip-subscribe-winfo3
@@ -1,393 +1,393 @@
#!/usr/bin/env python3
import os
import random
import select
import sys
import termios
from collections import deque
from optparse import OptionParser
from threading import Thread
from time import time
from application import log
from application.notification import IObserver, NotificationCenter, NotificationData
from application.python.queue import EventQueue
from eventlib.twistedutil import join_reactor
from twisted.internet import reactor
from twisted.internet.error import ReactorNotRunning
from zope.interface import implementer
from sipsimple.account import Account, AccountManager, BonjourAccount
from sipsimple.application import SIPApplication
from sipsimple.configuration import ConfigurationError, ConfigurationManager
from sipsimple.configuration.settings import SIPSimpleSettings
from sipsimple.core import ContactHeader, Engine, FromHeader, Route, RouteHeader, SIPCoreError, SIPURI, Subscription, ToHeader
from sipsimple.lookup import DNSLookup
from sipsimple.payloads import ParserError
from sipsimple.payloads.watcherinfo import WatcherInfoDocument
from sipsimple.storage import FileStorage
from sipsimple.threading import run_in_twisted_thread
from sipclient.configuration import config_directory
from sipclient.configuration.account import AccountExtension
from sipclient.configuration.settings import SIPSimpleSettingsExtension
from sipclient.log import Logger
class InputThread(Thread):
def __init__(self, application):
Thread.__init__(self)
self.application = application
self.daemon = True
self._old_terminal_settings = None
def run(self):
notification_center = NotificationCenter()
while True:
for char in self._getchars():
if char == "\x04":
self.application.stop()
return
else:
notification_center.post_notification('SAInputWasReceived', sender=self, data=NotificationData(input=char))
def stop(self):
self._termios_restore()
def _termios_restore(self):
if self._old_terminal_settings is not None:
termios.tcsetattr(sys.stdin.fileno(), termios.TCSADRAIN, self._old_terminal_settings)
def _getchars(self):
fd = sys.stdin.fileno()
if os.isatty(fd):
self._old_terminal_settings = termios.tcgetattr(fd)
new = termios.tcgetattr(fd)
new[3] = new[3] & ~termios.ICANON & ~termios.ECHO
new[6][termios.VMIN] = b'\000'
try:
termios.tcsetattr(fd, termios.TCSADRAIN, new)
if select.select([fd], [], [], None)[0]:
return sys.stdin.read(4192)
finally:
self._termios_restore()
else:
return os.read(fd, 4192)
@implementer(IObserver)
class WinfoApplication(object):
def __init__(self, account_name, trace_sip, trace_pjsip, trace_notifications):
self.account_name = account_name
self.input = InputThread(self)
self.output = EventQueue(lambda event: sys.stdout.write(event+'\n'))
self.logger = Logger(sip_to_stdout=trace_sip, pjsip_to_stdout=trace_pjsip, notifications_to_stdout=trace_notifications)
self.success = False
self.account = None
self.subscription = None
self.stopping = False
self._subscription_routes = None
self._subscription_timeout = 0.0
self._subscription_wait = 0.5
account_manager = AccountManager()
engine = Engine()
notification_center = NotificationCenter()
notification_center.add_observer(self, sender=account_manager)
notification_center.add_observer(self, sender=engine)
notification_center.add_observer(self, sender=self.input)
log.level.current = log.level.WARNING
def run(self):
account_manager = AccountManager()
configuration = ConfigurationManager()
engine = Engine()
# start output thread
self.output.start()
# startup configuration
Account.register_extension(AccountExtension)
BonjourAccount.register_extension(AccountExtension)
SIPSimpleSettings.register_extension(SIPSimpleSettingsExtension)
SIPApplication.storage = FileStorage(config_directory)
try:
configuration.start()
except ConfigurationError as e:
raise RuntimeError("Failed to load sipclient's configuration: %s\nIf an old configuration file is in place, delete it or move it and recreate the configuration using the sip_settings script." % str(e))
account_manager.load()
if self.account_name is None:
self.account = account_manager.default_account
else:
possible_accounts = [account for account in account_manager.iter_accounts() if self.account_name in account.id and account.enabled]
if len(possible_accounts) > 1:
raise RuntimeError("More than one account exists which matches %s: %s" % (self.account_name, ", ".join(sorted(account.id for account in possible_accounts))))
if len(possible_accounts) == 0:
raise RuntimeError("No enabled account that matches %s was found. Available and enabled accounts: %s" % (self.account_name, ", ".join(sorted(account.id for account in account_manager.get_accounts() if account.enabled))))
self.account = possible_accounts[0]
if self.account is None:
raise RuntimeError("Unknown account %s. Available accounts: %s" % (self.account_name, ', '.join(account.id for account in account_manager.iter_accounts())))
elif self.account == BonjourAccount():
raise RuntimeError("Cannot use bonjour account for watcherinfo subscription")
elif not self.account.presence.enabled:
raise RuntimeError("Presence is not enabled for account %s" % self.account.id)
elif not self.account.xcap.enabled:
raise RuntimeError("XCAP is not enabled for account %s" % self.account.id)
elif self.account.xcap.xcap_root is None:
raise RuntimeError("XCAP root is not defined for account %s" % self.account.id)
for account in account_manager.iter_accounts():
if account == self.account:
account.sip.register = False
else:
account.enabled = False
self.output.put('Using account %s' % self.account.id)
settings = SIPSimpleSettings()
# start logging
self.logger.start()
# start the engine
engine.start(
auto_sound=False,
events={'presence.winfo': [WatcherInfoDocument.content_type]},
udp_port=settings.sip.udp_port if "udp" in settings.sip.transport_list else None,
tcp_port=settings.sip.tcp_port if "tcp" in settings.sip.transport_list else None,
tls_port=settings.sip.tls_port if "tls" in settings.sip.transport_list else None,
- tls_verify_server=self.account.tls.verify_server,
+ tls_verify_server=settings.tls.verify_server,
tls_ca_file=os.path.expanduser(settings.tls.ca_list) if settings.tls.ca_list else None,
- tls_cert_file=os.path.expanduser(self.account.tls.certificate) if self.account.tls.certificate else None,
- tls_privkey_file=os.path.expanduser(self.account.tls.certificate) if self.account.tls.certificate else None,
+ tls_cert_file=os.path.expanduser(settings.tls.certificate) if settings.tls.certificate else None,
+ tls_privkey_file=os.path.expanduser(settings.tls.certificate) if settings.tls.certificate else None,
user_agent=settings.user_agent,
sample_rate=settings.audio.sample_rate,
rtp_port_range=(settings.rtp.port_range.start, settings.rtp.port_range.end),
trace_sip=settings.logs.trace_sip or self.logger.sip_to_stdout,
log_level=settings.logs.pjsip_level if (settings.logs.trace_pjsip or self.logger.pjsip_to_stdout) else 0
)
self.output.put('Subscribing to the presence.winfo event')
# start the input thread
self.input.start()
reactor.callLater(0, self._subscribe)
# start twisted
try:
reactor.run()
finally:
self.input.stop()
# stop the output
self.output.stop()
self.output.join()
self.logger.stop()
return 0 if self.success else 1
def stop(self):
self.stopping = True
if self.subscription is not None and self.subscription.state.lower() in ('accepted', 'pending', 'active'):
self.subscription.end(timeout=1)
else:
engine = Engine()
engine.stop()
def print_help(self):
message = 'Available control keys:\n'
message += ' t: toggle SIP trace on the console\n'
message += ' j: toggle PJSIP trace on the console\n'
message += ' Ctrl-d: quit the program\n'
message += ' ?: display this help message\n'
self.output.put('\n'+message)
def handle_notification(self, notification):
handler = getattr(self, '_NH_%s' % notification.name, None)
if handler is not None:
handler(notification)
def _NH_SIPSubscriptionDidStart(self, notification):
route = Route(notification.sender.route_header.uri.host, notification.sender.route_header.uri.port, notification.sender.route_header.uri.parameters.get('transport', 'udp'))
self._subscription_routes = None
self._subscription_wait = 0.5
self.output.put('Subscription succeeded at %s:%d;transport=%s' % (route.address, route.port, route.transport))
self.success = True
def _NH_SIPSubscriptionChangedState(self, notification):
route = Route(notification.sender.route_header.uri.host, notification.sender.route_header.uri.port, notification.sender.route_header.uri.parameters.get('transport', 'udp'))
if notification.data.state.lower() == "pending":
self.output.put('Subscription pending at %s:%d;transport=%s' % (route.address, route.port, route.transport))
elif notification.data.state.lower() == "active":
self.output.put('Subscription active at %s:%d;transport=%s' % (route.address, route.port, route.transport))
def _NH_SIPSubscriptionDidEnd(self, notification):
notification_center = NotificationCenter()
notification_center.remove_observer(self, sender=notification.sender)
self.subscription = None
route = Route(notification.sender.route_header.uri.host, notification.sender.route_header.uri.port, notification.sender.route_header.uri.parameters.get('transport', 'udp'))
self.output.put('Unsubscribed from %s:%d;transport=%s' % (route.address, route.port, route.transport))
self.stop()
def _NH_SIPSubscriptionDidFail(self, notification):
notification_center = NotificationCenter()
notification_center.remove_observer(self, sender=notification.sender)
self.subscription = None
route = Route(notification.sender.route_header.uri.host, notification.sender.route_header.uri.port, notification.sender.route_header.uri.parameters.get('transport', 'udp'))
if notification.data.code:
status = ': %d %s' % (notification.data.code, notification.data.reason)
else:
status = ': %s' % notification.data.reason
self.output.put('Subscription failed at %s:%d;transport=%s%s' % (route.address, route.port, route.transport, status))
if self.stopping or notification.data.code in (401, 403, 407) or self.success:
self.success = False
self.stop()
else:
if not self._subscription_routes or time() > self._subscription_timeout:
self._subscription_wait = min(self._subscription_wait*2, 30)
timeout = random.uniform(self._subscription_wait, 2*self._subscription_wait)
reactor.callFromThread(reactor.callLater, timeout, self._subscribe)
else:
route = self._subscription_routes.popleft()
route_header = RouteHeader(route.uri)
self.subscription = Subscription(self.account.uri,
FromHeader(self.account.uri, self.account.display_name),
ToHeader(self.account.uri, self.account.display_name),
ContactHeader(self.account.contact[route]),
b"presence.winfo",
route_header,
credentials=self.account.credentials,
refresh=self.account.sip.subscribe_interval)
notification_center.add_observer(self, sender=self.subscription)
self.subscription.subscribe(timeout=5)
def _NH_SIPSubscriptionGotNotify(self, notification):
if notification.data.content_type == WatcherInfoDocument.content_type:
self._handle_winfo(notification.data.body)
def _NH_DNSLookupDidSucceed(self, notification):
# create subscription and register to get notifications from it
self._subscription_routes = deque(notification.data.result)
route = self._subscription_routes.popleft()
route_header = RouteHeader(route.uri)
self.subscription = Subscription(self.account.uri,
FromHeader(self.account.uri, self.account.display_name),
ToHeader(self.account.uri, self.account.display_name),
ContactHeader(self.account.contact[route]),
b"presence.winfo",
route_header,
credentials=self.account.credentials,
refresh=self.account.sip.subscribe_interval)
notification_center = NotificationCenter()
notification_center.add_observer(self, sender=self.subscription)
self.subscription.subscribe(timeout=5)
def _NH_DNSLookupDidFail(self, notification):
self.output.put('DNS lookup failed: %s' % notification.data.error)
timeout = random.uniform(1.0, 2.0)
reactor.callLater(timeout, self._subscribe)
def _NH_SAInputWasReceived(self, notification):
engine = Engine()
settings = SIPSimpleSettings()
key = notification.data.input
if key == 't':
self.logger.sip_to_stdout = not self.logger.sip_to_stdout
engine.trace_sip = self.logger.sip_to_stdout or settings.logs.trace_sip
self.output.put('SIP tracing to console is now %s.' % ('activated' if self.logger.sip_to_stdout else 'deactivated'))
elif key == 'j':
self.logger.pjsip_to_stdout = not self.logger.pjsip_to_stdout
engine.log_level = settings.logs.pjsip_level if (self.logger.pjsip_to_stdout or settings.logs.trace_pjsip) else 0
self.output.put('PJSIP tracing to console is now %s.' % ('activated' if self.logger.pjsip_to_stdout else 'deactivated'))
elif key == 'n':
self.logger.notifications_to_stdout = not self.logger.notifications_to_stdout
self.output.put('Notification tracing to console is now %s.' % ('activated' if self.logger.notifications_to_stdout else 'deactivated'))
elif key == '?':
self.print_help()
@run_in_twisted_thread
def _NH_SIPEngineDidEnd(self, notification):
self._stop_reactor()
@run_in_twisted_thread
def _NH_SIPEngineDidFail(self, notification):
self.output.put('Engine failed.')
self._stop_reactor()
def _NH_SIPEngineGotException(self, notification):
self.output.put('An exception occured within the SIP core:\n'+notification.data.traceback)
def _stop_reactor(self):
try:
reactor.stop()
except ReactorNotRunning:
pass
def _subscribe(self):
settings = SIPSimpleSettings()
self._subscription_timeout = time()+30
lookup = DNSLookup()
notification_center = NotificationCenter()
notification_center.add_observer(self, sender=lookup)
if self.account.sip.outbound_proxy is not None:
uri = SIPURI(host=self.account.sip.outbound_proxy.host, port=self.account.sip.outbound_proxy.port, parameters={'transport': self.account.sip.outbound_proxy.transport})
else:
uri = SIPURI(host=self.account.id.domain)
lookup.lookup_sip_proxy(uri, settings.sip.transport_list, tls_name=self.account.sip.tls_name)
def _handle_winfo(self, body):
try:
watcher_info = WatcherInfoDocument.parse(body)
except ParserError as e:
self.output.put("Got illegal winfo document: %s\n%s" % (str(e), body))
else:
try:
wlist = watcher_info['sip:' + self.account.id]
except KeyError:
self.output.put("Expected an entry for account %s in the winfo document" % self.account.id)
else:
buf = ["Received NOTIFY:", "----"]
buf.append("Active watchers:")
for watcher in wlist.active:
buf.append(" %s" % watcher)
buf.append("Terminated watchers:")
for watcher in wlist.terminated:
buf.append(" %s" % watcher)
buf.append("Pending watchers:")
for watcher in wlist.pending:
buf.append(" %s" % watcher)
buf.append("Waiting watchers:")
for watcher in wlist.waiting:
buf.append(" %s" % watcher)
buf.append("----")
self.output.put('\n'.join(buf))
if __name__ == "__main__":
description = "This script subscribes to the presence.winfo event package and shows the received watcher info document's content. The program will un-SUBSCRIBE and quit when CTRL+D is pressed."
usage = "%prog [options] [target-user@target-domain.com]"
parser = OptionParser(usage=usage, description=description)
parser.print_usage = parser.print_help
parser.add_option("-a", "--account-name", type="string", dest="account_name", help="The name of the account to use.")
parser.add_option("-s", "--trace-sip", action="store_true", dest="trace_sip", default=False, help="Dump the raw contents of incoming and outgoing SIP messages (disabled by default).")
parser.add_option("-j", "--trace-pjsip", action="store_true", dest="trace_pjsip", default=False, help="Print PJSIP logging output (disabled by default).")
parser.add_option("-n", "--trace-notifications", action="store_true", dest="trace_notifications", default=False, help="Print all notifications (disabled by default).")
options, args = parser.parse_args()
try:
application = WinfoApplication(options.account_name, options.trace_sip, options.trace_pjsip, options.trace_notifications)
return_code = application.run()
except RuntimeError as e:
print("Error: %s" % str(e))
sys.exit(1)
except SIPCoreError as e:
print("Error: %s" % str(e))
sys.exit(1)
else:
sys.exit(return_code)
diff --git a/sip-subscribe-xcap-diff3 b/sip-subscribe-xcap-diff3
index 422669e..5c52b39 100755
--- a/sip-subscribe-xcap-diff3
+++ b/sip-subscribe-xcap-diff3
@@ -1,412 +1,412 @@
#!/usr/bin/env python3
import os
import random
import select
import sys
import termios
from collections import deque
from optparse import OptionParser
from threading import Thread
from time import time
from application import log
from application.notification import IObserver, NotificationCenter, NotificationData
from application.python.queue import EventQueue
from eventlib.twistedutil import join_reactor
from twisted.internet import reactor
from twisted.internet.error import ReactorNotRunning
from zope.interface import implementer
from sipsimple.account import Account, AccountManager, BonjourAccount
from sipsimple.application import SIPApplication
from sipsimple.configuration import ConfigurationError, ConfigurationManager
from sipsimple.configuration.settings import SIPSimpleSettings
from sipsimple.core import ContactHeader, Engine, FromHeader, Route, RouteHeader, SIPCoreError, SIPURI, Subscription, ToHeader
from sipsimple.lookup import DNSLookup
from sipsimple.payloads import ParserError
from sipsimple.payloads.xcapdiff import XCAPDiffDocument, Document, Element, Attribute
from sipsimple.payloads.resourcelists import ResourceListsDocument, ResourceLists, List, Entry
from sipsimple.storage import FileStorage
from sipsimple.threading import run_in_twisted_thread
from sipclient.configuration import config_directory
from sipclient.configuration.account import AccountExtension
from sipclient.configuration.settings import SIPSimpleSettingsExtension
from sipclient.log import Logger
class InputThread(Thread):
def __init__(self, application):
Thread.__init__(self)
self.application = application
self.daemon = True
self._old_terminal_settings = None
def run(self):
notification_center = NotificationCenter()
while True:
for char in self._getchars():
if char == "\x04":
self.application.stop()
return
else:
notification_center.post_notification('SAInputWasReceived', sender=self, data=NotificationData(input=char))
def stop(self):
self._termios_restore()
def _termios_restore(self):
if self._old_terminal_settings is not None:
termios.tcsetattr(sys.stdin.fileno(), termios.TCSADRAIN, self._old_terminal_settings)
def _getchars(self):
fd = sys.stdin.fileno()
if os.isatty(fd):
self._old_terminal_settings = termios.tcgetattr(fd)
new = termios.tcgetattr(fd)
new[3] = new[3] & ~termios.ICANON & ~termios.ECHO
new[6][termios.VMIN] = b'\000'
try:
termios.tcsetattr(fd, termios.TCSADRAIN, new)
if select.select([fd], [], [], None)[0]:
return sys.stdin.read(4192)
finally:
self._termios_restore()
else:
return os.read(fd, 4192)
@implementer(IObserver)
class SubscriptionApplication(object):
def __init__(self, account_name, trace_sip, trace_pjsip, trace_notifications):
self.account_name = account_name
self.target = None
self.input = InputThread(self)
self.output = EventQueue(lambda event: sys.stdout.write(event+'\n'))
self.logger = Logger(sip_to_stdout=trace_sip, pjsip_to_stdout=trace_pjsip, notifications_to_stdout=trace_notifications)
self.success = False
self.account = None
self.subscription = None
self.stopping = False
self.body = None
self.content_type = None
self._subscription_routes = None
self._subscription_timeout = 0.0
self._subscription_wait = 0.5
account_manager = AccountManager()
engine = Engine()
notification_center = NotificationCenter()
notification_center.add_observer(self, sender=account_manager)
notification_center.add_observer(self, sender=engine)
notification_center.add_observer(self, sender=self.input)
log.level.current = log.level.WARNING
def run(self):
account_manager = AccountManager()
configuration = ConfigurationManager()
engine = Engine()
# start output thread
self.output.start()
# startup configuration
Account.register_extension(AccountExtension)
BonjourAccount.register_extension(AccountExtension)
SIPSimpleSettings.register_extension(SIPSimpleSettingsExtension)
SIPApplication.storage = FileStorage(config_directory)
try:
configuration.start()
except ConfigurationError as e:
raise RuntimeError("Failed to load sipclient's configuration: %s\nIf an old configuration file is in place, delete it or move it and recreate the configuration using the sip_settings script." % str(e))
account_manager.load()
if self.account_name is None:
self.account = account_manager.default_account
else:
possible_accounts = [account for account in account_manager.iter_accounts() if self.account_name in account.id and account.enabled]
if len(possible_accounts) > 1:
raise RuntimeError("More than one account exists which matches %s: %s" % (self.account_name, ", ".join(sorted(account.id for account in possible_accounts))))
if len(possible_accounts) == 0:
raise RuntimeError("No enabled account that matches %s was found. Available and enabled accounts: %s" % (self.account_name, ", ".join(sorted(account.id for account in account_manager.get_accounts() if account.enabled))))
self.account = possible_accounts[0]
if self.account is None:
raise RuntimeError("Unknown account %s. Available accounts: %s" % (self.account_name, ', '.join(account.id for account in account_manager.iter_accounts())))
elif not self.account.enabled:
raise RuntimeError("Account %s is not enabled" % self.account.id)
elif self.account == BonjourAccount():
raise RuntimeError("Cannot use bonjour account for presence subscription")
elif not self.account.xcap.enabled:
raise RuntimeError("XCAP is not enabled for account %s" % self.account.id)
elif self.account.xcap.xcap_root is None:
raise RuntimeError("XCAP root is not defined for account %s" % self.account.id)
for account in account_manager.iter_accounts():
if account == self.account:
account.sip.register = False
else:
account.enabled = False
self.output.put('Using account %s' % self.account.id)
settings = SIPSimpleSettings()
# generate the body
list = List()
resource_lists = ResourceLists([list])
list.add(Entry('resource-lists/users/sip:%s/index' % self.account.id))
list.add(Entry('rls-services/users/sip:%s/index' % self.account.id))
list.add(Entry('pres-rules/users/sip:%s/index' % self.account.id))
self.body = resource_lists.toxml(pretty_print=True)
self.content_type = ResourceListsDocument.content_type
# start logging
self.logger.start()
# start the engine
engine.start(
auto_sound=False,
events={'xcap-diff': ['application/xcap-diff+xml']},
udp_port=settings.sip.udp_port if "udp" in settings.sip.transport_list else None,
tcp_port=settings.sip.tcp_port if "tcp" in settings.sip.transport_list else None,
tls_port=settings.sip.tls_port if "tls" in settings.sip.transport_list else None,
- tls_verify_server=self.account.tls.verify_server,
+ tls_verify_server=settings.tls.verify_server,
tls_ca_file=os.path.expanduser(settings.tls.ca_list) if settings.tls.ca_list else None,
- tls_cert_file=os.path.expanduser(self.account.tls.certificate) if self.account.tls.certificate else None,
- tls_privkey_file=os.path.expanduser(self.account.tls.certificate) if self.account.tls.certificate else None,
+ tls_cert_file=os.path.expanduser(settings.tls.certificate) if settings.tls.certificate else None,
+ tls_privkey_file=os.path.expanduser(settings.tls.certificate) if settings.tls.certificate else None,
user_agent=settings.user_agent,
sample_rate=settings.audio.sample_rate,
rtp_port_range=(settings.rtp.port_range.start, settings.rtp.port_range.end),
trace_sip=settings.logs.trace_sip or self.logger.sip_to_stdout,
log_level=settings.logs.pjsip_level if (settings.logs.trace_pjsip or self.logger.pjsip_to_stdout) else 0
)
self.target = ToHeader(SIPURI(user=self.account.id.username, host=self.account.id.domain))
self.output.put('Subscribing to %s for the xcap-diff event' % self.target.uri)
# start the input thread
self.input.start()
# start twisted
try:
reactor.run()
finally:
self.input.stop()
# stop the output
self.output.stop()
self.output.join()
self.logger.stop()
return 0 if self.success else 1
def stop(self):
self.stopping = True
if self.subscription is not None and self.subscription.state.lower() in ('accepted', 'pending', 'active'):
self.subscription.end(timeout=1)
else:
engine = Engine()
engine.stop()
def print_help(self):
message = 'Available control keys:\n'
message += ' t: toggle SIP trace on the console\n'
message += ' j: toggle PJSIP trace on the console\n'
message += ' n: toggle notifications trace on the console\n'
message += ' Ctrl-d: quit the program\n'
message += ' ?: display this help message\n'
self.output.put('\n'+message)
def handle_notification(self, notification):
handler = getattr(self, '_NH_%s' % notification.name, None)
if handler is not None:
handler(notification)
def _NH_SIPSubscriptionDidStart(self, notification):
route = Route(notification.sender.route_header.uri.host, notification.sender.route_header.uri.port, notification.sender.route_header.uri.parameters.get('transport', 'udp'))
self._subscription_routes = None
self._subscription_wait = 0.5
self.output.put('Subscription succeeded at %s:%d;transport=%s' % (route.address, route.port, route.transport))
self.success = True
def _NH_SIPSubscriptionChangedState(self, notification):
route = Route(notification.sender.route_header.uri.host, notification.sender.route_header.uri.port, notification.sender.route_header.uri.parameters.get('transport', 'udp'))
if notification.data.state.lower() == "pending":
self.output.put('Subscription pending at %s:%d;transport=%s' % (route.address, route.port, route.transport))
elif notification.data.state.lower() == "active":
self.output.put('Subscription active at %s:%d;transport=%s' % (route.address, route.port, route.transport))
def _NH_SIPSubscriptionDidEnd(self, notification):
notification_center = NotificationCenter()
notification_center.remove_observer(self, sender=notification.sender)
self.subscription = None
route = Route(notification.sender.route_header.uri.host, notification.sender.route_header.uri.port, notification.sender.route_header.uri.parameters.get('transport', 'udp'))
self.output.put('Unsubscribed from %s:%d;transport=%s' % (route.address, route.port, route.transport))
self.stop()
def _NH_SIPSubscriptionDidFail(self, notification):
notification_center = NotificationCenter()
notification_center.remove_observer(self, sender=notification.sender)
self.subscription = None
route = Route(notification.sender.route_header.uri.host, notification.sender.route_header.uri.port, notification.sender.route_header.uri.parameters.get('transport', 'udp'))
if notification.data.code:
status = ': %d %s' % (notification.data.code, notification.data.reason)
else:
status = ': %s' % notification.data.reason
self.output.put('Subscription failed at %s:%d;transport=%s%s' % (route.address, route.port, route.transport, status))
if self.stopping or notification.data.code in (401, 403, 407, 489) or self.success:
self.success = False
self.stop()
else:
if not self._subscription_routes or time() > self._subscription_timeout:
self._subscription_wait = min(self._subscription_wait*2, 30)
timeout = random.uniform(self._subscription_wait, 2*self._subscription_wait)
reactor.callFromThread(reactor.callLater, timeout, self._subscribe)
else:
route = self._subscription_routes.popleft()
route_header = RouteHeader(route.uri)
self.subscription = Subscription(self.target.uri,
FromHeader(self.account.uri, self.account.display_name),
self.target,
ContactHeader(self.account.contact[route]),
b"xcap-diff",
route_header,
credentials=self.account.credentials,
refresh=self.account.sip.subscribe_interval)
notification_center.add_observer(self, sender=self.subscription)
self.subscription.subscribe(body=self.body.encode(), content_type=self.content_type, timeout=5)
def _NH_SIPSubscriptionGotNotify(self, notification):
if notification.data.content_type == XCAPDiffDocument.content_type:
try:
xcap_diff = XCAPDiffDocument.parse(notification.data.body)
except ParserError as e:
self.output.put("xcap-diff document is invalid: %s" % str(e))
else:
self._display_xcapdiff(xcap_diff)
self.print_help()
def _NH_DNSLookupDidSucceed(self, notification):
# create subscription and register to get notifications from it
self._subscription_routes = deque(notification.data.result)
route = self._subscription_routes.popleft()
route_header = RouteHeader(route.uri)
self.subscription = Subscription(self.target.uri,
FromHeader(self.account.uri, self.account.display_name),
self.target,
ContactHeader(self.account.contact[route]),
b"xcap-diff",
route_header,
credentials=self.account.credentials,
refresh=self.account.sip.subscribe_interval)
notification_center = NotificationCenter()
notification_center.add_observer(self, sender=self.subscription)
self.subscription.subscribe(body=self.body, content_type=self.content_type, timeout=5)
def _NH_DNSLookupDidFail(self, notification):
self.output.put('DNS lookup failed: %s' % notification.data.error)
timeout = random.uniform(1.0, 2.0)
reactor.callLater(timeout, self._subscribe)
def _NH_SAInputWasReceived(self, notification):
engine = Engine()
settings = SIPSimpleSettings()
key = notification.data.input
if key == 't':
self.logger.sip_to_stdout = not self.logger.sip_to_stdout
engine.trace_sip = self.logger.sip_to_stdout or settings.logs.trace_sip
self.output.put('SIP tracing to console is now %s.' % ('activated' if self.logger.sip_to_stdout else 'deactivated'))
elif key == 'j':
self.logger.pjsip_to_stdout = not self.logger.pjsip_to_stdout
engine.log_level = settings.logs.pjsip_level if (self.logger.pjsip_to_stdout or settings.logs.trace_pjsip) else 0
self.output.put('PJSIP tracing to console is now %s.' % ('activated' if self.logger.pjsip_to_stdout else 'deactivated'))
elif key == 'n':
self.logger.notifications_to_stdout = not self.logger.notifications_to_stdout
self.output.put('Notification tracing to console is now %s.' % ('activated' if self.logger.notifications_to_stdout else 'deactivated'))
elif key == '?':
self.print_help()
def _NH_SIPEngineDidStart(self, notification):
self._subscribe()
@run_in_twisted_thread
def _NH_SIPEngineDidEnd(self, notification):
self._stop_reactor()
@run_in_twisted_thread
def _NH_SIPEngineDidFail(self, notification):
self.output.put('Engine failed.')
self._stop_reactor()
def _NH_SIPEngineGotException(self, notification):
self.output.put('An exception occured within the SIP core:\n'+notification.data.traceback)
def _stop_reactor(self):
try:
reactor.stop()
except ReactorNotRunning:
pass
def _subscribe(self):
settings = SIPSimpleSettings()
self._subscription_timeout = time()+30
lookup = DNSLookup()
notification_center = NotificationCenter()
notification_center.add_observer(self, sender=lookup)
if self.account.sip.outbound_proxy is not None:
uri = SIPURI(host=self.account.sip.outbound_proxy.host, port=self.account.sip.outbound_proxy.port, parameters={'transport': self.account.sip.outbound_proxy.transport})
else:
uri = self.target.uri
lookup.lookup_sip_proxy(uri, settings.sip.transport_list, tls_name=self.account.sip.tls_name)
def _display_xcapdiff(self, xcap_diff):
message = []
message.append('XCAP diff for XCAP root %s' % xcap_diff.xcap_root)
for child in xcap_diff:
if isinstance(child, Document):
message.append(' %s document %s for AUID %s changed' % ('Global' if child.selector.globaltree is not None else "User's %s" % child.selector.userstree, child.selector.document, child.selector.auid))
message.append(' URL: %s' % child.selector)
if child.previous_etag:
message.append(' Previous ETag: %s' % child.previous_etag)
if child.new_etag:
message.append(' New ETag: %s' % child.new_etag)
if child.empty_body:
message.append(' Body did not change')
elif isinstance(child, Element):
message.append(' %s element %s in document %s for AUID %s changed' % ('Global' if child.selector.globaltree is not None else "User's %s" % child.selector.userstree, child.selector.node, child.selector.document, child.selector.auid))
message.append(' URL: %s/%s' % (xcap_diff.xcap_root, child.selector))
elif isinstance(child, Attribute):
message.append(' %s attribute %s in document %s for AUID %s changed' % ('Global' if child.selector.globaltree is not None else "User's %s" % child.selector.userstree, child.selector.node, child.selector.document, child.selector.auid))
message.append(' URL: %s/%s' % (xcap_diff.xcap_root, child.selector))
if child.value:
message.append(' New value: %s' % child.value)
self.output.put('\n'.join(message))
if __name__ == "__main__":
description = "This script subscribes to the xcap-diff event package for the given SIP account. The program will un-SUBSCRIBE and quit when CTRL+D is pressed."
usage = "%prog [options]"
parser = OptionParser(usage=usage, description=description)
parser.print_usage = parser.print_help
parser.add_option("-a", "--account-name", type="string", dest="account_name", help="The name of the account to use.")
parser.add_option("-s", "--trace-sip", action="store_true", dest="trace_sip", default=False, help="Dump the raw contents of incoming and outgoing SIP messages (disabled by default).")
parser.add_option("-j", "--trace-pjsip", action="store_true", dest="trace_pjsip", default=False, help="Print PJSIP logging output (disabled by default).")
parser.add_option("-n", "--trace-notifications", action="store_true", dest="trace_notifications", default=False, help="Print all notifications (disabled by default).")
options, args = parser.parse_args()
try:
application = SubscriptionApplication(options.account_name, options.trace_sip, options.trace_pjsip, options.trace_notifications)
return_code = application.run()
except RuntimeError as e:
print("Error: %s" % str(e))
sys.exit(1)
except SIPCoreError as e:
print("Error: %s" % str(e))
sys.exit(1)
else:
sys.exit(return_code)

File Metadata

Mime Type
text/x-diff
Expires
Sat, Nov 23, 4:51 AM (1 d, 3 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
3408818
Default Alt Text
(411 KB)

Event Timeline