Example #1
0
class ChatSessionHandler(object):
    implements(IObserver)

    sip_identity = WriteOnceAttribute()
    xmpp_identity = WriteOnceAttribute()

    def __init__(self):
        self.started = False
        self.ended = False
        self.sip_session = None
        self.msrp_stream = None
        self._sip_session_timer = None

        self.use_receipts = False
        self.xmpp_session = None
        self._xmpp_message_queue = deque()

        self._pending_msrp_chunks = {}
        self._pending_xmpp_stanzas = {}

    def _get_started(self):
        return self.__dict__['started']

    def _set_started(self, value):
        old_value = self.__dict__.get('started', False)
        self.__dict__['started'] = value
        if not old_value and value:
            NotificationCenter().post_notification('ChatSessionDidStart',
                                                   sender=self)
            self._send_queued_messages()

    started = property(_get_started, _set_started)
    del _get_started, _set_started

    def _get_xmpp_session(self):
        return self.__dict__['xmpp_session']

    def _set_xmpp_session(self, session):
        self.__dict__['xmpp_session'] = session
        if session is not None:
            # Reet SIP session timer in case it's active
            if self._sip_session_timer is not None and self._sip_session_timer.active(
            ):
                self._sip_session_timer.reset(SESSION_TIMEOUT)
            NotificationCenter().add_observer(self, sender=session)
            session.start()
            # Reet SIP session timer in case it's active
            if self._sip_session_timer is not None and self._sip_session_timer.active(
            ):
                self._sip_session_timer.reset(SESSION_TIMEOUT)

    xmpp_session = property(_get_xmpp_session, _set_xmpp_session)
    del _get_xmpp_session, _set_xmpp_session

    @classmethod
    def new_from_sip_session(cls, sip_identity, session):
        instance = cls()
        instance.sip_identity = sip_identity
        instance._start_incoming_sip_session(session)
        return instance

    @classmethod
    def new_from_xmpp_stanza(cls, xmpp_identity, recipient):
        instance = cls()
        instance.xmpp_identity = xmpp_identity
        instance._start_outgoing_sip_session(recipient)
        return instance

    @run_in_green_thread
    def _start_incoming_sip_session(self, session):
        self.sip_session = session
        self.msrp_stream = next(stream for stream in session.proposed_streams
                                if stream.type == 'chat')
        notification_center = NotificationCenter()
        notification_center.add_observer(self, sender=self.sip_session)
        notification_center.add_observer(self, sender=self.msrp_stream)
        self.sip_session.accept([self.msrp_stream])

    @run_in_green_thread
    def _start_outgoing_sip_session(self, target_uri):
        notification_center = NotificationCenter()
        # self.xmpp_identity is our local identity
        from_uri = self.xmpp_identity.uri.as_sip_uri()
        del from_uri.parameters['gr']  # no GRUU in From header
        contact_uri = self.xmpp_identity.uri.as_sip_uri()
        contact_uri.parameters['gr'] = encode_resource(
            contact_uri.parameters['gr'].decode('utf-8'))
        to_uri = target_uri.as_sip_uri()
        lookup = DNSLookup()
        settings = SIPSimpleSettings()
        account = DefaultAccount()
        if account.sip.outbound_proxy is not None:
            uri = SIPURI(
                host=account.sip.outbound_proxy.host,
                port=account.sip.outbound_proxy.port,
                parameters={'transport': account.sip.outbound_proxy.transport})
        else:
            uri = to_uri
        try:
            routes = lookup.lookup_sip_proxy(
                uri, settings.sip.transport_list).wait()
        except DNSLookupError:
            log.warning('DNS lookup error while looking for %s proxy' % uri)
            notification_center.post_notification(
                'ChatSessionDidFail',
                sender=self,
                data=NotificationData(reason='DNS lookup error'))
            return
        self.msrp_stream = MediaStreamRegistry.get('chat')()
        route = routes.pop(0)
        from_header = FromHeader(from_uri)
        to_header = ToHeader(to_uri)
        contact_header = ContactHeader(contact_uri)
        self.sip_session = Session(account)
        notification_center.add_observer(self, sender=self.sip_session)
        notification_center.add_observer(self, sender=self.msrp_stream)
        self.sip_session.connect(from_header,
                                 to_header,
                                 contact_header=contact_header,
                                 route=route,
                                 streams=[self.msrp_stream])

    def end(self):
        if self.ended:
            return
        if self._sip_session_timer is not None and self._sip_session_timer.active(
        ):
            self._sip_session_timer.cancel()
        self._sip_session_timer = None
        notification_center = NotificationCenter()
        if self.sip_session is not None:
            notification_center.remove_observer(self, sender=self.sip_session)
            notification_center.remove_observer(self, sender=self.msrp_stream)
            self.sip_session.end()
            self.sip_session = None
            self.msrp_stream = None
        if self.xmpp_session is not None:
            notification_center.remove_observer(self, sender=self.xmpp_session)
            self.xmpp_session.end()
            self.xmpp_session = None
        self.ended = True
        if self.started:
            notification_center.post_notification('ChatSessionDidEnd',
                                                  sender=self)
        else:
            notification_center.post_notification(
                'ChatSessionDidFail',
                sender=self,
                data=NotificationData(reason='Ended before actually started'))

    def enqueue_xmpp_message(self, message):
        self._xmpp_message_queue.append(message)
        if self.started:
            self._send_queued_messages()

    def _send_queued_messages(self):
        sender = None
        while self._xmpp_message_queue:
            message = self._xmpp_message_queue.popleft()
            if message.body is None:
                continue
            sender_uri = message.sender.uri.as_sip_uri()
            sender_uri.parameters['gr'] = encode_resource(
                sender_uri.parameters['gr'].decode('utf-8'))
            sender = ChatIdentity(sender_uri)
            self.msrp_stream.send_message(message.body,
                                          'text/plain',
                                          sender=sender,
                                          message_id=str(message.id),
                                          notify_progress=message.use_receipt)
        if sender:
            self.msrp_stream.send_composing_indication('idle',
                                                       30,
                                                       sender=sender)

    def _inactivity_timeout(self):
        log.info("Ending SIP session %s due to inactivity" %
                 self.sip_session.call_id)
        self.sip_session.end()

    def handle_notification(self, notification):
        handler = getattr(self, '_NH_%s' % notification.name, Null)
        handler(notification)

    def _NH_SIPSessionDidStart(self, notification):
        log.info("SIP session %s started" % self.sip_session.call_id)
        self._sip_session_timer = reactor.callLater(SESSION_TIMEOUT,
                                                    self._inactivity_timeout)

        if self.sip_session.direction == 'outgoing':
            # Time to set sip_identity and create the XMPPChatSession
            contact_uri = self.sip_session._invitation.remote_contact_header.uri
            if contact_uri.parameters.get('gr') is not None:
                sip_leg_uri = FrozenURI(contact_uri.user, contact_uri.host,
                                        contact_uri.parameters.get('gr'))
            else:
                tmp = self.sip_session.remote_identity.uri
                sip_leg_uri = FrozenURI(tmp.user, tmp.host,
                                        generate_sylk_resource())
            self.sip_identity = Identity(
                sip_leg_uri, self.sip_session.remote_identity.display_name)
            session = XMPPChatSession(local_identity=self.sip_identity,
                                      remote_identity=self.xmpp_identity)
            self.xmpp_session = session
            # Session is now established on both ends
            self.started = True
            # Try to wakeup XMPP clients
            self.xmpp_session.send_composing_indication('active')
            self.xmpp_session.send_message(' ', 'text/plain')
        else:
            if self.xmpp_session is not None:
                # Session is now established on both ends
                self.started = True
                # Try to wakeup XMPP clients
                self.xmpp_session.send_composing_indication('active')
                self.xmpp_session.send_message(' ', 'text/plain')
            else:
                # Try to wakeup XMPP clients
                sender = self.sip_identity
                tmp = self.sip_session.local_identity.uri
                recipient_uri = FrozenURI(tmp.user, tmp.host)
                recipient = Identity(recipient_uri)
                xmpp_manager = XMPPManager()
                xmpp_manager.send_stanza(
                    ChatMessage(sender, recipient, ' ', 'text/plain'))
                # Send queued messages
                self._send_queued_messages()

    def _NH_SIPSessionDidEnd(self, notification):
        log.info("SIP session %s ended" % self.sip_session.call_id)
        notification.center.remove_observer(self, sender=self.sip_session)
        notification.center.remove_observer(self, sender=self.msrp_stream)
        self.sip_session = None
        self.msrp_stream = None
        self.end()

    def _NH_SIPSessionDidFail(self, notification):
        log.info("SIP session %s failed" % self.sip_session.call_id)
        notification.center.remove_observer(self, sender=self.sip_session)
        notification.center.remove_observer(self, sender=self.msrp_stream)
        self.sip_session = None
        self.msrp_stream = None
        self.end()

    def _NH_SIPSessionNewProposal(self, notification):
        if notification.data.originator == 'remote':
            self.sip_session.reject_proposal()

    def _NH_SIPSessionTransferNewIncoming(self, notification):
        self.sip_session.reject_transfer(403)

    def _NH_ChatStreamGotMessage(self, notification):
        # Notification is sent by the MSRP stream
        message = notification.data.message
        content_type = message.content_type.lower()
        if content_type not in ('text/plain', 'text/html'):
            return
        if content_type == 'text/plain':
            html_body = None
            body = message.content
        else:
            html_body = message.content
            body = None
        if self._sip_session_timer is not None and self._sip_session_timer.active(
        ):
            self._sip_session_timer.reset(SESSION_TIMEOUT)
        chunk = notification.data.chunk
        if self.started:
            self.xmpp_session.send_message(body,
                                           html_body,
                                           message_id=chunk.message_id)
            if self.use_receipts:
                self._pending_msrp_chunks[chunk.message_id] = chunk
            else:
                self.msrp_stream.msrp_session.send_report(chunk, 200, 'OK')
        else:
            sender = self.sip_identity
            recipient_uri = FrozenURI.parse(message.recipients[0].uri)
            recipient = Identity(recipient_uri,
                                 message.recipients[0].display_name)
            xmpp_manager = XMPPManager()
            xmpp_manager.send_stanza(
                ChatMessage(sender, recipient, body, html_body))
            self.msrp_stream.msrp_session.send_report(chunk, 200, 'OK')

    def _NH_ChatStreamGotComposingIndication(self, notification):
        # Notification is sent by the MSRP stream
        if self._sip_session_timer is not None and self._sip_session_timer.active(
        ):
            self._sip_session_timer.reset(SESSION_TIMEOUT)
        if not self.started:
            return
        state = None
        if notification.data.state == 'active':
            state = 'composing'
        elif notification.data.state == 'idle':
            state = 'paused'
        if state is not None:
            self.xmpp_session.send_composing_indication(state)

    def _NH_ChatStreamDidDeliverMessage(self, notification):
        if self.started:
            message = self._pending_xmpp_stanzas.pop(
                notification.data.message_id, None)
            if message is not None:
                self.xmpp_session.send_receipt_acknowledgement(message.id)

    def _NH_ChatStreamDidNotDeliverMessage(self, notification):
        if self.started:
            message = self._pending_xmpp_stanzas.pop(
                notification.data.message_id, None)
            if message is not None:
                self.xmpp_session.send_error(message, 'TODO', [])  # TODO

    def _NH_XMPPChatSessionDidStart(self, notification):
        if self.sip_session is not None:
            # Session is now established on both ends
            self.started = True

    def _NH_XMPPChatSessionDidEnd(self, notification):
        notification.center.remove_observer(self, sender=self.xmpp_session)
        self.xmpp_session = None
        self.end()

    def _NH_XMPPChatSessionGotMessage(self, notification):
        if self.sip_session is None or self.sip_session.state != 'connected':
            self._xmpp_message_queue.append(notification.data.message)
            return
        if self._sip_session_timer is not None and self._sip_session_timer.active(
        ):
            self._sip_session_timer.reset(SESSION_TIMEOUT)
        message = notification.data.message
        sender_uri = message.sender.uri.as_sip_uri()
        del sender_uri.parameters['gr']  # no GRUU in CPIM From header
        sender = ChatIdentity(sender_uri)
        self.use_receipts = message.use_receipt
        if not message.use_receipt:
            notify_progress = False
        else:
            notify_progress = True
            self._pending_xmpp_stanzas[message.id] = message
        # Prefer plaintext
        self.msrp_stream.send_message(message.body,
                                      'text/plain',
                                      sender=sender,
                                      message_id=str(message.id),
                                      notify_progress=notify_progress)
        self.msrp_stream.send_composing_indication('idle', 30, sender=sender)

    def _NH_XMPPChatSessionGotComposingIndication(self, notification):
        if self.sip_session is None or self.sip_session.state != 'connected':
            return
        if self._sip_session_timer is not None and self._sip_session_timer.active(
        ):
            self._sip_session_timer.reset(SESSION_TIMEOUT)
        message = notification.data.message
        state = None
        if message.state == 'composing':
            state = 'active'
        elif message.state == 'paused':
            state = 'idle'
        if state is not None:
            sender_uri = message.sender.uri.as_sip_uri()
            del sender_uri.parameters['gr']  # no GRUU in CPIM From header
            sender = ChatIdentity(sender_uri)
            self.msrp_stream.send_composing_indication(state,
                                                       30,
                                                       sender=sender)
            if message.use_receipt:
                self.xmpp_session.send_receipt_acknowledgement(message.id)

    def _NH_XMPPChatSessionDidDeliverMessage(self, notification):
        chunk = self._pending_msrp_chunks.pop(notification.data.message_id,
                                              None)
        if chunk is not None:
            self.msrp_stream.msrp_session.send_report(chunk, 200, 'OK')

    def _NH_XMPPChatSessionDidNotDeliverMessage(self, notification):
        chunk = self._pending_msrp_chunks.pop(notification.data.message_id,
                                              None)
        if chunk is not None:
            self.msrp_stream.msrp_session.send_report(chunk,
                                                      notification.data.code,
                                                      notification.data.reason)
