diff --git a/sip-session b/sip-session index e0ab7ef..43ec962 100755 --- a/sip-session +++ b/sip-session @@ -1,1974 +1,2123 @@ #!/usr/bin/python2 import os import re import signal import sys from datetime import datetime from itertools import chain from lxml import html from optparse import OptionParser +from otr import OTRState, SMPStatus from threading import Event, Thread from time import sleep from application import log from application.notification import IObserver, NotificationCenter from application.python import Null from eventlib import api from twisted.internet import reactor from zope.interface import implements from sipsimple.core import Engine, SIPCoreError, SIPURI, ToHeader from sipsimple.account import Account, AccountManager, BonjourAccount from sipsimple.application import SIPApplication from sipsimple.audio import WavePlayer from sipsimple.configuration import ConfigurationError from sipsimple.configuration.settings import SIPSimpleSettings from sipsimple.lookup import DNSLookup from sipsimple.session import IllegalStateError, Session from sipsimple.streams import MediaStreamRegistry from sipsimple.streams.msrp.filetransfer import FileSelector from sipsimple.storage import FileStorage from sipsimple.threading.green import run_in_green_thread from sipclient.configuration import config_directory from sipclient.configuration.account import AccountExtension from sipclient.configuration.datatypes import ResourcePath from sipclient.configuration.settings import SIPSimpleSettingsExtension from sipclient.log import Logger from sipclient.system import IPAddressMonitor from sipclient.ui import Prompt, Question, RichText, UI # This is a helper function for sending formatted notice messages def send_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(send_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 OutgoingCallInitializer(object): implements(IObserver) def __init__(self, account, target, audio=False, chat=False): self.account = account self.target = target self.streams = [] if audio: self.streams.append(MediaStreamRegistry.AudioStream()) if chat: self.streams.append(MediaStreamRegistry.ChatStream()) self.wave_ringtone = None def start(self): if isinstance(self.account, BonjourAccount) and '@' not in self.target: send_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: send_notice('Illegal SIP URI: %s' % self.target) else: if '.' not in self.target.host and not isinstance(self.account, BonjourAccount): self.target.host = '%s.%s' % (self.target.host, self.account.id.domain) lookup = DNSLookup() notification_center = NotificationCenter() notification_center.add_observer(self, sender=lookup) settings = SIPSimpleSettings() if isinstance(self.account, Account) and self.account.sip.outbound_proxy is not None: uri = SIPURI(host=self.account.sip.outbound_proxy.host, port=self.account.sip.outbound_proxy.port, parameters={'transport': self.account.sip.outbound_proxy.transport}) elif isinstance(self.account, Account) and self.account.sip.always_use_my_proxy: uri = SIPURI(host=self.account.id.domain) else: uri = self.target lookup.lookup_sip_proxy(uri, settings.sip.transport_list) def handle_notification(self, notification): handler = getattr(self, '_NH_%s' % notification.name, Null) handler(notification) def _NH_DNSLookupDidSucceed(self, notification): notification_center = NotificationCenter() notification_center.remove_observer(self, sender=notification.sender) session = Session(self.account) notification_center.add_observer(self, sender=session) session.connect(ToHeader(self.target), routes=notification.data.result, streams=self.streams) application = SIPSessionApplication() application.outgoing_session = session def _NH_DNSLookupDidFail(self, notification): send_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) send_notice("Initiating SIP session 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() SIPApplication.voice_audio_bridge.remove(self.wave_ringtone) self.wave_ringtone = None ui.status = 'Connecting...' def _NH_SIPSessionDidStart(self, notification): notification_center = NotificationCenter() ui = UI() session = notification.sender notification_center.remove_observer(self, sender=session) ui.status = 'Connected' reactor.callLater(2, setattr, ui, 'status', None) application = SIPSessionApplication() application.outgoing_session = None for stream in notification.data.streams: if stream.type == 'audio': send_notice('Audio session established using "%s" codec at %sHz' % (stream.codec, stream.sample_rate)) if stream.ice_active: send_notice('Audio RTP endpoints %s:%d (ICE type %s) <-> %s:%d (ICE type %s)' % (stream.local_rtp_address, stream.local_rtp_port, stream.local_rtp_candidate.type.lower(), stream.remote_rtp_address, stream.remote_rtp_port, stream.remote_rtp_candidate.type.lower())) else: send_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: send_notice('RTP audio stream is encrypted using %s (%s)\n' % (stream.encryption.type, stream.encryption.cipher)) if session.remote_user_agent is not None: send_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: send_notice('SIP session cancelled') elif notification.data.failure_reason == 'user request': send_notice('SIP session rejected by user (%d %s)' % (notification.data.code, notification.data.reason)) else: send_notice('SIP session failed: %s' % notification.data.failure_reason) class IncomingCallInitializer(object): implements(IObserver) sessions = 0 tone_ringtone = None def __init__(self, session, auto_answer_interval=None): self.session = session self.auto_answer_interval = auto_answer_interval self.question = None def start(self): IncomingCallInitializer.sessions += 1 notification_center = NotificationCenter() notification_center.add_observer(self, sender=self.session) # start auto-answer self.answer_timer = None if self.auto_answer_interval == 0: self.session.accept(self.session.proposed_streams) return elif self.auto_answer_interval > 0: self.answer_timer = reactor.callFromThread(reactor.callLater, self.auto_answer_interval, self.session.accept, self.session.proposed_streams) # start ringing application = SIPSessionApplication() self.wave_ringtone = None if application.active_session is None: if IncomingCallInitializer.sessions == 1: ringtone = self.session.account.sounds.audio_inbound.sound_file if self.session.account.sounds.audio_inbound is not None else None if ringtone: self.wave_ringtone = WavePlayer(SIPApplication.alert_audio_mixer, ringtone.path.normalized, volume=ringtone.volume, loop_count=0, pause_time=2) SIPApplication.alert_audio_bridge.add(self.wave_ringtone) self.wave_ringtone.start() elif IncomingCallInitializer.tone_ringtone is None: IncomingCallInitializer.tone_ringtone = WavePlayer(SIPApplication.voice_audio_mixer, ResourcePath('sounds/ring_tone.wav').normalized, loop_count=0, pause_time=6) SIPApplication.voice_audio_bridge.add(IncomingCallInitializer.tone_ringtone) IncomingCallInitializer.tone_ringtone.start() self.session.send_ring_indication() # ask question identity = str(self.session.remote_identity.uri) if self.session.remote_identity.display_name: identity = '"%s" <%s>' % (self.session.remote_identity.display_name, identity) streams = '/'.join(stream.type for stream in self.session.proposed_streams) self.question = Question("Incoming %s from '%s', do you want to accept? (a)ccept/(r)eject/(b)usy" % (streams, identity), 'arbi', bold=True) notification_center.add_observer(self, sender=self.question) ui = UI() ui.add_question(self.question) def handle_notification(self, notification): handler = getattr(self, '_NH_%s' % notification.name, Null) handler(notification) def _NH_UIQuestionGotAnswer(self, notification): notification_center = NotificationCenter() ui = UI() notification_center.remove_observer(self, sender=notification.sender) answer = notification.data.answer self.question = None if answer == 'a': self.session.accept(self.session.proposed_streams) ui.status = 'Accepting...' elif answer == 'r': self.session.reject() ui.status = 'Rejecting...' elif answer == 'b': self.session.reject(486) ui.status = 'Sending Busy Here...' if self.wave_ringtone: self.wave_ringtone.stop() self.wave_ringtone = None if IncomingCallInitializer.sessions > 1: if IncomingCallInitializer.tone_ringtone is None: IncomingCallInitializer.tone_ringtone = WavePlayer(SIPApplication.voice_audio_mixer, ResourcePath('sounds/ring_tone.wav').normalized, loop_count=0, pause_time=6) SIPApplication.voice_audio_bridge.add(IncomingCallInitializer.tone_ringtone) IncomingCallInitializer.tone_ringtone.start() elif IncomingCallInitializer.tone_ringtone: IncomingCallInitializer.tone_ringtone.stop() IncomingCallInitializer.tone_ringtone = None if self.answer_timer is not None and self.answer_timer.active(): self.answer_timer.cancel() def _NH_SIPSessionWillStart(self, notification): ui = UI() if self.question is not None: notification_center = NotificationCenter() notification_center.remove_observer(self, sender=self.question) ui.remove_question(self.question) self.question = None ui.status = 'Connecting...' def _NH_SIPSessionDidStart(self, notification): notification_center = NotificationCenter() session = notification.sender notification_center.remove_observer(self, sender=session) IncomingCallInitializer.sessions -= 1 ui = UI() ui.status = 'Connected' reactor.callLater(2, setattr, ui, 'status', None) identity = str(session.remote_identity.uri) if session.remote_identity.display_name: identity = '"%s" <%s>' % (session.remote_identity.display_name, identity) send_notice("SIP session with '%s' established" % identity) for stream in notification.data.streams: if stream.type == 'audio': send_notice('Audio stream using "%s" codec at %sHz' % (stream.codec, stream.sample_rate)) if stream.ice_active: send_notice('Audio RTP endpoints %s:%d (ICE type %s) <-> %s:%d (ICE type %s)' % (stream.local_rtp_address, stream.local_rtp_port, stream.local_rtp_candidate_type, stream.remote_rtp_address, stream.remote_rtp_port, stream.remote_rtp_candidate_type)) else: send_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: send_notice('RTP audio stream is encrypted using %s (%s)\n' % (stream.encryption.type, stream.encryption.cipher)) if session.remote_user_agent is not None: send_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: send_notice('SIP session cancelled by user') if notification.data.failure_reason == 'Call completed elsewhere' and notification.data.code == 487: send_notice('SIP session cancelled, call was answered elsewhere') elif notification.data.failure_reason == 'user request': send_notice('SIP session rejected (%d %s)' % (notification.data.code, notification.data.reason)) else: send_notice('SIP session failed: %s' % notification.data.failure_reason) class OutgoingProposalHandler(object): implements(IObserver) def __init__(self, session, audio=False, chat=False): self.session = session self.stream = None if audio: self.stream = MediaStreamRegistry.AudioStream() if chat: self.stream = MediaStreamRegistry.ChatStream() if not self.stream: raise ValueError("Need to specify exactly one stream") def start(self): notification_center = NotificationCenter() notification_center.add_observer(self, sender=self.session) try: self.session.add_stream(self.stream) except IllegalStateError: notification_center.remove_observer(self, sender=self.session) raise remote_identity = str(self.session.remote_identity.uri) if self.session.remote_identity.display_name: remote_identity = '"%s" <%s>' % (self.session.remote_identity.display_name, remote_identity) send_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) send_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: send_notice('Proposal cancelled (%d %s)' % (notification.data.code, notification.data.reason)) else: send_notice('Proposal rejected (%d %s)' % (notification.data.code, notification.data.reason)) def _NH_SIPSessionDidEnd(self, notification): notification_center = NotificationCenter() notification_center.remove_observer(self, sender=self.session) class IncomingProposalHandler(object): implements(IObserver) sessions = 0 tone_ringtone = None def __init__(self, session): self.session = session self.question = None def start(self): IncomingProposalHandler.sessions += 1 notification_center = NotificationCenter() notification_center.add_observer(self, sender=self.session) # start ringing if IncomingProposalHandler.tone_ringtone is None: IncomingProposalHandler.tone_ringtone = WavePlayer(SIPApplication.voice_audio_mixer, ResourcePath('sounds/ring_tone.wav').normalized, loop_count=0, pause_time=6) SIPApplication.voice_audio_bridge.add(IncomingProposalHandler.tone_ringtone) IncomingProposalHandler.tone_ringtone.start() self.session.send_ring_indication() # ask question identity = str(self.session.remote_identity.uri) if self.session.remote_identity.display_name: identity = '"%s" <%s>' % (self.session.remote_identity.display_name, identity) streams = ', '.join(stream.type for stream in self.session.proposed_streams) self.question = Question("'%s' wants to add %s, do you want to accept? (a)ccept/(r)eject" % (identity, streams), 'ar', bold=True) notification_center.add_observer(self, sender=self.question) ui = UI() ui.add_question(self.question) def handle_notification(self, notification): handler = getattr(self, '_NH_%s' % notification.name, Null) handler(notification) def _NH_UIQuestionGotAnswer(self, notification): notification_center = NotificationCenter() ui = UI() notification_center.remove_observer(self, sender=notification.sender) answer = notification.data.answer self.question = None if answer == 'a': self.session.accept_proposal(self.session.proposed_streams) ui.status = 'Accepting proposal...' elif answer == 'r': self.session.reject_proposal() ui.status = 'Rejecting proposal...' if IncomingProposalHandler.sessions == 1 and IncomingProposalHandler.tone_ringtone: IncomingProposalHandler.tone_ringtone.stop() IncomingProposalHandler.tone_ringtone = None def _NH_SIPSessionProposalAccepted(self, notification): notification_center = NotificationCenter() session = notification.sender notification_center.remove_observer(self, sender=session) application = SIPSessionApplication() application.sessions_with_proposals.remove(notification.sender) IncomingProposalHandler.sessions -= 1 ui = UI() ui.status = None send_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: send_notice('Proposal cancelled (%d %s)' % (notification.data.code, notification.data.reason)) else: send_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 send_notice('Proposal failed (%s)' % notification.data.failure_reason) def _NH_SIPSessionDidEnd(self, notification): notification_center = NotificationCenter() ui = UI() session = notification.sender notification_center.remove_observer(self, sender=session) ui.status = None if self.question is not None: notification_center.remove_observer(self, sender=self.question) ui.remove_question(self.question) self.question = None IncomingProposalHandler.sessions -= 1 if IncomingProposalHandler.sessions == 0 and IncomingProposalHandler.tone_ringtone is not None: IncomingProposalHandler.tone_ringtone.stop() IncomingProposalHandler.tone_ringtone = None class OutgoingTransferHandler(object): implements(IObserver) def __init__(self, account, target, filepath): self.account = account self.target = target self.filepath = filepath.decode(sys.getfilesystemencoding()) self.file_selector = None self.hash_compute_proc = None self.session = None self.stream = None self.handler = None self.wave_ringtone = None @run_in_green_thread def start(self): if isinstance(self.account, BonjourAccount) and '@' not in self.target: send_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: send_notice('Illegal SIP URI: %s' % self.target) return send_notice('Preparing transfer...') self.file_selector = FileSelector.for_file(self.filepath) if '.' not in self.target.host and not isinstance(self.account, BonjourAccount): self.target.host = '%s.%s' % (self.target.host, self.account.id.domain) lookup = DNSLookup() notification_center = NotificationCenter() notification_center.add_observer(self, sender=lookup) settings = SIPSimpleSettings() if isinstance(self.account, Account) and self.account.sip.outbound_proxy is not None: uri = SIPURI(host=self.account.sip.outbound_proxy.host, port=self.account.sip.outbound_proxy.port, parameters={'transport': self.account.sip.outbound_proxy.transport}) elif isinstance(self.account, Account) and self.account.sip.always_use_my_proxy: uri = SIPURI(host=self.account.id.domain) else: uri = self.target lookup.lookup_sip_proxy(uri, settings.sip.transport_list) def _terminate(self, failure_reason=None): notification_center = NotificationCenter() notification_center.remove_observer(self, sender=self.session) notification_center.remove_observer(self, sender=self.stream) notification_center.remove_observer(self, sender=self.handler) ui = UI() ui.status = None if self.wave_ringtone: self.wave_ringtone.stop() self.wave_ringtone = None if failure_reason is None: send_notice('File transfer of %s finished' % os.path.basename(self.filepath)) else: send_notice('File transfer of %s failed: %s' % (os.path.basename(self.filepath), failure_reason)) self.session = None self.stream = None self.handler = None def handle_notification(self, notification): handler = getattr(self, '_NH_%s' % notification.name, Null) handler(notification) def _NH_DNSLookupDidSucceed(self, notification): notification.center.remove_observer(self, sender=notification.sender) self.session = Session(self.account) self.stream = MediaStreamRegistry.FileTransferStream(self.file_selector, 'sendonly') self.handler = self.stream.handler notification.center.add_observer(self, sender=self.session) notification.center.add_observer(self, sender=self.stream) notification.center.add_observer(self, sender=self.handler) self.session.connect(ToHeader(self.target), routes=notification.data.result, streams=[self.stream]) def _NH_DNSLookupDidFail(self, notification): notification.center.remove_observer(self, sender=notification.sender) send_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) send_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) send_notice("File transfer for %s to '%s' started" % (os.path.basename(self.filepath), identity)) def _NH_MediaStreamDidNotInitialize(self, notification): self._terminate(failure_reason=notification.data.reason) def _NH_FileTransferHandlerDidEnd(self, notification): self.session.end() self._terminate(failure_reason=notification.data.reason) class IncomingTransferHandler(object): implements(IObserver) sessions = 0 tone_ringtone = None def __init__(self, session, auto_answer_interval=None): self.session = session self.stream = None self.handler = None self.auto_answer_interval = auto_answer_interval self.file = None self.filename = None self.finished = False self.hash = None self.question = None self.wave_ringtone = None def start(self): self.stream = self.session.proposed_streams[0] self.handler = self.stream.handler self.file_selector = self.stream.file_selector self.filename = self.file_selector.name IncomingTransferHandler.sessions += 1 notification_center = NotificationCenter() notification_center.add_observer(self, sender=self.session) notification_center.add_observer(self, sender=self.stream) notification_center.add_observer(self, sender=self.handler) # start auto-answer self.answer_timer = None if self.auto_answer_interval == 0: self.session.accept(self.session.proposed_streams) return elif self.auto_answer_interval > 0: self.answer_timer = reactor.callFromThread(reactor.callLater, self.auto_answer_interval, self.session.accept, self.session.proposed_streams) # start ringing application = SIPSessionApplication() if application.active_session is None: if IncomingTransferHandler.sessions == 1: ringtone = self.session.account.sounds.audio_inbound.sound_file if self.session.account.sounds.audio_inbound is not None else None if ringtone: self.wave_ringtone = WavePlayer(SIPApplication.alert_audio_mixer, ringtone.path.normalized, volume=ringtone.volume, loop_count=0, pause_time=2) SIPApplication.alert_audio_bridge.add(self.wave_ringtone) self.wave_ringtone.start() elif IncomingTransferHandler.tone_ringtone is None: IncomingTransferHandler.tone_ringtone = WavePlayer(SIPApplication.voice_audio_mixer, ResourcePath('sounds/ring_tone.wav').normalized, loop_count=0, pause_time=6) SIPApplication.voice_audio_bridge.add(IncomingTransferHandler.tone_ringtone) IncomingTransferHandler.tone_ringtone.start() self.session.send_ring_indication() # ask question identity = str(self.session.remote_identity.uri) if self.session.remote_identity.display_name: identity = '"%s" <%s>' % (self.session.remote_identity.display_name, identity) self.question = Question("Incoming file transfer for %s from '%s', do you want to accept? (a)ccept/(r)eject" % (os.path.basename(self.filename), identity), 'ari', bold=True) notification_center.add_observer(self, sender=self.question) ui = UI() ui.add_question(self.question) def _terminate(self, failure_reason=None): notification_center = NotificationCenter() notification_center.remove_observer(self, sender=self.session) notification_center.remove_observer(self, sender=self.stream) notification_center.remove_observer(self, sender=self.handler) ui = UI() ui.status = None if self.question is not None: notification_center.remove_observer(self, sender=self.question) ui.remove_question(self.question) self.question = None if self.wave_ringtone: self.wave_ringtone.stop() if failure_reason is None: send_notice('File transfer of %s finished' % os.path.basename(self.filename)) else: send_notice('File transfer of %s failed: %s' % (os.path.basename(self.filename), failure_reason)) self.session = None self.stream = None self.handler = None def handle_notification(self, notification): handler = getattr(self, '_NH_%s' % notification.name, Null) handler(notification) def _NH_UIQuestionGotAnswer(self, notification): notification_center = NotificationCenter() ui = UI() notification_center.remove_observer(self, sender=notification.sender) answer = notification.data.answer self.question = None if answer == 'a': self.session.accept(self.session.proposed_streams) ui.status = 'Accepting...' elif answer == 'r': self.session.reject() ui.status = 'Rejecting...' if IncomingTransferHandler.sessions == 1: if self.wave_ringtone: self.wave_ringtone.stop() self.wave_ringtone = None if IncomingTransferHandler.tone_ringtone: IncomingTransferHandler.tone_ringtone.stop() IncomingTransferHandler.tone_ringtone = None if self.answer_timer is not None and self.answer_timer.active(): self.answer_timer.cancel() def _NH_SIPSessionWillStart(self, notification): ui = UI() if self.question is not None: notification_center = NotificationCenter() notification_center.remove_observer(self, sender=self.question) ui.remove_question(self.question) self.question = None ui.status = 'Connecting...' notification_center = NotificationCenter() notification_center.add_observer(self, sender=notification.sender.proposed_streams[0]) def _NH_SIPSessionDidStart(self, notification): session = notification.sender IncomingCallInitializer.sessions -= 1 ui = UI() ui.status = 'File transfer connected' identity = str(session.remote_identity.uri) if session.remote_identity.display_name: identity = '"%s" <%s>' % (session.remote_identity.display_name, identity) send_notice("File transfer for %s with '%s' started" % (os.path.basename(self.filename), identity)) if IncomingTransferHandler.sessions == 1: if self.wave_ringtone: self.wave_ringtone.stop() self.wave_ringtone = None if IncomingTransferHandler.tone_ringtone: IncomingTransferHandler.tone_ringtone.stop() IncomingTransferHandler.tone_ringtone = None def _NH_SIPSessionDidFail(self, notification): IncomingTransferHandler.sessions -= 1 if self.wave_ringtone: self.wave_ringtone.stop() self.wave_ringtone = None if IncomingTransferHandler.sessions == 0 and IncomingTransferHandler.tone_ringtone is not None: IncomingTransferHandler.tone_ringtone.stop() IncomingTransferHandler.tone_ringtone = None def _NH_MediaStreamDidNotInitialize(self, notification): self._terminate(failure_reason=notification.data.reason) def _NH_FileTransferHandlerDidInitialize(self, notification): self.filename = self.stream.file_selector.name def _NH_FileTransferHandlerProgress(self, notification): ui = UI() ui.status = '%s: %s%%' % (os.path.basename(self.filename), notification.data.transferred_bytes*100//notification.data.total_bytes) def _NH_FileTransferHandlerDidEnd(self, notification): reactor.callFromThread(reactor.callLater, 0, self.session.end) self._terminate(failure_reason=notification.data.reason) class SIPSessionApplication(SIPApplication): # public methods # def __init__(self): self.account = None self.options = None self.target = None self.active_session = None self.outgoing_session = None self.connected_sessions = [] self.sessions_with_proposals = set() self.hangup_timers = {} self.neighbours = {} self.registration_succeeded = False self.stopped_event = Event() self.ip_address_monitor = IPAddressMonitor() self.logger = None self.rtp_statistics = None self.hold_tone = None self.ignore_local_hold = False self.ignore_local_unhold = False + self.smp_verification_question = 'What is the ZRTP authentication string' + self.smp_verifified_using_zrtp = False + self.smp_verification_delay = 0 + self.smp_verification_tries = 5 + self.smp_secret = None + def start(self, target, options): notification_center = NotificationCenter() ui = UI() self.options = options self.target = target self.logger = Logger(sip_to_stdout=options.trace_sip, msrp_to_stdout=options.trace_msrp, pjsip_to_stdout=options.trace_pjsip, notifications_to_stdout=options.trace_notifications) notification_center.add_observer(self, sender=self) notification_center.add_observer(self, sender=ui) notification_center.add_observer(self, name='SIPSessionNewIncoming') notification_center.add_observer(self, name='SIPSessionNewOutgoing') notification_center.add_observer(self, name='RTPStreamDidChangeRTPParameters') notification_center.add_observer(self, name='RTPStreamICENegotiationDidSucceed') notification_center.add_observer(self, name='RTPStreamICENegotiationDidFail') log.level.current = log.level.WARNING # get rid of twisted messages control_bindings={'s': 'trace sip', 'm': 'trace msrp', 'j': 'trace pjsip', 'n': 'trace notifications', 'h': 'hangup', 'r': 'record', 'i': 'input', 'o': 'output', 'a': 'alert', 'u': 'mute', ' ': 'hold', 'q': 'quit', '/': 'help', '?': 'help', '0': 'dtmf 0', '1': 'dtmf 1', '2': 'dtmf 2', '3': 'dtmf 3', '4': 'dtmf 4', '5': 'dtmf 5', '6': 'dtmf 6', '7': 'dtmf 7', '8': 'dtmf 8', '9': 'dtmf 9', '*': 'dtmf *', '#': 'dtmf #', 'A': 'dtmf A', 'B': 'dtmf B', 'C': 'dtmf C', 'D': 'dtmf D'} ui.start(control_bindings=control_bindings, display_text=False) Account.register_extension(AccountExtension) BonjourAccount.register_extension(AccountExtension) SIPSimpleSettings.register_extension(SIPSimpleSettingsExtension) try: SIPApplication.start(self, FileStorage(options.config_directory or config_directory)) except ConfigurationError, e: send_notice("Failed to load sipclient's configuration: %s\n" % str(e), bold=False) send_notice("If an old configuration file is in place, delete it or move it and recreate the configuration using the sip_settings script.", bold=False) ui.stop() self.stopped_event.set() # notification handlers # def _NH_SIPApplicationWillStart(self, notification): account_manager = AccountManager() notification_center = NotificationCenter() settings = SIPSimpleSettings() ui = UI() settings.logs.trace_sip = self.options.trace_sip settings.logs.trace_msrp = self.options.trace_msrp settings.logs.trace_pjsip = self.options.trace_pjsip settings.logs.trace_notifications = self.options.trace_notifications settings.save() for account in account_manager.iter_accounts(): if isinstance(account, Account): account.sip.register = False account.presence.enabled = False account.xcap.enabled = False account.message_summary.enabled = False 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: send_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: send_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] notification_center.add_observer(self, sender=self.account) if isinstance(self.account, Account): self.account.sip.register = True self.account.presence.enabled = False self.account.xcap.enabled = False self.account.message_summary.enabled = False send_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: send_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: send_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: send_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: send_notice('Logging notifications trace to file "%s"' % self.logger._notifications_filename, bold=False) if self.options.disable_sound: settings.audio.input_device = None settings.audio.output_device = None settings.audio.alert_device = None def _NH_SIPApplicationDidStart(self, notification): settings = SIPSimpleSettings() self.ip_address_monitor.start() # set the file transfer directory if it's not set if settings.file_transfer.directory is None: settings.file_transfer.directory = 'file_transfers' # display a list of available devices self._CH_devices() send_notice('Type /help to see a list of available commands.', bold=False) if self.target is not None: call_initializer = OutgoingCallInitializer(self.account, self.target, audio=True) call_initializer.start() def _NH_SIPApplicationWillEnd(self, notification): 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: send_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: send_notice('Illegal use of command /%s. Type /help for a list of available commands.' % notification.data.command) else: send_notice('Unknown command /%s. Type /help for a list of available commands.' % notification.data.command) def _NH_UIInputGotText(self, notification): msrp_chat = None 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 is None: send_notice('No active chat session') return msrp_chat.send_message(notification.data.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) ui = UI() ui.write(RichText('%s> ' % local_identity, foreground='darkred') + notification.data.text) def _NH_SIPEngineGotException(self, notification): lines = ['An exception occured within the SIP core:'] lines.extend(notification.data.traceback.split('\n')) send_notice(lines) 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 lines = ['%s Registered contact "%s" for sip:%s at %s:%d;transport=%s (expires in %d seconds).' % (datetime.now().replace(microsecond=0), contact_header.uri, self.account.id, registrar.address, registrar.port, registrar.transport, expires)] if len(contact_header_list) > 1: lines.append('Other registered contacts:') lines.extend(' %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) account = notification.sender if account.contact.public_gruu is not None: lines.append('Public GRUU: %s' % account.contact.public_gruu) if account.contact.temporary_gruu is not None: lines.append('Temporary GRUU: %s' % account.contact.temporary_gruu) send_notice(lines) self.registration_succeeded = True def _NH_SIPAccountRegistrationDidFail(self, notification): send_notice('%s Failed to register contact for sip:%s: %s (retrying in %.2f seconds)' % (datetime.now().replace(microsecond=0), self.account.id, notification.data.error, notification.data.retry_after)) self.registration_succeeded = False def _NH_SIPAccountRegistrationDidEnd(self, notification): send_notice('%s Registration ended.' % datetime.now().replace(microsecond=0)) def _NH_BonjourAccountRegistrationDidSucceed(self, notification): send_notice('%s Registered Bonjour contact "%s"' % (datetime.now().replace(microsecond=0), notification.data.name)) def _NH_BonjourAccountRegistrationDidFail(self, notification): send_notice('%s Failed to register Bonjour contact: %s' % (datetime.now().replace(microsecond=0), notification.data.reason)) def _NH_BonjourAccountRegistrationDidEnd(self, notification): send_notice('%s Registration ended.' % datetime.now().replace(microsecond=0)) def _NH_BonjourAccountDidAddNeighbour(self, notification): neighbour, record = notification.data.neighbour, notification.data.record now = datetime.now().replace(microsecond=0) send_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: send_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: send_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: send_notice('%s Bonjour neighbour left: "%s (%s)" <%s>' % (now, bonjour_neighbour.display_name, bonjour_neighbour.host, bonjour_neighbour.uri)) def _NH_SIPSessionNewIncoming(self, notification): session = notification.sender transfer_streams = [stream for stream in session.proposed_streams if stream.type == 'file-transfer' and stream.direction == 'recvonly'] # only allow sessions with 0 or 1 file transfers if transfer_streams: if len(transfer_streams) > 1: session.reject(488) return transfer_handler = IncomingTransferHandler(session, self.options.auto_answer_interval) transfer_handler.start() else: notification_center = NotificationCenter() notification_center.add_observer(self, sender=session) call_initializer = IncomingCallInitializer(session, self.options.auto_answer_interval) call_initializer.start() def _NH_SIPSessionNewOutgoing(self, notification): session = notification.sender transfer_streams = [stream for stream in session.proposed_streams if stream.type == 'file-transfer'] if not transfer_streams: notification_center = NotificationCenter() notification_center.add_observer(self, sender=session) def _NH_SIPSessionDidFail(self, notification): notification_center = NotificationCenter() notification_center.remove_observer(self, sender=notification.sender) def _NH_SIPSessionWillStart(self, notification): notification_center = NotificationCenter() for stream in notification.sender.proposed_streams: notification_center.add_observer(self, sender=stream) def _NH_SIPSessionDidStart(self, notification): session = notification.sender self.connected_sessions.append(session) if self.active_session is not None: self.active_session.hold() self.active_session = session self._update_prompt() if len(self.connected_sessions) > 1: # this displays the conencted sessions self._CH_sessions() if self.options.auto_hangup_interval is not None: if self.options.auto_hangup_interval == 0: session.end() else: timer = reactor.callLater(self.options.auto_hangup_interval, session.end) self.hangup_timers[id(session)] = timer def _NH_SIPSessionWillEnd(self, notification): notification_center = NotificationCenter() session = notification.sender if id(session) in self.hangup_timers: timer = self.hangup_timers[id(session)] if timer.active(): timer.cancel() del self.hangup_timers[id(session)] hangup_tone = WavePlayer(self.voice_audio_mixer, ResourcePath('sounds/hangup_tone.wav').normalized) notification_center.add_observer(self, sender=hangup_tone) self.voice_audio_bridge.add(hangup_tone) hangup_tone.start() def _NH_SIPSessionDidEnd(self, notification): notification_center = NotificationCenter() session = notification.sender notification_center.remove_observer(self, sender=session) for stream in session.streams or session.proposed_streams or []: notification_center.remove_observer(self, sender=stream) ui = UI() ui.status = None identity = str(session.remote_identity.uri) if session.remote_identity.display_name: identity = '"%s" <%s>' % (session.remote_identity.display_name, identity) if notification.data.end_reason == 'user request': send_notice('SIP session with %s ended by %s party' % (identity, notification.data.originator)) else: send_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' send_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.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) send_notice('Active SIP session: "%s" (%d/%d)' % (identity, self.connected_sessions.index(self.active_session)+1, len(self.connected_sessions))) else: self.active_session = None self._update_prompt() if session is self.outgoing_session: self.outgoing_session = None on_hold_streams = [stream for stream in chain(*(session.streams for session in self.connected_sessions)) if stream.on_hold] if not on_hold_streams and self.hold_tone is not None: self.hold_tone.stop() self.hold_tone = None def _NH_SIPSessionDidChangeHoldState(self, notification): session = notification.sender if notification.data.on_hold: if notification.data.originator == 'remote': if session is self.active_session: send_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) send_notice('%s has put the session on hold' % identity) elif not self.ignore_local_hold: if session is self.active_session: send_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) send_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: send_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) send_notice('%s has taken the session out of hold' % identity) elif not self.ignore_local_unhold: if session is self.active_session: send_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) send_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) send_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() send_notice('Got DMTF %s' % notification.data.digit) def _NH_RTPStreamDidChangeRTPParameters(self, notification): stream = notification.sender send_notice('Audio RTP parameters changed:') send_notice('Audio stream using "%s" codec at %sHz' % (stream.codec, stream.sample_rate)) send_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: send_notice('RTP audio stream is encrypted using %s (%s)\n' % (stream.encryption.type, stream.encryption.cipher)) def _NH_AudioStreamDidStartRecordingAudio(self, notification): send_notice('Recording audio to %s' % notification.data.filename) def _NH_AudioStreamDidStopRecordingAudio(self, notification): send_notice('Stopped recording audio to %s' % notification.data.filename) + def _NH_ChatStreamSMPVerificationDidStart(self, notification): + send_notice('OTR SMP verification started') + data = notification.data + session = self.active_session + if data.originator == 'remote': + send_notice("OTR SMP verification requested by 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: + send_notice("OTR SMP verification done automatically using ZRTP SAS") + stream.encryption.smp_answer(audio_stream.encryption.zrtp.sas) + self.smp_verifified_using_zrtp = True + else: + send_notice('OTR verification question: %s' % data.question) + self._update_prompt() + else: + try: + audio_stream = next(stream for stream in session.streams if stream.type=='audio' and stream.encryption.type=='ZRTP' and stream.encryption.active and stream.encryption.zrtp.verified) + except StopIteration: + audio_stream = None + + if audio_stream: + self.stream.encryption.smp_verify(audio_stream.encryption.zrtp.sas, question=self.smp_verification_question) + send_notice("Performing OTR SMP verification using ZRTP SAS...") + self._update_prompt() + + def _NH_ChatStreamSMPVerificationDidNotStart(self, notification): + send_notice('OTR SMP verification did not start') + + def _NH_ChatStreamSMPVerificationDidEnd(self, notification): + data = notification.data + stream = notification.sender + if data.status is SMPStatus.Success: + send_notice("OTR SMP verification succeeded") + stream.encryption.verified = True + self._update_prompt() + elif data.status is SMPStatus.Interrupted: + send_notice("OTR SMP verification aborted: %s" % data.reason) + elif data.status is SMPStatus.ProtocolError: + send_notice("OTR SMP verification error: %s" % data.reason) + 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): + send_notice("Chat encryption error: %s", notification.data.error) + + def _NH_ChatStreamOTRVerifiedStateChanged(self, notification): + self._update_prompt() + + def _NH_ChatStreamOTREncryptionStateChanged(self, notification): + data = notification.data + stream = notification.sender + if data.new_state is OTRState.Encrypted: + local_fingerprint = stream.encryption.key_fingerprint.encode('hex').upper() + remote_fingerprint = stream.encryption.peer_fingerprint.encode('hex').upper() + send_notice("Chat encryption activated using OTR protocol") + send_notice("OTR local fingerprint %s" % local_fingerprint) + send_notice("OTR remote fingerprint %s" % remote_fingerprint) + + if stream.encryption.verified: + send_notice("OTR remote fingerprint has been verified") + else: + send_notice("OTR remote fingerprint has not yet been verified") + + elif data.new_state is OTRState.Finished: + send_notice("Chat encryption deactivated") + elif data.new_state is OTRState.Plaintext: + send_notice("Chat encryption deactivated") + + 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': send_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': send_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': send_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: send_notice('Added audio device(s): %s' % ', '.join(sorted(added_devices))) if removed_devices: send_notice('Removed audio device(s): %s' % ', '.join(sorted(removed_devices))) if changed_input_device: send_notice('Input device has been switched to: %s' % self.voice_audio_mixer.real_input_device) if changed_output_device: send_notice('Output device has been switched to: %s' % self.voice_audio_mixer.real_output_device) if changed_alert_device: send_notice('Alert device has been switched to: %s' % self.alert_audio_mixer.real_output_device) def _NH_WavePlayerDidEnd(self, notification): notification_center = NotificationCenter() notification_center.remove_observer(self, sender=notification.sender) def _NH_RTPStreamICENegotiationDidSucceed(self, notification): send_notice(" ") send_notice("ICE negotiation succeeded in %s" % notification.data.duration) if self.rtp_statistics: send_notice(" ") send_notice("Local ICE candidates:") for candidate in notification.data.local_candidates: send_notice(str(candidate)) send_notice(" ") send_notice("Remote ICE candidates:") for candidate in notification.data.remote_candidates: send_notice(str(candidate)) send_notice(" ") send_notice("ICE connectivity valid pairs:") for check in notification.data.valid_pairs: send_notice(str(check)) send_notice(" ") def _NH_RTPStreamICENegotiationDidFail(self, notification): send_notice("\n") send_notice("ICE negotiation failed: %s" % notification.data.reason) # command handlers # + def _CH_call(self, target): if self.outgoing_session is not None: send_notice('Please cancel any outgoing sessions before makeing any new ones') return call_initializer = OutgoingCallInitializer(self.account, target, audio=True, chat=True) call_initializer.start() def _CH_audio(self, target, chat_option=None): if chat_option and chat_option != '+chat': raise TypeError() if self.outgoing_session is not None: send_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_chat(self, target, audio_option=None): if audio_option and audio_option != '+audio': raise TypeError() if self.outgoing_session is not None: send_notice('Please cancel any outgoing sessions before makeing any new ones') return call_initializer = OutgoingCallInitializer(self.account, target, audio=audio_option=='+audio', chat=True) call_initializer.start() def _CH_send(self, target, filepath): if '~' in filepath: filepath = os.path.expanduser(filepath) filepath = os.path.abspath(filepath) transfer_handler = OutgoingTransferHandler(self.account, target, filepath) transfer_handler.start() def _CH_next(self): if len(self.connected_sessions) > 1: self.active_session.hold() self.active_session = self.connected_sessions[(self.connected_sessions.index(self.active_session)+1) % len(self.connected_sessions)] self.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) send_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.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) send_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') send_notice(lines) else: send_notice('There are no connected sessions') def _CH_neighbours(self): if not isinstance(self.account, BonjourAccount): send_notice('This command is only available if using the Bonjour account') return lines = ['Bonjour neighbours:'] for key, neighbour in self.neighbours.iteritems(): lines.append(' "%s (%s)" <%s>' % (neighbour.display_name, neighbour.host, neighbour.uri)) send_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')) send_notice(lines) return add_types = [type[1:] for type in types if type[0] == '+'] remove_types = [type[1:] for type in types if type[0] == '-'] toggle_types = [type for type in types if type[0] not in ('+', '-')] settings = SIPSimpleSettings() if 'sip' in add_types or ('sip' in toggle_types and not self.logger.sip_to_stdout): self.logger.sip_to_stdout = True settings.logs.trace_sip = True send_notice('SIP tracing to console is now activated') elif 'sip' in remove_types or ('sip' in toggle_types and self.logger.sip_to_stdout): self.logger.sip_to_stdout = False settings.logs.trace_sip = False send_notice('SIP tracing to console is now deactivated') if 'msrp' in add_types or ('msrp' in toggle_types and not self.logger.msrp_to_stdout): self.logger.msrp_to_stdout = True settings.logs.trace_msrp = True send_notice('MSRP tracing to console is now activated') elif 'msrp' in remove_types or ('msrp' in toggle_types and self.logger.msrp_to_stdout): self.logger.msrp_to_stdout = False settings.logs.trace_msrp = False send_notice('MSRP tracing to console is now deactivated') if 'pjsip' in add_types or ('pjsip' in toggle_types and not self.logger.pjsip_to_stdout): self.logger.pjsip_to_stdout = True settings.logs.trace_pjsip = True send_notice('PJSIP tracing to console is now activated') elif 'pjsip' in remove_types or ('pjsip' in toggle_types and self.logger.pjsip_to_stdout): self.logger.pjsip_to_stdout = False settings.logs.trace_pjsip = False send_notice('PJSIP tracing to console is now deactivated') if 'notifications' in add_types or ('notifications' in toggle_types and not self.logger.notifications_to_stdout): self.logger.notifications_to_stdout = True settings.logs.trace_notifications = True send_notice('Notification tracing to console is now activated') elif 'notifications' in remove_types or ('notifications' in toggle_types and self.logger.notifications_to_stdout): self.logger.notifications_to_stdout = False settings.logs.trace_notifications = False send_notice('Notification tracing to console is now deactivated') settings.save() def _CH_rtp(self, state='toggle'): if state == 'toggle': new_state = self.rtp_statistics is None elif state == 'on': new_state = True elif state == 'off': new_state = False else: raise TypeError() if (self.rtp_statistics and new_state) or (not self.rtp_statistics and not new_state): return if new_state: self.rtp_statistics = RTPStatisticsThread() self.rtp_statistics.start() send_notice('Output of RTP statistics and ICE negotiation results on console is now activated') else: self.rtp_statistics.stop() self.rtp_statistics = None send_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 send_notice('The microphone is now %s' % ('muted' if self.voice_audio_mixer.muted else 'unmuted')) def _CH_input(self, device=None): engine = Engine() settings = SIPSimpleSettings() input_devices = [None, u'system_default'] + sorted(engine.input_devices) if device is None: if self.voice_audio_mixer.input_device in input_devices: old_input_device = self.voice_audio_mixer.input_device else: old_input_device = None tail_length = settings.audio.echo_canceller.tail_length if settings.audio.echo_canceller.enabled else 0 new_input_device = input_devices[(input_devices.index(old_input_device)+1) % len(input_devices)] try: self.voice_audio_mixer.set_sound_devices(new_input_device, self.voice_audio_mixer.output_device, tail_length) except SIPCoreError, e: send_notice('Failed to set input device to %s: %s' % (new_input_device, str(e))) else: if new_input_device == u'system_default': send_notice('Input device changed to %s (system default device)' % self.voice_audio_mixer.real_input_device) else: send_notice('Input device changed to %s' % new_input_device) else: device = device.decode(sys.getfilesystemencoding()) if device == u'None': device = None elif device not in input_devices: send_notice('Unknown input device %s. Type /devices to see a list of available devices' % device) return tail_length = settings.audio.echo_canceller.tail_length if settings.audio.echo_canceller.enabled else 0 try: self.voice_audio_mixer.set_sound_devices(device, self.voice_audio_mixer.output_device, self.voice_audio_mixer.tail_length) except SIPCoreError, e: send_notice('Failed to set input device to %s: %s' % (device, str(e))) else: if device == u'system_default': send_notice('Input device changed to %s (system default device)' % self.voice_audio_mixer.real_input_device) else: send_notice('Input device changed to %s' % device) def _CH_output(self, device=None): engine = Engine() settings = SIPSimpleSettings() output_devices = [None, u'system_default'] + sorted(engine.output_devices) if device is None: if self.voice_audio_mixer.output_device in output_devices: old_output_device = self.voice_audio_mixer.output_device else: old_output_device = None tail_length = settings.audio.echo_canceller.tail_length if settings.audio.echo_canceller.enabled else 0 new_output_device = output_devices[(output_devices.index(old_output_device)+1) % len(output_devices)] try: self.voice_audio_mixer.set_sound_devices(self.voice_audio_mixer.input_device, new_output_device, tail_length) except SIPCoreError, e: send_notice('Failed to set output device to %s: %s' % (new_output_device, str(e))) else: if new_output_device == u'system_default': send_notice('Output device changed to %s (system default device)' % self.voice_audio_mixer.real_output_device) else: send_notice('Output device changed to %s' % new_output_device) else: device = device.decode(sys.getfilesystemencoding()) if device == u'None': device = None elif device not in output_devices: send_notice('Unknown output device %s. Type /devices to see a list of available devices' % device) return tail_length = settings.audio.echo_canceller.tail_length if settings.audio.echo_canceller.enabled else 0 try: self.voice_audio_mixer.set_sound_devices(self.voice_audio_mixer.input_device, device, tail_length) except SIPCoreError, e: send_notice('Failed to set output device to %s: %s' % (device, str(e))) else: if device == u'system_default': send_notice('Output device changed to %s (system default device)' % self.voice_audio_mixer.real_output_device) else: send_notice('Output device changed to %s' % device) def _CH_alert(self, device=None): engine = Engine() settings = SIPSimpleSettings() output_devices = [None, u'system_default'] + sorted(engine.output_devices) if device is None: if self.alert_audio_mixer.output_device in output_devices: old_output_device = self.alert_audio_mixer.output_device else: old_output_device = None tail_length = settings.audio.echo_canceller.tail_length if settings.audio.echo_canceller.enabled else 0 new_output_device = output_devices[(output_devices.index(old_output_device)+1) % len(output_devices)] try: self.alert_audio_mixer.set_sound_devices(self.alert_audio_mixer.input_device, new_output_device, tail_length) except SIPCoreError, e: old_output_device = new_output_device send_notice('Failed to set alert device to %s: %s' % (new_output_device, str(e))) else: if new_output_device == u'system_default': send_notice('Alert device changed to %s (system default device)' % self.alert_audio_mixer.real_output_device) else: send_notice('Alert device changed to %s' % new_output_device) else: device = device.decode(sys.getfilesystemencoding()) if device == u'None': device = None elif device not in output_devices: send_notice('Unknown output device %s. Type /devices to see a list of available devices' % device) return tail_length = settings.audio.echo_canceller.tail_length if settings.audio.echo_canceller.enabled else 0 try: self.alert_audio_mixer.set_sound_devices(self.alert_audio_mixer.input_device, device, tail_length) except SIPCoreError, e: send_notice('Failed to set alert device to %s: %s' % (device, str(e))) else: if device == u'system_default': send_notice('Alert device changed to %s (system default device)' % self.alert_audio_mixer.real_output_device) else: send_notice('Alert device changed to %s' % device) def _CH_devices(self): engine = Engine() send_notice('Available audio input devices: %s' % ', '.join(['None', 'system_default'] + sorted(engine.input_devices)), bold=False) send_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': send_notice('Using audio input device: %s (system default device)' % self.voice_audio_mixer.real_input_device, bold=False) else: send_notice('Using audio input device: %s' % self.voice_audio_mixer.input_device, bold=False) if self.voice_audio_mixer.output_device == 'system_default': send_notice('Using audio output device: %s (system default device)' % self.voice_audio_mixer.real_output_device, bold=False) else: send_notice('Using audio output device: %s' % self.voice_audio_mixer.output_device, bold=False) if self.alert_audio_mixer.output_device == 'system_default': send_notice('Using audio alert device: %s (system default device)' % self.alert_audio_mixer.real_output_device, bold=False) else: send_notice('Using audio alert device: %s' % self.alert_audio_mixer.output_device, bold=False) def _CH_help(self): self._print_help() def _CH_quit(self): self.stop() def _CH_eof(self): ui = UI() if self.active_session is not None: if self.active_session in self.sessions_with_proposals: ui.status = 'Cancelling proposal...' self.active_session.cancel_proposal() else: ui.status = 'Ending SIP session...' self.active_session.end() elif self.outgoing_session is not None: ui.status = 'Cancelling SIP session...' self.outgoing_session.end() else: self.stop() def _CH_hangup(self): if self.active_session is not None: send_notice('Ending SIP session...') self.active_session.end() elif self.outgoing_session is not None: send_notice('Cancelling SIP session...') self.outgoing_session.end() @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: send_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_otr(self): + if self.active_session is None: + 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: + send_notice("Chat encryption will stop") + chat_stream.encryption.stop() + else: + send_notice("Chat encryption will start") + chat_stream.encryption.start() + + def _CH_smp_s(self, *args): + secret = " ".join(args) if args else None + self.smp_secret = secret + send_notice("OTR SMP answer set to: %s" % secret) + + def _CH_smp_a(self, *args): + if self.active_session is None: + return + + chat_stream = next((stream for stream in self.active_session.streams if stream.type == 'chat'), None) + + if chat_stream is None: + return + + answer = " ".join(args) if args else None + + if answer: + chat_stream.encryption.smp_answer(answer) + + def _CH_smp_q(self, *args): + if self.active_session is None: + return + + chat_stream = next((stream for stream in self.active_session.streams if stream.type == 'chat'), None) + + if chat_stream is None: + return + + if self.smp_secret is None: + send_notice("Please set the SMP secret answer using /smp_s command") + return + + question = " ".join(args) if args else None + + if question: + send_notice("OTR SMP verification question will be asked: %s" % question) + chat_stream.encryption.verified = False + chat_stream.encryption.smp_verify(self.smp_secret, question=question) + self._update_prompt() + 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: send_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: send_notice('There is no active session') return if stream_name in (stream.type for stream in self.active_session.streams): send_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: send_notice('Cannot add a stream while another transaction is in progress') def _CH_remove(self, stream_name): if self.active_session is None: send_notice('There is no active session') return try: stream = (stream for stream in self.active_session.streams if stream.type==stream_name).next() except StopIteration: send_notice('The current active session does not have any %s streams' % stream_name) else: try: self.active_session.remove_stream(stream) except IllegalStateError: send_notice('Cannot remove a stream while another transaction is in progress') def _CH_add_participant(self, uri): if self.active_session is None: send_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: send_notice('Invalid SIP URI') else: self.active_session.conference.add_participant(uri) def _CH_remove_participant(self, uri): if self.active_session is None: send_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: send_notice('Invalid SIP URI') else: self.active_session.conference.remove_participant(uri) def _CH_transfer(self, uri): if self.active_session is None: send_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: send_notice('Invalid SIP URI') else: self.active_session.transfer(uri) def _CH_nickname(self, nickname): if self.active_session is not None: try: chat_stream = (stream for stream in self.active_session.streams if stream.type=='chat').next() except StopIteration: return try: chat_stream.set_local_nickname(nickname) except Exception, e: send_notice('Error setting nickname: %s' % e) # private methods # def _print_help(self): lines = [] lines.append('General commands:') lines.append(' /call {user[@domain]}: call the specified user using audio and chat') lines.append(' /audio {user[@domain]} [+chat]: call the specified user using audio and possibly chat') lines.append(' /chat {user[@domain]} [+audio]: call the specified user using chat and possibly audio') lines.append(' /send {user[@domain]} {file}: initiate a file transfer with the specified user') lines.append(' /next: select the next connected session') lines.append(' /prev: select the previous connected session') lines.append(' /sessions: show the list of connected sessions') if isinstance(self.account, BonjourAccount): lines.append(' /neighbours: show the list of bonjour neighbours') lines.append(' /trace [[+|-]sip] [[+|-]msrp] [[+|-]pjsip] [[+|-]notifications]: toggle/set tracing on the console (ctrl-x s | ctrl-x m | ctrl-x j | ctrl-x n)') lines.append(' /rtp [on|off]: toggle/set printing RTP statistics and ICE negotiation results on the console (ctrl-x p)') lines.append(' /mute [on|off]: mute the microphone (ctrl-x u)') lines.append(' /input [device]: change audio input device (ctrl-x i)') lines.append(' /output [device]: change audio output device (ctrl-x o)') lines.append(' /alert [device]: change audio alert device (ctrl-x a)') lines.append(' /quit: quit the program (ctrl-x q)') lines.append(' /help: display this help message (ctrl-x ?)') lines.append('In call commands:') lines.append(' /hangup: hang-up the active session (ctrl-x h)') lines.append(' /dtmf {0-9|*|#|A-D}...: send DTMF tones (ctrl-x 0-9|*|#|A-D)') lines.append(' /record [on|off]: toggle/set audio recording (ctrl-x r)') lines.append(' /hold [on|off]: hold/unhold (ctrl-x SPACE)') + lines.append(' /otr: toggle OTR encryption for the chat stream') + lines.append(' /smp_a answer: Answer OTR verification question') + lines.append(' /smp_s secret: Set the secret answer for the OTR verification question') + lines.append(' /smp_q question: Ask OTR verification question') 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') send_notice(lines, bold=False) def _update_prompt(self): ui = UI() session = self.active_session if session is None: - ui.prompt = Prompt(self.account.id, foreground='default') + ui.prompt = Prompt('%s@%s' % (self.account.id.username, self.account.id.domain), foreground='default') else: + info = 'not encrypted' 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) - if not streams: - streams = 'Session without media' - ui.prompt = Prompt('%s to %s' % (streams, identity), foreground='darkred') + chat_stream = next((stream for stream in session.streams if stream.type == 'chat'), None) + if chat_stream and chat_stream.encryption.active: + info = 'OTR encrypted verified' if chat_stream.encryption.verified else 'OTR encrypted NOT verified' + + ui.prompt = Prompt('%s to %s (%s)' % (streams, identity, info), foreground='darkred') def parse_handle_call_option(option, opt_str, value, parser, name): try: value = parser.rargs[0] except IndexError: value = 0 else: if value == '' or value[0] == '-': value = 0 else: try: value = int(value) except ValueError: value = 0 else: del parser.rargs[0] setattr(parser.values, name, value) + if __name__ == '__main__': description = '%prog is a command-line client for handling multiple audio, chat and file-transfer sessions' usage = '%prog [options] [user@domain]' parser = OptionParser(usage=usage, description=description) parser.print_usage = parser.print_help parser.add_option('-a', '--account', type='string', dest='account', help='The account name to use for any outgoing traffic. If not supplied, the default account will be used.', metavar='NAME') parser.add_option('-c', '--config-directory', type='string', dest='config_directory', help='The configuration directory to use. This overrides the default location.') parser.add_option('-s', '--trace-sip', action='store_true', dest='trace_sip', default=False, help='Dump the raw contents of incoming and outgoing SIP messages.') parser.add_option('-m', '--trace-msrp', action='store_true', dest='trace_msrp', default=False, help='Dump msrp logging information and the raw contents of incoming and outgoing MSRP messages.') parser.add_option('-j', '--trace-pjsip', action='store_true', dest='trace_pjsip', default=False, help='Print PJSIP logging output.') parser.add_option('-n', '--trace-notifications', action='store_true', dest='trace_notifications', default=False, help='Print all notifications (disabled by default).') parser.add_option('-S', '--disable-sound', action='store_true', dest='disable_sound', default=False, help='Disables initializing the sound card.') parser.set_default('auto_answer_interval', None) parser.add_option('--auto-answer', action='callback', callback=parse_handle_call_option, callback_args=('auto_answer_interval',), help='Interval after which to answer an incoming session (disabled by default). If the option is specified but the interval is not, it defaults to 0 (accept the session as soon as it starts ringing).', metavar='[INTERVAL]') parser.set_default('auto_hangup_interval', None) parser.add_option('--auto-hangup', action='callback', callback=parse_handle_call_option, callback_args=('auto_hangup_interval',), help='Interval after which to hang up an established session (disabled by default). If the option is specified but the interval is not, it defaults to 0 (hangup the session as soon as it connects).', metavar='[INTERVAL]') options, args = parser.parse_args() target = args[0] if args else None application = SIPSessionApplication() application.start(target, options) signal.signal(signal.SIGINT, signal.SIG_DFL) application.stopped_event.wait() sleep(0.1)