Example #2
0
class MediaSessionHandler(object):
    implements(IObserver)

    def __init__(self):
        self.started = False
        self.ended = False

        self._sip_identity = None
        self._xmpp_identity = None

        self._audio_bidge = AudioConference()
        self.sip_session = None
        self.jingle_session = None

    @classmethod
    def new_from_sip_session(cls, session):
        proposed_stream_types = set(
            [stream.type for stream in session.proposed_streams])
        streams = []
        for stream_type in proposed_stream_types:
            try:
                klass = JingleMediaStreamRegistry.get(stream_type)
            except Exception:
                continue
            streams.append(klass())
        if not streams:
            session.reject(488)
            return None
        session.send_ring_indication()
        instance = cls()
        NotificationCenter().add_observer(instance, sender=session)
        # Get URI representing the SIP side
        contact_uri = session._invitation.remote_contact_header.uri
        if contact_uri.parameters.get('gr') is not None:
            sip_leg_uri = FrozenURI(contact_uri.user, contact_uri.host,
                                    contact_uri.parameters.get('gr'))
        else:
            tmp = session.remote_identity.uri
            sip_leg_uri = FrozenURI(tmp.user, tmp.host,
                                    generate_sylk_resource())
        instance._sip_identity = Identity(sip_leg_uri)
        # Get URI representing the XMPP side
        request_uri = session.request_uri
        remote_resource = request_uri.parameters.get('gr', None)
        if remote_resource is not None:
            try:
                remote_resource = decode_resource(remote_resource)
            except (TypeError, UnicodeError):
                remote_resource = None
        xmpp_leg_uri = FrozenURI(request_uri.user, request_uri.host,
                                 remote_resource)
        instance._xmpp_identity = Identity(xmpp_leg_uri)
        instance.sip_session = session
        instance._start_outgoing_jingle_session(streams)
        return instance

    @classmethod
    def new_from_jingle_session(cls, session):
        proposed_stream_types = set(
            [stream.type for stream in session.proposed_streams])
        streams = []
        for stream_type in proposed_stream_types:
            try:
                klass = SIPMediaStreamRegistry.get(stream_type)
            except Exception:
                continue
            streams.append(klass())
        if not streams:
            session.reject('unsupported-applications')
            return None
        session.send_ring_indication()
        instance = cls()
        NotificationCenter().add_observer(instance, sender=session)
        instance._xmpp_identity = session.remote_identity
        instance._sip_identity = session.local_identity
        instance.jingle_session = session
        instance._start_outgoing_sip_session(streams)
        return instance

    @property
    def sip_identity(self):
        return self._sip_identity

    @property
    def xmpp_identity(self):
        return self._xmpp_identity

    def _set_started(self, value):
        old_value = self.__dict__.get('started', False)
        self.__dict__['started'] = value
        if not old_value and value:
            NotificationCenter().post_notification(
                'MediaSessionHandlerDidStart', sender=self)

    def _get_started(self):
        return self.__dict__['started']

    started = property(_get_started, _set_started)
    del _get_started, _set_started

    @run_in_green_thread
    def _start_outgoing_sip_session(self, streams):
        notification_center = NotificationCenter()
        # self.xmpp_identity is our local identity on the SIP side
        from_uri = self.xmpp_identity.uri.as_sip_uri()
        from_uri.parameters.pop('gr', None)  # no GRUU in From header
        to_uri = self.sip_identity.uri.as_sip_uri()
        to_uri.parameters.pop('gr', None)  # no GRUU in To header
        # TODO: need to fix GRUU in the proxy
        #contact_uri = self.xmpp_identity.uri.as_sip_uri()
        #contact_uri.parameters['gr'] = encode_resource(contact_uri.parameters['gr'].decode('utf-8'))
        lookup = DNSLookup()
        settings = SIPSimpleSettings()
        account = DefaultAccount()
        if account.sip.outbound_proxy is not None:
            uri = SIPURI(
                host=account.sip.outbound_proxy.host,
                port=account.sip.outbound_proxy.port,
                parameters={'transport': account.sip.outbound_proxy.transport})
        else:
            uri = to_uri
        try:
            routes = lookup.lookup_sip_proxy(
                uri, settings.sip.transport_list).wait()
        except DNSLookupError:
            log.warning('DNS lookup error while looking for %s proxy' % uri)
            notification_center.post_notification(
                'MedialSessionHandlerDidFail',
                sender=self,
                data=NotificationData(reason='DNS lookup error'))
            return
        route = routes.pop(0)
        from_header = FromHeader(from_uri)
        to_header = ToHeader(to_uri)
        self.sip_session = Session(account)
        notification_center.add_observer(self, sender=self.sip_session)
        self.sip_session.connect(from_header,
                                 to_header,
                                 route=route,
                                 streams=streams)

    @run_in_green_thread
    def _start_outgoing_jingle_session(self, streams):
        if self.xmpp_identity.uri.resource is not None:
            self.sip_session.reject()
            return
        xmpp_manager = XMPPManager()
        local_jid = self.sip_identity.uri.as_xmpp_jid()
        remote_jid = self.xmpp_identity.uri.as_xmpp_jid()

        # If this was an invitation to a conference, use the information in the Referred-By header
        if self.sip_identity.uri.host in xmpp_manager.muc_domains and self.sip_session.transfer_info and self.sip_session.transfer_info.referred_by:
            try:
                referred_by_uri = SIPURI.parse(
                    self.sip_session.transfer_info.referred_by)
            except SIPCoreError:
                self.sip_session.reject(488)
                return
            else:
                inviter_uri = FrozenURI(referred_by_uri.user,
                                        referred_by_uri.host)
                local_jid = inviter_uri.as_xmpp_jid()

        # Use disco to gather potential JIDs to call
        d = xmpp_manager.disco_client_protocol.requestItems(remote_jid,
                                                            sender=local_jid)
        try:
            items = block_on(d)
        except Exception:
            items = []
        if not items:
            self.sip_session.reject(480)
            return

        # Check which items support Jingle
        valid = []
        for item in items:
            d = xmpp_manager.disco_client_protocol.requestInfo(
                item.entity,
                nodeIdentifier=item.nodeIdentifier,
                sender=local_jid)
            try:
                info = block_on(d)
            except Exception:
                continue
            if jingle.NS_JINGLE in info.features and jingle.NS_JINGLE_APPS_RTP in info.features:
                valid.append(item.entity)
        if not valid:
            self.sip_session.reject(480)
            return

        # TODO: start multiple sessions?
        self._xmpp_identity = Identity(FrozenURI.parse(valid[0]))

        notification_center = NotificationCenter()
        if self.sip_identity.uri.host in xmpp_manager.muc_domains:
            self.jingle_session = JingleSession(
                xmpp_manager.jingle_coin_protocol)
        else:
            self.jingle_session = JingleSession(xmpp_manager.jingle_protocol)
        notification_center.add_observer(self, sender=self.jingle_session)
        self.jingle_session.connect(self.sip_identity,
                                    self.xmpp_identity,
                                    streams,
                                    is_focus=self.sip_session.remote_focus)

    def end(self):
        if self.ended:
            return
        notification_center = NotificationCenter()
        if self.sip_session is not None:
            notification_center.remove_observer(self, sender=self.sip_session)
            if self.sip_session.direction == 'incoming' and not self.started:
                self.sip_session.reject()
            else:
                self.sip_session.end()
            self.sip_session = None
        if self.jingle_session is not None:
            notification_center.remove_observer(self,
                                                sender=self.jingle_session)
            if self.jingle_session.direction == 'incoming' and not self.started:
                self.jingle_session.reject()
            else:
                self.jingle_session.end()
            self.jingle_session = None
        self.ended = True
        if self.started:
            notification_center.post_notification('MediaSessionHandlerDidEnd',
                                                  sender=self)
        else:
            notification_center.post_notification('MediaSessionHandlerDidFail',
                                                  sender=self)

    @run_in_twisted_thread
    def handle_notification(self, notification):
        handler = getattr(self, '_NH_%s' % notification.name, Null)
        handler(notification)

    def _NH_SIPSessionDidStart(self, notification):
        log.info("SIP session %s started" % self.sip_session.call_id)
        if self.sip_session.direction == 'outgoing':
            # Time to accept the Jingle session and bridge them together
            try:
                audio_stream = next(stream
                                    for stream in self.sip_session.streams
                                    if stream.type == 'audio')
            except StopIteration:
                pass
            else:
                self._audio_bidge.add(audio_stream)
            self.jingle_session.accept(self.jingle_session.proposed_streams,
                                       is_focus=self.sip_session.remote_focus)
        else:
            # Both sessions have been accepted now
            self.started = True
            try:
                audio_stream = next(stream
                                    for stream in self.sip_session.streams
                                    if stream.type == 'audio')
            except StopIteration:
                pass
            else:
                self._audio_bidge.add(audio_stream)

    def _NH_SIPSessionDidEnd(self, notification):
        log.info("SIP session %s ended" % self.sip_session.call_id)
        notification.center.remove_observer(self, sender=self.sip_session)
        self.sip_session = None
        self.end()

    def _NH_SIPSessionDidFail(self, notification):
        log.info("SIP session %s failed (%s)" %
                 (self.sip_session.call_id, notification.data.reason))
        notification.center.remove_observer(self, sender=self.sip_session)
        self.sip_session = None
        self.end()

    def _NH_SIPSessionNewProposal(self, notification):
        if notification.data.originator == 'remote':
            self.sip_session.reject_proposal()

    def _NH_SIPSessionTransferNewIncoming(self, notification):
        self.sip_session.reject_transfer(403)

    def _NH_SIPSessionDidChangeHoldState(self, notification):
        if notification.data.originator == 'remote':
            if notification.data.on_hold:
                self.jingle_session.hold()
            else:
                self.jingle_session.unhold()

    def _NH_SIPSessionGotConferenceInfo(self, notification):
        self.jingle_session._send_conference_info(
            notification.data.conference_info.toxml())

    def _NH_JingleSessionDidStart(self, notification):
        log.info("Jingle session %s started" % notification.sender.id)
        if self.jingle_session.direction == 'incoming':
            # Both sessions have been accepted now
            self.started = True
            try:
                audio_stream = next(stream
                                    for stream in self.jingle_session.streams
                                    if stream.type == 'audio')
            except StopIteration:
                pass
            else:
                self._audio_bidge.add(audio_stream)
        else:
            # Time to accept the Jingle session and bridge them together
            try:
                audio_stream = next(stream
                                    for stream in self.jingle_session.streams
                                    if stream.type == 'audio')
            except StopIteration:
                pass
            else:
                self._audio_bidge.add(audio_stream)
            self.sip_session.accept(self.sip_session.proposed_streams)

    def _NH_JingleSessionDidEnd(self, notification):
        log.info("Jingle session %s ended" % notification.sender.id)
        notification.center.remove_observer(self, sender=self.jingle_session)
        self.jingle_session = None
        self.end()

    def _NH_JingleSessionDidFail(self, notification):
        log.info("Jingle session %s failed (%s)" %
                 (notification.sender.id, notification.data.reason))
        notification.center.remove_observer(self, sender=self.jingle_session)
        self.jingle_session = None
        self.end()

    def _NH_JingleSessionDidChangeHoldState(self, notification):
        if notification.data.originator == 'remote':
            if notification.data.on_hold:
                self.sip_session.hold()
            else:
                self.sip_session.unhold()
Example #3
0
class MediaSessionHandler(object):
    implements(IObserver)

    def __init__(self):
        self.started = False
        self.ended = False

        self._sip_identity = None
        self._xmpp_identity = None

        self._audio_bidge = AudioConference()
        self.sip_session = None
        self.jingle_session = None

    @classmethod
    def new_from_sip_session(cls, session):
        proposed_stream_types = set([stream.type for stream in session.proposed_streams])
        streams = []
        for stream_type in proposed_stream_types:
            try:
                klass = JingleMediaStreamRegistry.get(stream_type)
            except Exception:
                continue
            streams.append(klass())
        if not streams:
            session.reject(488)
            return None
        session.send_ring_indication()
        instance = cls()
        NotificationCenter().add_observer(instance, sender=session)
        # Get URI representing the SIP side
        contact_uri = session._invitation.remote_contact_header.uri
        if contact_uri.parameters.get('gr') is not None:
            sip_leg_uri = FrozenURI(contact_uri.user, contact_uri.host, contact_uri.parameters.get('gr'))
        else:
            tmp = session.remote_identity.uri
            sip_leg_uri = FrozenURI(tmp.user, tmp.host, generate_sylk_resource())
        instance._sip_identity = Identity(sip_leg_uri)
        # Get URI representing the XMPP side
        request_uri = session.request_uri
        remote_resource = request_uri.parameters.get('gr', None)
        if remote_resource is not None:
            try:
                remote_resource = decode_resource(remote_resource)
            except (TypeError, UnicodeError):
                remote_resource = None
        xmpp_leg_uri = FrozenURI(request_uri.user, request_uri.host, remote_resource)
        instance._xmpp_identity = Identity(xmpp_leg_uri)
        instance.sip_session = session
        instance._start_outgoing_jingle_session(streams)
        return instance

    @classmethod
    def new_from_jingle_session(cls, session):
        proposed_stream_types = set([stream.type for stream in session.proposed_streams])
        streams = []
        for stream_type in proposed_stream_types:
            try:
                klass = SIPMediaStreamRegistry.get(stream_type)
            except Exception:
                continue
            streams.append(klass())
        if not streams:
            session.reject('unsupported-applications')
            return None
        session.send_ring_indication()
        instance = cls()
        NotificationCenter().add_observer(instance, sender=session)
        instance._xmpp_identity = session.remote_identity
        instance._sip_identity = session.local_identity
        instance.jingle_session = session
        instance._start_outgoing_sip_session(streams)
        return instance

    @property
    def sip_identity(self):
        return self._sip_identity

    @property
    def xmpp_identity(self):
        return self._xmpp_identity

    def _set_started(self, value):
        old_value = self.__dict__.get('started', False)
        self.__dict__['started'] = value
        if not old_value and value:
            NotificationCenter().post_notification('MediaSessionHandlerDidStart', sender=self)
    def _get_started(self):
        return self.__dict__['started']
    started = property(_get_started, _set_started)
    del _get_started, _set_started

    @run_in_green_thread
    def _start_outgoing_sip_session(self, streams):
        notification_center = NotificationCenter()
        # self.xmpp_identity is our local identity on the SIP side
        from_uri = self.xmpp_identity.uri.as_sip_uri()
        from_uri.parameters.pop('gr', None)    # no GRUU in From header
        to_uri = self.sip_identity.uri.as_sip_uri()
        to_uri.parameters.pop('gr', None)      # no GRUU in To header
        # TODO: need to fix GRUU in the proxy
        #contact_uri = self.xmpp_identity.uri.as_sip_uri()
        #contact_uri.parameters['gr'] = encode_resource(contact_uri.parameters['gr'].decode('utf-8'))
        lookup = DNSLookup()
        settings = SIPSimpleSettings()
        account = DefaultAccount()
        if account.sip.outbound_proxy is not None:
            uri = SIPURI(host=account.sip.outbound_proxy.host,
                         port=account.sip.outbound_proxy.port,
                         parameters={'transport': account.sip.outbound_proxy.transport})
        else:
            uri = to_uri
        try:
            routes = lookup.lookup_sip_proxy(uri, settings.sip.transport_list).wait()
        except DNSLookupError:
            log.warning('DNS lookup error while looking for %s proxy' % uri)
            notification_center.post_notification('MedialSessionHandlerDidFail', sender=self, data=NotificationData(reason='DNS lookup error'))
            return
        route = routes.pop(0)
        from_header = FromHeader(from_uri)
        to_header = ToHeader(to_uri)
        self.sip_session = Session(account)
        notification_center.add_observer(self, sender=self.sip_session)
        self.sip_session.connect(from_header, to_header, route=route, streams=streams)

    @run_in_green_thread
    def _start_outgoing_jingle_session(self, streams):
        if self.xmpp_identity.uri.resource is not None:
            self.sip_session.reject()
            return
        xmpp_manager = XMPPManager()
        local_jid = self.sip_identity.uri.as_xmpp_jid()
        remote_jid = self.xmpp_identity.uri.as_xmpp_jid()

        # If this was an invitation to a conference, use the information in the Referred-By header
        if self.sip_identity.uri.host in xmpp_manager.muc_domains and self.sip_session.transfer_info and self.sip_session.transfer_info.referred_by:
            try:
                referred_by_uri = SIPURI.parse(self.sip_session.transfer_info.referred_by)
            except SIPCoreError:
                self.sip_session.reject(488)
                return
            else:
                inviter_uri = FrozenURI(referred_by_uri.user, referred_by_uri.host)
                local_jid = inviter_uri.as_xmpp_jid()

        # Use disco to gather potential JIDs to call
        d = xmpp_manager.disco_client_protocol.requestItems(remote_jid, sender=local_jid)
        try:
            items = block_on(d)
        except Exception:
            items = []
        if not items:
            self.sip_session.reject(480)
            return

        # Check which items support Jingle
        valid = []
        for item in items:
            d = xmpp_manager.disco_client_protocol.requestInfo(item.entity, nodeIdentifier=item.nodeIdentifier, sender=local_jid)
            try:
                info = block_on(d)
            except Exception:
                continue
            if jingle.NS_JINGLE in info.features and jingle.NS_JINGLE_APPS_RTP in info.features:
                valid.append(item.entity)
        if not valid:
            self.sip_session.reject(480)
            return

        # TODO: start multiple sessions?
        self._xmpp_identity = Identity(FrozenURI.parse(valid[0]))

        notification_center = NotificationCenter()
        if self.sip_identity.uri.host in xmpp_manager.muc_domains:
            self.jingle_session = JingleSession(xmpp_manager.jingle_coin_protocol)
        else:
            self.jingle_session = JingleSession(xmpp_manager.jingle_protocol)
        notification_center.add_observer(self, sender=self.jingle_session)
        self.jingle_session.connect(self.sip_identity, self.xmpp_identity, streams, is_focus=self.sip_session.remote_focus)

    def end(self):
        if self.ended:
            return
        notification_center = NotificationCenter()
        if self.sip_session is not None:
            notification_center.remove_observer(self, sender=self.sip_session)
            if self.sip_session.direction == 'incoming' and not self.started:
                self.sip_session.reject()
            else:
                self.sip_session.end()
            self.sip_session = None
        if self.jingle_session is not None:
            notification_center.remove_observer(self, sender=self.jingle_session)
            if self.jingle_session.direction == 'incoming' and not self.started:
                self.jingle_session.reject()
            else:
                self.jingle_session.end()
            self.jingle_session = None
        self.ended = True
        if self.started:
            notification_center.post_notification('MediaSessionHandlerDidEnd', sender=self)
        else:
            notification_center.post_notification('MediaSessionHandlerDidFail', sender=self)

    @run_in_twisted_thread
    def handle_notification(self, notification):
        handler = getattr(self, '_NH_%s' % notification.name, Null)
        handler(notification)

    def _NH_SIPSessionDidStart(self, notification):
        log.msg("SIP session %s started" % self.sip_session.call_id)
        if self.sip_session.direction == 'outgoing':
            # Time to accept the Jingle session and bridge them together
            try:
                audio_stream = next(stream for stream in self.sip_session.streams if stream.type=='audio')
            except StopIteration:
                pass
            else:
                self._audio_bidge.add(audio_stream)
            self.jingle_session.accept(self.jingle_session.proposed_streams, is_focus=self.sip_session.remote_focus)
        else:
            # Both sessions have been accepted now
            self.started = True
            try:
                audio_stream = next(stream for stream in self.sip_session.streams if stream.type=='audio')
            except StopIteration:
                pass
            else:
                self._audio_bidge.add(audio_stream)

    def _NH_SIPSessionDidEnd(self, notification):
        log.msg("SIP session %s ended" % self.sip_session.call_id)
        notification.center.remove_observer(self, sender=self.sip_session)
        self.sip_session = None
        self.end()

    def _NH_SIPSessionDidFail(self, notification):
        log.msg("SIP session %s failed (%s)" % (self.sip_session.call_id, notification.data.reason))
        notification.center.remove_observer(self, sender=self.sip_session)
        self.sip_session = None
        self.end()

    def _NH_SIPSessionNewProposal(self, notification):
        if notification.data.originator == 'remote':
            self.sip_session.reject_proposal()

    def _NH_SIPSessionTransferNewIncoming(self, notification):
        self.sip_session.reject_transfer(403)

    def _NH_SIPSessionDidChangeHoldState(self, notification):
        if notification.data.originator == 'remote':
            if notification.data.on_hold:
                self.jingle_session.hold()
            else:
                self.jingle_session.unhold()

    def _NH_SIPSessionGotConferenceInfo(self, notification):
        self.jingle_session._send_conference_info(notification.data.conference_info.toxml())

    def _NH_JingleSessionDidStart(self, notification):
        log.msg("Jingle session %s started" % notification.sender.id)
        if self.jingle_session.direction == 'incoming':
            # Both sessions have been accepted now
            self.started = True
            try:
                audio_stream = next(stream for stream in self.jingle_session.streams if stream.type=='audio')
            except StopIteration:
                pass
            else:
                self._audio_bidge.add(audio_stream)
        else:
            # Time to accept the Jingle session and bridge them together
            try:
                audio_stream = next(stream for stream in self.jingle_session.streams if stream.type=='audio')
            except StopIteration:
                pass
            else:
                self._audio_bidge.add(audio_stream)
            self.sip_session.accept(self.sip_session.proposed_streams)

    def _NH_JingleSessionDidEnd(self, notification):
        log.msg("Jingle session %s ended" % notification.sender.id)
        notification.center.remove_observer(self, sender=self.jingle_session)
        self.jingle_session = None
        self.end()

    def _NH_JingleSessionDidFail(self, notification):
        log.msg("Jingle session %s failed (%s)" % (notification.sender.id, notification.data.reason))
        notification.center.remove_observer(self, sender=self.jingle_session)
        self.jingle_session = None
        self.end()

    def _NH_JingleSessionDidChangeHoldState(self, notification):
        if notification.data.originator == 'remote':
            if notification.data.on_hold:
                self.sip_session.hold()
            else:
                self.sip_session.unhold()
Example #4
0
class OutgoingFileTransferHandler(object):
    implements(IObserver)

    def __init__(self, sender_uri, destination_uri, file_selector):
        self.sender_uri = sender_uri
        self.destination_uri = destination_uri
        self.file_selector = file_selector
        self.session = None
        self.stream = None
        self.timer = None
        self.success = False

    @run_in_green_thread
    def start(self):
        notification_center = NotificationCenter()
        self.greenlet = api.getcurrent()
        settings = SIPSimpleSettings()
        account = AccountManager().sylkserver_account
        if account.sip.outbound_proxy is not None:
            uri = SIPURI(
                host=account.sip.outbound_proxy.host,
                port=account.sip.outbound_proxy.port,
                parameters={'transport': account.sip.outbound_proxy.transport})
        else:
            uri = self.destination_uri
        lookup = DNSLookup()
        try:
            routes = lookup.lookup_sip_proxy(
                uri, settings.sip.transport_list).wait()
        except DNSLookupError:
            notification_center.post_notification(
                'OutgoingFileTransferHandlerDidFail', sender=self)
            return

        self.session = Session(account)
        self.stream = FileTransferStream(self.file_selector, 'sendonly')
        notification_center.add_observer(self, sender=self.session)
        notification_center.add_observer(self, sender=self.stream)
        from_header = FromHeader(self.sender_uri, u'SIPStache File Transfer')
        to_header = ToHeader(self.destination_uri)
        transport = routes[0].transport
        parameters = {} if transport == 'udp' else {'transport': transport}
        contact_header = ContactHeader(
            SIPURI(user=self.sender_uri.user,
                   host=SIPConfig.local_ip.normalized,
                   port=getattr(Engine(), '%s_port' % transport),
                   parameters=parameters))
        extra_headers = []
        if ThorNodeConfig.enabled:
            extra_headers.append(Header('Thor-Scope', 'sipstache-file'))
        extra_headers.append(
            Header('X-Originator-From', str(self.destination_uri)))
        self.session.connect(from_header,
                             to_header,
                             contact_header=contact_header,
                             routes=routes,
                             streams=[self.stream],
                             is_focus=False,
                             extra_headers=extra_headers)
        notification_center.post_notification(
            'OutgoingFileTransferHandlerDidStart', sender=self)

    def stop(self):
        if self.session is not None:
            self.session.end()

    @run_in_twisted_thread
    def handle_notification(self, notification):
        handler = getattr(self, '_NH_%s' % notification.name, Null)
        handler(notification)

    def _NH_FileTransferStreamDidFinish(self, notification):
        self.success = True
        if self.timer is None:
            self.timer = reactor.callLater(2, self.session.end)

    def _NH_SIPSessionDidEnd(self, notification):
        if self.timer is not None and self.timer.active():
            self.timer.cancel()
        self.timer = None
        notification.center.remove_observer(self, sender=self.stream)
        notification.center.remove_observer(self, sender=self.session)
        self.session = None
        self.stream = None
        if self.success:
            notification.center.post_notification(
                'OutgoingFileTransferHandlerDidFail', sender=self)
        else:
            notification.center.post_notification(
                'OutgoingFileTransferHandlerDidEnd', sender=self)

    _NH_SIPSessionDidFail = _NH_SIPSessionDidEnd
Example #5
0
class OutgoingFileTransferHandler(object):
    implements(IObserver)

    def __init__(self, sender_uri, destination_uri, file_selector):
        self.sender_uri = sender_uri
        self.destination_uri = destination_uri
        self.file_selector = file_selector
        self.session = None
        self.stream = None
        self.timer = None
        self.success = False

    @run_in_green_thread
    def start(self):
        notification_center = NotificationCenter()
        self.greenlet = api.getcurrent()
        settings = SIPSimpleSettings()
        account = AccountManager().sylkserver_account
        if account.sip.outbound_proxy is not None:
            uri = SIPURI(host=account.sip.outbound_proxy.host,
                         port=account.sip.outbound_proxy.port,
                         parameters={'transport': account.sip.outbound_proxy.transport})
        else:
            uri = self.destination_uri
        lookup = DNSLookup()
        try:
            routes = lookup.lookup_sip_proxy(uri, settings.sip.transport_list).wait()
        except DNSLookupError:
            notification_center.post_notification('OutgoingFileTransferHandlerDidFail', sender=self)
            return

        self.session = Session(account)
        self.stream = FileTransferStream(self.file_selector, 'sendonly')
        notification_center.add_observer(self, sender=self.session)
        notification_center.add_observer(self, sender=self.stream)
        from_header = FromHeader(self.sender_uri, u'SIPStache File Transfer')
        to_header = ToHeader(self.destination_uri)
        transport = routes[0].transport
        parameters = {} if transport=='udp' else {'transport': transport}
        contact_header = ContactHeader(SIPURI(user=self.sender_uri.user, host=SIPConfig.local_ip.normalized, port=getattr(Engine(), '%s_port' % transport), parameters=parameters))
        extra_headers = []
        if ThorNodeConfig.enabled:
            extra_headers.append(Header('Thor-Scope', 'sipstache-file'))
        extra_headers.append(Header('X-Originator-From', str(self.destination_uri)))
        self.session.connect(from_header, to_header, contact_header=contact_header, routes=routes, streams=[self.stream], is_focus=False, extra_headers=extra_headers)
        notification_center.post_notification('OutgoingFileTransferHandlerDidStart', sender=self)

    def stop(self):
        if self.session is not None:
            self.session.end()

    @run_in_twisted_thread
    def handle_notification(self, notification):
        handler = getattr(self, '_NH_%s' % notification.name, Null)
        handler(notification)

    def _NH_FileTransferStreamDidFinish(self, notification):
        self.success = True
        if self.timer is None:
            self.timer = reactor.callLater(2, self.session.end)

    def _NH_SIPSessionDidEnd(self, notification):
        if self.timer is not None and self.timer.active():
            self.timer.cancel()
        self.timer = None
        notification.center.remove_observer(self, sender=self.stream)
        notification.center.remove_observer(self, sender=self.session)
        self.session = None
        self.stream = None
        if self.success:
            notification.center.post_notification('OutgoingFileTransferHandlerDidFail', sender=self)
        else:
            notification.center.post_notification('OutgoingFileTransferHandlerDidEnd', sender=self)

    _NH_SIPSessionDidFail = _NH_SIPSessionDidEnd
Example #6
0
class ChatSessionHandler(object):
    implements(IObserver)

    sip_identity = WriteOnceAttribute()
    xmpp_identity = WriteOnceAttribute()

    def __init__(self):
        self.started = False
        self.ended = False
        self.sip_session = None
        self.msrp_stream = None
        self._sip_session_timer = None

        self.use_receipts = False
        self.xmpp_session = None
        self._xmpp_message_queue = deque()

        self._pending_msrp_chunks = {}
        self._pending_xmpp_stanzas = {}

    def _set_started(self, value):
        old_value = self.__dict__.get('started', False)
        self.__dict__['started'] = value
        if not old_value and value:
            NotificationCenter().post_notification('ChatSessionDidStart', sender=self)
            self._send_queued_messages()
    def _get_started(self):
        return self.__dict__['started']
    started = property(_get_started, _set_started)
    del _get_started, _set_started

    def _set_xmpp_session(self, session):
        self.__dict__['xmpp_session'] = session
        if session is not None:
            # Reet SIP session timer in case it's active
            if self._sip_session_timer is not None and self._sip_session_timer.active():
                self._sip_session_timer.reset(SESSION_TIMEOUT)
            NotificationCenter().add_observer(self, sender=session)
            session.start()
            # Reet SIP session timer in case it's active
            if self._sip_session_timer is not None and self._sip_session_timer.active():
                self._sip_session_timer.reset(SESSION_TIMEOUT)
    def _get_xmpp_session(self):
        return self.__dict__['xmpp_session']
    xmpp_session = property(_get_xmpp_session, _set_xmpp_session)
    del _get_xmpp_session, _set_xmpp_session

    @classmethod
    def new_from_sip_session(cls, sip_identity, session):
        instance = cls()
        instance.sip_identity = sip_identity
        instance._start_incoming_sip_session(session)
        return instance

    @classmethod
    def new_from_xmpp_stanza(cls, xmpp_identity, recipient):
        instance = cls()
        instance.xmpp_identity = xmpp_identity
        instance._start_outgoing_sip_session(recipient)
        return instance

    @run_in_green_thread
    def _start_incoming_sip_session(self, session):
        self.sip_session = session
        self.msrp_stream = next(stream for stream in session.proposed_streams if stream.type=='chat')
        notification_center = NotificationCenter()
        notification_center.add_observer(self, sender=self.sip_session)
        notification_center.add_observer(self, sender=self.msrp_stream)
        self.sip_session.accept([self.msrp_stream])

    @run_in_green_thread
    def _start_outgoing_sip_session(self, target_uri):
        notification_center = NotificationCenter()
        # self.xmpp_identity is our local identity
        from_uri = self.xmpp_identity.uri.as_sip_uri()
        del from_uri.parameters['gr']    # no GRUU in From header
        contact_uri = self.xmpp_identity.uri.as_sip_uri()
        contact_uri.parameters['gr'] = encode_resource(contact_uri.parameters['gr'].decode('utf-8'))
        to_uri = target_uri.as_sip_uri()
        lookup = DNSLookup()
        settings = SIPSimpleSettings()
        account = DefaultAccount()
        if account.sip.outbound_proxy is not None:
            uri = SIPURI(host=account.sip.outbound_proxy.host,
                         port=account.sip.outbound_proxy.port,
                         parameters={'transport': account.sip.outbound_proxy.transport})
        else:
            uri = to_uri
        try:
            routes = lookup.lookup_sip_proxy(uri, settings.sip.transport_list).wait()
        except DNSLookupError:
            log.warning('DNS lookup error while looking for %s proxy' % uri)
            notification_center.post_notification('ChatSessionDidFail', sender=self, data=NotificationData(reason='DNS lookup error'))
            return
        self.msrp_stream = MediaStreamRegistry().get('chat')()
        route = routes.pop(0)
        from_header = FromHeader(from_uri)
        to_header = ToHeader(to_uri)
        contact_header = ContactHeader(contact_uri)
        self.sip_session = Session(account)
        notification_center.add_observer(self, sender=self.sip_session)
        notification_center.add_observer(self, sender=self.msrp_stream)
        self.sip_session.connect(from_header, to_header, contact_header=contact_header, route=route, streams=[self.msrp_stream])

    def end(self):
        if self.ended:
            return
        if self._sip_session_timer is not None and self._sip_session_timer.active():
            self._sip_session_timer.cancel()
        self._sip_session_timer = None
        notification_center = NotificationCenter()
        if self.sip_session is not None:
            notification_center.remove_observer(self, sender=self.sip_session)
            notification_center.remove_observer(self, sender=self.msrp_stream)
            self.sip_session.end()
            self.sip_session = None
            self.msrp_stream = None
        if self.xmpp_session is not None:
            notification_center.remove_observer(self, sender=self.xmpp_session)
            self.xmpp_session.end()
            self.xmpp_session = None
        self.ended = True
        if self.started:
            notification_center.post_notification('ChatSessionDidEnd', sender=self)
        else:
            notification_center.post_notification('ChatSessionDidFail', sender=self, data=NotificationData(reason='Ended before actually started'))

    def enqueue_xmpp_message(self, message):
        self._xmpp_message_queue.append(message)
        if self.started:
            self._send_queued_messages()

    def _send_queued_messages(self):
        if self._xmpp_message_queue:
            while self._xmpp_message_queue:
                message = self._xmpp_message_queue.popleft()
                if message.body is None:
                    continue
                if not message.use_receipt:
                    success_report = 'no'
                    failure_report = 'no'
                else:
                    success_report = 'yes'
                    failure_report = 'yes'
                sender_uri = message.sender.uri.as_sip_uri()
                sender_uri.parameters['gr'] = encode_resource(sender_uri.parameters['gr'].decode('utf-8'))
                sender = CPIMIdentity(sender_uri)
                self.msrp_stream.send_message(message.body, 'text/plain', sender=sender, message_id=message.id, notify_progress=True, success_report=success_report, failure_report=failure_report)
            self.msrp_stream.send_composing_indication('idle', 30, sender=sender)

    def _inactivity_timeout(self):
        log.msg("Ending SIP session %s due to inactivity" % self.sip_session.call_id)
        self.sip_session.end()

    def handle_notification(self, notification):
        handler = getattr(self, '_NH_%s' % notification.name, Null)
        handler(notification)

    def _NH_SIPSessionDidStart(self, notification):
        log.msg("SIP session %s started" % self.sip_session.call_id)
        self._sip_session_timer = reactor.callLater(SESSION_TIMEOUT, self._inactivity_timeout)

        if self.sip_session.direction == 'outgoing':
            # Time to set sip_identity and create the XMPPChatSession
            contact_uri = self.sip_session._invitation.remote_contact_header.uri
            if contact_uri.parameters.get('gr') is not None:
                sip_leg_uri = FrozenURI(contact_uri.user, contact_uri.host, contact_uri.parameters.get('gr'))
            else:
                tmp = self.sip_session.remote_identity.uri
                sip_leg_uri = FrozenURI(tmp.user, tmp.host, generate_sylk_resource())
            self.sip_identity = Identity(sip_leg_uri, self.sip_session.remote_identity.display_name)
            session = XMPPChatSession(local_identity=self.sip_identity, remote_identity=self.xmpp_identity)
            self.xmpp_session = session
            # Session is now established on both ends
            self.started = True
            # Try to wakeup XMPP clients
            self.xmpp_session.send_composing_indication('active')
            self.xmpp_session.send_message(' ', 'text/plain')
        else:
            if self.xmpp_session is not None:
                # Session is now established on both ends
                self.started = True
                # Try to wakeup XMPP clients
                self.xmpp_session.send_composing_indication('active')
                self.xmpp_session.send_message(' ', 'text/plain')
            else:
                # Try to wakeup XMPP clients
                sender = self.sip_identity
                tmp = self.sip_session.local_identity.uri
                recipient_uri = FrozenURI(tmp.user, tmp.host)
                recipient = Identity(recipient_uri)
                xmpp_manager = XMPPManager()
                xmpp_manager.send_stanza(ChatMessage(sender, recipient, ' ', 'text/plain'))
                # Send queued messages
                self._send_queued_messages()

    def _NH_SIPSessionDidEnd(self, notification):
        log.msg("SIP session %s ended" % self.sip_session.call_id)
        notification.center.remove_observer(self, sender=self.sip_session)
        notification.center.remove_observer(self, sender=self.msrp_stream)
        self.sip_session = None
        self.msrp_stream = None
        self.end()

    def _NH_SIPSessionDidFail(self, notification):
        log.msg("SIP session %s failed" % self.sip_session.call_id)
        notification.center.remove_observer(self, sender=self.sip_session)
        notification.center.remove_observer(self, sender=self.msrp_stream)
        self.sip_session = None
        self.msrp_stream = None
        self.end()

    def _NH_SIPSessionNewProposal(self, notification):
        if notification.data.originator == 'remote':
            self.sip_session.reject_proposal()

    def _NH_SIPSessionTransferNewIncoming(self, notification):
        self.sip_session.reject_transfer(403)

    def _NH_ChatStreamGotMessage(self, notification):
        # Notification is sent by the MSRP stream
        message = notification.data.message
        content_type = message.content_type.lower()
        if content_type not in ('text/plain', 'text/html'):
            return
        if content_type == 'text/plain':
            html_body = None
            body = message.body
        else:
            html_body = message.body
            body = None
        if self._sip_session_timer is not None and self._sip_session_timer.active():
            self._sip_session_timer.reset(SESSION_TIMEOUT)
        chunk = notification.data.chunk
        if self.started:
            self.xmpp_session.send_message(body, html_body, message_id=chunk.message_id)
            if self.use_receipts:
                self._pending_msrp_chunks[chunk.message_id] = chunk
            else:
                self.msrp_stream.msrp_session.send_report(chunk, 200, 'OK')
        else:
            sender = self.sip_identity
            recipient_uri = FrozenURI.parse(message.recipients[0].uri)
            recipient = Identity(recipient_uri, message.recipients[0].display_name)
            xmpp_manager = XMPPManager()
            xmpp_manager.send_stanza(ChatMessage(sender, recipient, body, html_body))
            self.msrp_stream.msrp_session.send_report(chunk, 200, 'OK')

    def _NH_ChatStreamGotComposingIndication(self, notification):
        # Notification is sent by the MSRP stream
        if self._sip_session_timer is not None and self._sip_session_timer.active():
            self._sip_session_timer.reset(SESSION_TIMEOUT)
        if not self.started:
            return
        state = None
        if notification.data.state == 'active':
            state = 'composing'
        elif notification.data.state == 'idle':
            state = 'paused'
        if state is not None:
            self.xmpp_session.send_composing_indication(state)

    def _NH_ChatStreamDidDeliverMessage(self, notification):
        if self.started:
            message = self._pending_xmpp_stanzas.pop(notification.data.message_id, None)
            if message is not None:
                self.xmpp_session.send_receipt_acknowledgement(message.id)

    def _NH_ChatStreamDidNotDeliverMessage(self, notification):
        if self.started:
            message = self._pending_xmpp_stanzas.pop(notification.data.message_id, None)
            if message is not None:
                self.xmpp_session.send_error(message, 'TODO', [])    # TODO

    def _NH_XMPPChatSessionDidStart(self, notification):
        if self.sip_session is not None:
            # Session is now established on both ends
            self.started = True

    def _NH_XMPPChatSessionDidEnd(self, notification):
        notification.center.remove_observer(self, sender=self.xmpp_session)
        self.xmpp_session = None
        self.end()

    def _NH_XMPPChatSessionGotMessage(self, notification):
        if self.sip_session is None or self.sip_session.state != 'connected':
            self._xmpp_message_queue.append(notification.data.message)
            return
        if self._sip_session_timer is not None and self._sip_session_timer.active():
            self._sip_session_timer.reset(SESSION_TIMEOUT)
        message = notification.data.message
        sender_uri = message.sender.uri.as_sip_uri()
        del sender_uri.parameters['gr']    # no GRUU in CPIM From header
        sender = CPIMIdentity(sender_uri)
        self.use_receipts = message.use_receipt
        if not message.use_receipt:
            success_report = 'no'
            failure_report = 'no'
        else:
            success_report = 'yes'
            failure_report = 'yes'
            self._pending_xmpp_stanzas[message.id] = message
        # Prefer plaintext
        self.msrp_stream.send_message(message.body, 'text/plain', sender=sender, message_id=message.id, notify_progress=True, success_report=success_report, failure_report=failure_report)
        self.msrp_stream.send_composing_indication('idle', 30, sender=sender)

    def _NH_XMPPChatSessionGotComposingIndication(self, notification):
        if self.sip_session is None or self.sip_session.state != 'connected':
            return
        if self._sip_session_timer is not None and self._sip_session_timer.active():
            self._sip_session_timer.reset(SESSION_TIMEOUT)
        message = notification.data.message
        state = None
        if message.state == 'composing':
            state = 'active'
        elif message.state == 'paused':
            state = 'idle'
        if state is not None:
            sender_uri = message.sender.uri.as_sip_uri()
            del sender_uri.parameters['gr']    # no GRUU in CPIM From header
            sender = CPIMIdentity(sender_uri)
            self.msrp_stream.send_composing_indication(state, 30, sender=sender)
            if message.use_receipt:
                self.xmpp_session.send_receipt_acknowledgement(message.id)

    def _NH_XMPPChatSessionDidDeliverMessage(self, notification):
        chunk = self._pending_msrp_chunks.pop(notification.data.message_id, None)
        if chunk is not None:
            self.msrp_stream.msrp_session.send_report(chunk, 200, 'OK')

    def _NH_XMPPChatSessionDidNotDeliverMessage(self, notification):
        chunk = self._pending_msrp_chunks.pop(notification.data.message_id, None)
        if chunk is not None:
            self.msrp_stream.msrp_session.send_report(chunk, notification.data.code, notification.data.reason)