示例#1
0
    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
示例#2
0
 def start(self):
     if self.state != 'stopped':
         return
     config = get_room_configuration(self.uri.split('@')[0])
     factory = IRCBotFactory(config)
     host, port = config.server
     self.irc_connector = reactor.connectTCP(host, port, factory)
     NotificationCenter().add_observer(self, sender=self.irc_connector.factory)
     self.message_dispatcher = proc.spawn(self._message_dispatcher)
     self.audio_conference = AudioConference()
     self.audio_conference.hold()
     self.state = 'started'
示例#3
0
    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
示例#4
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()
示例#5
0
class IRCRoom(object):
    """
    Object representing a conference room, it will handle the message dispatching
    among all the participants.
    """
    __metaclass__ = Singleton
    implements(IObserver)

    def __init__(self, uri):
        self.uri = uri
        self.identity = ChatIdentity.parse('<sip:%s>' % self.uri)
        self.sessions = []
        self.sessions_with_proposals = []
        self.subscriptions = []
        self.pending_messages = []
        self.state = 'stopped'
        self.incoming_message_queue = coros.queue()
        self.message_dispatcher = None
        self.audio_conference = None
        self.conference_info_payload = None
        self.conference_info_version = count(1)
        self.irc_connector = None
        self.irc_protocol = None

    @classmethod
    def get_room(cls, uri):
        room_uri = '%s@%s' % (uri.user, uri.host)
        room = cls(room_uri)
        return room

    @property
    def empty(self):
        return len(self.sessions) == 0

    @property
    def started(self):
        return self.state == 'started'

    def start(self):
        if self.state != 'stopped':
            return
        config = get_room_configuration(self.uri.split('@')[0])
        factory = IRCBotFactory(config)
        host, port = config.server
        self.irc_connector = reactor.connectTCP(host, port, factory)
        NotificationCenter().add_observer(self, sender=self.irc_connector.factory)
        self.message_dispatcher = proc.spawn(self._message_dispatcher)
        self.audio_conference = AudioConference()
        self.audio_conference.hold()
        self.state = 'started'

    def stop(self):
        if self.state != 'started':
            return
        self.state = 'stopped'
        NotificationCenter().remove_observer(self, sender=self.irc_connector.factory)
        self.irc_connector.factory.stop_requested = True
        self.irc_connector.disconnect()
        self.irc_connector = None
        self.message_dispatcher.kill(proc.ProcExit)
        self.audio_conference = None

    def _message_dispatcher(self):
        """Read from self.incoming_message_queue and dispatch the messages to other participants"""
        while True:
            session, message_type, data = self.incoming_message_queue.wait()
            if message_type == 'msrp_message':
                if data.sender.uri != session.remote_identity.uri:
                    return
                self.dispatch_message(session, data)
            elif message_type == 'irc_message':
                self.dispatch_irc_message(data)

    def dispatch_message(self, session, message):
        identity = ChatIdentity.parse(format_identity(session.remote_identity))
        for s in (s for s in self.sessions if s is not session):
            try:
                chat_stream = next(stream for stream in s.streams if stream.type == 'chat')
            except StopIteration:
                pass
            else:
                chat_stream.send_message(message.content, message.content_type, sender=identity, recipients=[self.identity], timestamp=message.timestamp)

    def dispatch_irc_message(self, message):
        for session in self.sessions:
            try:
                chat_stream = next(stream for stream in session.streams if stream.type == 'chat')
            except StopIteration:
                pass
            else:
                chat_stream.send_message(message.content, message.content_type, sender=message.sender, recipients=[self.identity])

    def dispatch_server_message(self, content, content_type='text/plain', exclude=None):
        for session in (session for session in self.sessions if session is not exclude):
            try:
                chat_stream = next(stream for stream in session.streams if stream.type == 'chat')
            except StopIteration:
                pass
            else:
                chat_stream.send_message(content, content_type, sender=self.identity, recipients=[self.identity])

    def get_conference_info(self):
        # Send request to get participants list, we'll get a notification with it
        if self.irc_protocol is not None:
            self.irc_protocol.get_participants()
        else:
            self.dispatch_conference_info([])

    def dispatch_conference_info(self, irc_participants):
        data = self.build_conference_info_payload(irc_participants)
        for subscription in (subscription for subscription in self.subscriptions if subscription.state == 'active'):
           try:
               subscription.push_content(ConferenceDocument.content_type, data)
           except (SIPCoreError, SIPCoreInvalidStateError):
               pass

    def build_conference_info_payload(self, irc_participants):
        irc_configuration = get_room_configuration(self.uri.split('@')[0])
        if self.conference_info_payload is None:
            settings = SIPSimpleSettings()
            conference_description = ConferenceDescription(display_text='#%s on %s' % (irc_configuration.channel, irc_configuration.server[0]), free_text='Hosted by %s' % settings.user_agent)
            host_info = HostInfo(web_page=WebPage(irc_configuration.website))
            self.conference_info_payload = Conference(self.identity.uri, conference_description=conference_description, host_info=host_info, users=Users())
        self.conference_info_payload.version = next(self.conference_info_version)
        user_count = len({str(s.remote_identity.uri) for s in self.sessions}) + len(irc_participants)
        self.conference_info_payload.conference_state = ConferenceState(user_count=user_count, active=True)
        users = Users()
        for session in self.sessions:
            try:
                user = next(user for user in users if user.entity == str(session.remote_identity.uri))
            except StopIteration:
                user = User(str(session.remote_identity.uri), display_text=session.remote_identity.display_name)
                users.add(user)
            joining_info = JoiningInfo(when=session.start_time)
            holdable_streams = [stream for stream in session.streams if stream.hold_supported]
            session_on_hold = holdable_streams and all(stream.on_hold_by_remote for stream in holdable_streams)
            hold_status = EndpointStatus('on-hold' if session_on_hold else 'connected')
            endpoint = Endpoint(str(session._invitation.remote_contact_header.uri), display_text=session.remote_identity.display_name, joining_info=joining_info, status=hold_status)
            for stream in session.streams:
                endpoint.add(Media(id(stream), media_type=format_conference_stream_type(stream)))
            user.add(endpoint)
        for nick in irc_participants:
            irc_uri = '%s@%s' % (urllib.quote(nick), irc_configuration.server[0])
            user = User(irc_uri, display_text=nick)
            users.add(user)
            endpoint = Endpoint(irc_uri, display_text=nick)
            endpoint.add(Media(random.randint(100000000, 999999999), media_type='message'))
            user.add(endpoint)
        self.conference_info_payload.users = users
        return self.conference_info_payload.toxml()

    def add_session(self, session):
        notification_center = NotificationCenter()
        notification_center.add_observer(self, sender=session)
        self.sessions.append(session)
        try:
            chat_stream = next(stream for stream in session.streams if stream.type == 'chat')
        except StopIteration:
            pass
        else:
            notification_center.add_observer(self, sender=chat_stream)
        try:
            audio_stream = next(stream for stream in session.streams if stream.type == 'audio')
        except StopIteration:
            pass
        else:
            notification_center.add_observer(self, sender=audio_stream)
            log.msg(u'Audio stream using %s/%sHz, end-points: %s:%d <-> %s:%d' % (audio_stream.codec, audio_stream.sample_rate,
                                                                                  audio_stream.local_rtp_address, audio_stream.local_rtp_port,
                                                                                  audio_stream.remote_rtp_address, audio_stream.remote_rtp_port))
            welcome_handler = WelcomeHandler(self, session)
            welcome_handler.start()
        self.get_conference_info()
        if len(self.sessions) == 1:
            log.msg(u'%s started conference %s %s' % (format_identity(session.remote_identity), self.uri, format_stream_types(session.streams)))
        else:
            log.msg(u'%s joined conference %s %s' % (format_identity(session.remote_identity), self.uri, format_stream_types(session.streams)))
        if str(session.remote_identity.uri) not in set(str(s.remote_identity.uri) for s in self.sessions if s is not session):
            self.dispatch_server_message('%s has joined the room %s' % (format_identity(session.remote_identity), format_stream_types(session.streams)), exclude=session)

    def remove_session(self, session):
        notification_center = NotificationCenter()
        try:
            chat_stream = next(stream for stream in session.streams or [] if stream.type == 'chat')
        except StopIteration:
            pass
        else:
            notification_center.remove_observer(self, sender=chat_stream)
        try:
            audio_stream = next(stream for stream in session.streams or [] if stream.type == 'audio')
        except StopIteration:
            pass
        else:
            notification_center.remove_observer(self, sender=audio_stream)
            try:
                self.audio_conference.remove(audio_stream)
            except ValueError:
                # User may hangup before getting bridged into the conference
                pass
            if len(self.audio_conference.streams) == 0:
                self.audio_conference.hold()
        notification_center.remove_observer(self, sender=session)
        self.sessions.remove(session)
        self.get_conference_info()
        log.msg(u'%s left conference %s after %s' % (format_identity(session.remote_identity), self.uri, format_session_duration(session)))
        if not self.sessions:
            log.msg(u'Last participant left conference %s' % self.uri)
        if str(session.remote_identity.uri) not in set(str(s.remote_identity.uri) for s in self.sessions if s is not session):
            self.dispatch_server_message('%s has left the room after %s' % (format_identity(session.remote_identity), format_session_duration(session)))

    def accept_proposal(self, session, streams):
        if session in self.sessions_with_proposals:
            session.accept_proposal(streams)
            self.sessions_with_proposals.remove(session)

    def handle_incoming_subscription(self, subscribe_request, data):
        if subscribe_request.event != 'conference':
            subscribe_request.reject(489)
            return
        NotificationCenter().add_observer(self, sender=subscribe_request)
        self.subscriptions.append(subscribe_request)
        try:
            subscribe_request.accept()
        except SIPCoreError, e:
            log.warning('Error accepting SIP subscription: %s' % e)
            subscribe_request.end()
        self.get_conference_info()
示例#6
0
class IRCRoom(object):
    """
    Object representing a conference room, it will handle the message dispatching
    among all the participants.
    """
    __metaclass__ = Singleton
    implements(IObserver)

    def __init__(self, uri):
        self.uri = uri
        self.identity = ChatIdentity.parse('<sip:%s>' % self.uri)
        self.sessions = []
        self.sessions_with_proposals = []
        self.subscriptions = []
        self.pending_messages = []
        self.state = 'stopped'
        self.incoming_message_queue = coros.queue()
        self.message_dispatcher = None
        self.audio_conference = None
        self.conference_info_payload = None
        self.conference_info_version = count(1)
        self.irc_connector = None
        self.irc_protocol = None

    @classmethod
    def get_room(cls, uri):
        room_uri = '%s@%s' % (uri.user, uri.host)
        room = cls(room_uri)
        return room

    @property
    def empty(self):
        return len(self.sessions) == 0

    @property
    def started(self):
        return self.state == 'started'

    def start(self):
        if self.state != 'stopped':
            return
        config = get_room_configuration(self.uri.split('@')[0])
        factory = IRCBotFactory(config)
        host, port = config.server
        self.irc_connector = reactor.connectTCP(host, port, factory)
        NotificationCenter().add_observer(self,
                                          sender=self.irc_connector.factory)
        self.message_dispatcher = proc.spawn(self._message_dispatcher)
        self.audio_conference = AudioConference()
        self.audio_conference.hold()
        self.state = 'started'

    def stop(self):
        if self.state != 'started':
            return
        self.state = 'stopped'
        NotificationCenter().remove_observer(self,
                                             sender=self.irc_connector.factory)
        self.irc_connector.factory.stop_requested = True
        self.irc_connector.disconnect()
        self.irc_connector = None
        self.message_dispatcher.kill(proc.ProcExit)
        self.audio_conference = None

    def _message_dispatcher(self):
        """Read from self.incoming_message_queue and dispatch the messages to other participants"""
        while True:
            session, message_type, data = self.incoming_message_queue.wait()
            if message_type == 'msrp_message':
                if data.sender.uri != session.remote_identity.uri:
                    return
                self.dispatch_message(session, data)
            elif message_type == 'irc_message':
                self.dispatch_irc_message(data)

    def dispatch_message(self, session, message):
        identity = ChatIdentity.parse(format_identity(session.remote_identity))
        for s in (s for s in self.sessions if s is not session):
            try:
                chat_stream = next(stream for stream in s.streams
                                   if stream.type == 'chat')
            except StopIteration:
                pass
            else:
                chat_stream.send_message(message.content,
                                         message.content_type,
                                         sender=identity,
                                         recipients=[self.identity],
                                         timestamp=message.timestamp)

    def dispatch_irc_message(self, message):
        for session in self.sessions:
            try:
                chat_stream = next(stream for stream in session.streams
                                   if stream.type == 'chat')
            except StopIteration:
                pass
            else:
                chat_stream.send_message(message.content,
                                         message.content_type,
                                         sender=message.sender,
                                         recipients=[self.identity])

    def dispatch_server_message(self,
                                content,
                                content_type='text/plain',
                                exclude=None):
        for session in (session for session in self.sessions
                        if session is not exclude):
            try:
                chat_stream = next(stream for stream in session.streams
                                   if stream.type == 'chat')
            except StopIteration:
                pass
            else:
                chat_stream.send_message(content,
                                         content_type,
                                         sender=self.identity,
                                         recipients=[self.identity])

    def get_conference_info(self):
        # Send request to get participants list, we'll get a notification with it
        if self.irc_protocol is not None:
            self.irc_protocol.get_participants()
        else:
            self.dispatch_conference_info([])

    def dispatch_conference_info(self, irc_participants):
        data = self.build_conference_info_payload(irc_participants)
        for subscription in (subscription
                             for subscription in self.subscriptions
                             if subscription.state == 'active'):
            try:
                subscription.push_content(ConferenceDocument.content_type,
                                          data)
            except (SIPCoreError, SIPCoreInvalidStateError):
                pass

    def build_conference_info_payload(self, irc_participants):
        irc_configuration = get_room_configuration(self.uri.split('@')[0])
        if self.conference_info_payload is None:
            settings = SIPSimpleSettings()
            conference_description = ConferenceDescription(
                display_text='#%s on %s' %
                (irc_configuration.channel, irc_configuration.server[0]),
                free_text='Hosted by %s' % settings.user_agent)
            host_info = HostInfo(web_page=WebPage(irc_configuration.website))
            self.conference_info_payload = Conference(
                self.identity.uri,
                conference_description=conference_description,
                host_info=host_info,
                users=Users())
        self.conference_info_payload.version = next(
            self.conference_info_version)
        user_count = len({str(s.remote_identity.uri)
                          for s in self.sessions}) + len(irc_participants)
        self.conference_info_payload.conference_state = ConferenceState(
            user_count=user_count, active=True)
        users = Users()
        for session in self.sessions:
            try:
                user = next(user for user in users
                            if user.entity == str(session.remote_identity.uri))
            except StopIteration:
                user = User(str(session.remote_identity.uri),
                            display_text=session.remote_identity.display_name)
                users.add(user)
            joining_info = JoiningInfo(when=session.start_time)
            holdable_streams = [
                stream for stream in session.streams if stream.hold_supported
            ]
            session_on_hold = holdable_streams and all(
                stream.on_hold_by_remote for stream in holdable_streams)
            hold_status = EndpointStatus(
                'on-hold' if session_on_hold else 'connected')
            endpoint = Endpoint(
                str(session._invitation.remote_contact_header.uri),
                display_text=session.remote_identity.display_name,
                joining_info=joining_info,
                status=hold_status)
            for stream in session.streams:
                endpoint.add(
                    Media(id(stream),
                          media_type=format_conference_stream_type(stream)))
            user.add(endpoint)
        for nick in irc_participants:
            irc_uri = '%s@%s' % (urllib.quote(nick),
                                 irc_configuration.server[0])
            user = User(irc_uri, display_text=nick)
            users.add(user)
            endpoint = Endpoint(irc_uri, display_text=nick)
            endpoint.add(
                Media(random.randint(100000000, 999999999),
                      media_type='message'))
            user.add(endpoint)
        self.conference_info_payload.users = users
        return self.conference_info_payload.toxml()

    def add_session(self, session):
        notification_center = NotificationCenter()
        notification_center.add_observer(self, sender=session)
        self.sessions.append(session)
        try:
            chat_stream = next(stream for stream in session.streams
                               if stream.type == 'chat')
        except StopIteration:
            pass
        else:
            notification_center.add_observer(self, sender=chat_stream)
        try:
            audio_stream = next(stream for stream in session.streams
                                if stream.type == 'audio')
        except StopIteration:
            pass
        else:
            notification_center.add_observer(self, sender=audio_stream)
            log.info(
                u'Audio stream using %s/%sHz, end-points: %s:%d <-> %s:%d' %
                (audio_stream.codec, audio_stream.sample_rate,
                 audio_stream.local_rtp_address, audio_stream.local_rtp_port,
                 audio_stream.remote_rtp_address,
                 audio_stream.remote_rtp_port))
            welcome_handler = WelcomeHandler(self, session)
            welcome_handler.start()
        self.get_conference_info()
        if len(self.sessions) == 1:
            log.info(u'%s started conference %s %s' %
                     (format_identity(session.remote_identity), self.uri,
                      format_stream_types(session.streams)))
        else:
            log.info(u'%s joined conference %s %s' %
                     (format_identity(session.remote_identity), self.uri,
                      format_stream_types(session.streams)))
        if str(session.remote_identity.uri) not in set(
                str(s.remote_identity.uri) for s in self.sessions
                if s is not session):
            self.dispatch_server_message(
                '%s has joined the room %s' %
                (format_identity(session.remote_identity),
                 format_stream_types(session.streams)),
                exclude=session)

    def remove_session(self, session):
        notification_center = NotificationCenter()
        try:
            chat_stream = next(stream for stream in session.streams or []
                               if stream.type == 'chat')
        except StopIteration:
            pass
        else:
            notification_center.remove_observer(self, sender=chat_stream)
        try:
            audio_stream = next(stream for stream in session.streams or []
                                if stream.type == 'audio')
        except StopIteration:
            pass
        else:
            notification_center.remove_observer(self, sender=audio_stream)
            try:
                self.audio_conference.remove(audio_stream)
            except ValueError:
                # User may hangup before getting bridged into the conference
                pass
            if len(self.audio_conference.streams) == 0:
                self.audio_conference.hold()
        notification_center.remove_observer(self, sender=session)
        self.sessions.remove(session)
        self.get_conference_info()
        log.info(u'%s left conference %s after %s' %
                 (format_identity(session.remote_identity), self.uri,
                  format_session_duration(session)))
        if not self.sessions:
            log.info(u'Last participant left conference %s' % self.uri)
        if str(session.remote_identity.uri) not in set(
                str(s.remote_identity.uri) for s in self.sessions
                if s is not session):
            self.dispatch_server_message(
                '%s has left the room after %s' %
                (format_identity(session.remote_identity),
                 format_session_duration(session)))

    def accept_proposal(self, session, streams):
        if session in self.sessions_with_proposals:
            session.accept_proposal(streams)
            self.sessions_with_proposals.remove(session)

    def handle_incoming_subscription(self, subscribe_request, data):
        if subscribe_request.event != 'conference':
            subscribe_request.reject(489)
            return
        NotificationCenter().add_observer(self, sender=subscribe_request)
        self.subscriptions.append(subscribe_request)
        try:
            subscribe_request.accept()
        except SIPCoreError as e:
            log.warning('Error accepting SIP subscription: %s' % e)
            subscribe_request.end()
        self.get_conference_info()

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

    def _NH_SIPIncomingSubscriptionDidEnd(self, notification):
        subscription = notification.sender
        notification_center = NotificationCenter()
        notification_center.remove_observer(self, sender=subscription)
        self.subscriptions.remove(subscription)

    def _NH_SIPSessionDidChangeHoldState(self, notification):
        session = notification.sender
        if notification.data.originator == 'remote':
            if notification.data.on_hold:
                log.info(u'%s has put the audio session on hold' %
                         format_identity(session.remote_identity))
            else:
                log.info(u'%s has taken the audio session out of hold' %
                         format_identity(session.remote_identity))
            self.get_conference_info()

    def _NH_SIPSessionNewProposal(self, notification):
        if notification.data.originator == 'remote':
            session = notification.sender
            audio_streams = [
                stream for stream in notification.data.proposed_streams
                if stream.type == 'audio'
            ]
            chat_streams = [
                stream for stream in notification.data.proposed_streams
                if stream.type == 'chat'
            ]
            if not audio_streams and not chat_streams:
                session.reject_proposal()
                return
            if chat_streams:
                chat_streams[0].chatroom_capabilities = []
            streams = [
                streams[0] for streams in (audio_streams, chat_streams)
                if streams
            ]
            self.sessions_with_proposals.append(session)
            reactor.callLater(4, self.accept_proposal, session, streams)

    def _NH_SIPSessionProposalRejected(self, notification):
        session = notification.sender
        self.sessions_with_proposals.remove(session)

    def _NH_SIPSessionDidRenegotiateStreams(self, notification):
        session = notification.sender
        for stream in notification.data.added_streams:
            notification.center.add_observer(self, sender=stream)
            log.info(u'%s has added %s to %s' % (format_identity(
                session.remote_identity), stream.type, self.uri))
            self.dispatch_server_message(
                '%s has added %s' %
                (format_identity(session.remote_identity), stream.type),
                exclude=session)
            if stream.type == 'audio':
                log.info(
                    u'Audio stream using %s/%sHz, end-points: %s:%d <-> %s:%d'
                    % (stream.codec, stream.sample_rate,
                       stream.local_rtp_address, stream.local_rtp_port,
                       stream.remote_rtp_address, stream.remote_rtp_port))
                welcome_handler = WelcomeHandler(self, session)
                welcome_handler.start(welcome_prompt=False)

        for stream in notification.data.removed_streams:
            notification.center.remove_observer(self, sender=stream)
            log.info(u'%s has removed %s from %s' % (format_identity(
                session.remote_identity), stream.type, self.uri))
            self.dispatch_server_message(
                '%s has removed %s' %
                (format_identity(session.remote_identity), stream.type),
                exclude=session)
            if stream.type == 'audio':
                try:
                    self.audio_conference.remove(stream)
                except ValueError:
                    # User may hangup before getting bridged into the conference
                    pass
                if len(self.audio_conference.streams) == 0:
                    self.audio_conference.hold()
            if not session.streams:
                log.info(
                    u'%s has removed all streams from %s, session will be terminated'
                    % (format_identity(session.remote_identity), self.uri))
                session.end()
        self.get_conference_info()

    def _NH_RTPStreamDidTimeout(self, notification):
        stream = notification.sender
        if stream.type != 'audio':
            return
        session = stream.session
        log.info(u'Audio stream for session %s timed out' %
                 format_identity(session.remote_identity))
        if session.streams == [stream]:
            session.end()

    def _NH_ChatStreamGotMessage(self, notification):
        stream = notification.sender
        message = notification.data.message
        if message.content_type not in ('text/html', 'text/plain'):
            log.info(u'Unsupported content type: %s, ignoring message' %
                     message.content_type)
            stream.msrp_session.send_report(notification.data.chunk, 413,
                                            'Unwanted message')
            return
        stream.msrp_session.send_report(notification.data.chunk, 200, 'OK')
        # Send MSRP chat message to other participants
        session = stream.session
        self.incoming_message_queue.send((session, 'msrp_message', message))
        # Send MSRP chat message to IRC chat room
        if message.content_type == 'text/html':
            content = html2text(message.content)
        elif message.content_type == 'text/plain':
            content = message.content
        else:
            log.warning('unexpected message type: %s' % message.content_type)
            return
        sender = message.sender
        irc_message = '%s: %s' % (format_identity(sender), content)
        if self.irc_protocol is not None:
            self.irc_protocol.send_message(irc_message.encode('utf-8'))
        else:
            self.pending_messages.append(irc_message)

    def _NH_ChatStreamGotNicknameRequest(self, notification):
        # Discard the nickname but pretend we accept it so that XMPP clients can work
        chunk = notification.data.chunk
        notification.sender.accept_nickname(chunk)

    def _NH_IRCBotGotConnected(self, notification):
        self.irc_protocol = notification.data.protocol
        # Send enqueued messages
        while self.pending_messages:
            message = self.pending_messages.pop(0)
            self.irc_protocol.send_message(message.encode('utf-8'))
        # Update participants list
        self.get_conference_info()

    def _NH_IRCBotGotDisconnected(self, notification):
        self.irc_protocol = None

    def _NH_IRCBotGotMessage(self, notification):
        message = notification.data.message
        self.incoming_message_queue.send((None, 'irc_message', message))

    def _NH_IRCBotGotParticipantsList(self, notification):
        self.dispatch_conference_info(notification.data.participants)

    def _NH_IRCBotJoinedChannel(self, notification):
        self.get_conference_info()

    def _NH_IRCBotUserJoined(self, notification):
        self.dispatch_server_message('%s joined the IRC channel' %
                                     notification.data.user)
        self.get_conference_info()

    def _NH_IRCBotUserLeft(self, notification):
        self.dispatch_server_message('%s left the IRC channel' %
                                     notification.data.user)
        self.get_conference_info()

    def _NH_IRCBotUserQuit(self, notification):
        self.dispatch_server_message(
            '%s quit the IRC channel: %s' %
            (notification.data.user, notification.data.reason))
        self.get_conference_info()

    def _NH_IRCBotUserKicked(self, notification):
        data = notification.data
        self.dispatch_server_message(
            '%s kicked %s out of the IRC channel: %s' %
            (data.kicker, data.kickee, data.reason))
        self.get_conference_info()

    def _NH_IRCBotUserRenamed(self, notification):
        self.dispatch_server_message(
            '%s changed his name to %s' %
            (notification.data.oldname, notification.data.newname))
        self.get_conference_info()

    def _NH_IRCBotUserAction(self, notification):
        self.dispatch_server_message(
            '%s %s' % (notification.data.user, notification.data.action))
示例#7
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()
示例#8
0
class IRCRoom(object):
    """
    Object representing a conference room, it will handle the message dispatching
    among all the participants.
    """
    __metaclass__ = Singleton
    implements(IObserver)

    def __init__(self, uri):
        self.uri = uri
        self.identity = ChatIdentity.parse('<sip:%s>' % self.uri)
        self.sessions = []
        self.sessions_with_proposals = []
        self.subscriptions = []
        self.pending_messages = []
        self.state = 'stopped'
        self.incoming_message_queue = coros.queue()
        self.message_dispatcher = None
        self.audio_conference = None
        self.conference_info_payload = None
        self.conference_info_version = count(1)
        self.irc_connector = None
        self.irc_protocol = None

    @classmethod
    def get_room(cls, uri):
        room_uri = '%s@%s' % (uri.user, uri.host)
        room = cls(room_uri)
        return room

    @property
    def empty(self):
        return len(self.sessions) == 0

    @property
    def started(self):
        return self.state == 'started'

    def start(self):
        if self.state != 'stopped':
            return
        config = get_room_configuration(self.uri.split('@')[0])
        factory = IRCBotFactory(config)
        host, port = config.server
        self.irc_connector = reactor.connectTCP(host, port, factory)
        NotificationCenter().add_observer(self, sender=self.irc_connector.factory)
        self.message_dispatcher = proc.spawn(self._message_dispatcher)
        self.audio_conference = AudioConference()
        self.audio_conference.hold()
        self.state = 'started'

    def stop(self):
        if self.state != 'started':
            return
        self.state = 'stopped'
        NotificationCenter().remove_observer(self, sender=self.irc_connector.factory)
        self.irc_connector.factory.stop_requested = True
        self.irc_connector.disconnect()
        self.irc_connector = None
        self.message_dispatcher.kill(proc.ProcExit)
        self.audio_conference = None

    def _message_dispatcher(self):
        """Read from self.incoming_message_queue and dispatch the messages to other participants"""
        while True:
            session, message_type, data = self.incoming_message_queue.wait()
            if message_type == 'msrp_message':
                if data.sender.uri != session.remote_identity.uri:
                    return
                self.dispatch_message(session, data)
            elif message_type == 'irc_message':
                self.dispatch_irc_message(data)

    def dispatch_message(self, session, message):
        identity = ChatIdentity.parse(format_identity(session.remote_identity, True))
        for s in (s for s in self.sessions if s is not session):
            try:
                chat_stream = next(stream for stream in s.streams if stream.type == 'chat')
            except StopIteration:
                pass
            else:
                chat_stream.send_message(message.content, message.content_type, sender=identity, recipients=[self.identity], timestamp=message.timestamp)

    def dispatch_irc_message(self, message):
        for session in self.sessions:
            try:
                chat_stream = next(stream for stream in session.streams if stream.type == 'chat')
            except StopIteration:
                pass
            else:
                chat_stream.send_message(message.content, message.content_type, sender=message.sender, recipients=[self.identity])

    def dispatch_server_message(self, content, content_type='text/plain', exclude=None):
        for session in (session for session in self.sessions if session is not exclude):
            try:
                chat_stream = next(stream for stream in session.streams if stream.type == 'chat')
            except StopIteration:
                pass
            else:
                chat_stream.send_message(content, content_type, sender=self.identity, recipients=[self.identity])

    def get_conference_info(self):
        # Send request to get participants list, we'll get a notification with it
        if self.irc_protocol is not None:
            self.irc_protocol.get_participants()
        else:
            self.dispatch_conference_info([])

    def dispatch_conference_info(self, irc_participants):
        data = self.build_conference_info_payload(irc_participants)
        for subscription in (subscription for subscription in self.subscriptions if subscription.state == 'active'):
           try:
               subscription.push_content(ConferenceDocument.content_type, data)
           except (SIPCoreError, SIPCoreInvalidStateError):
               pass

    def build_conference_info_payload(self, irc_participants):
        irc_configuration = get_room_configuration(self.uri.split('@')[0])
        if self.conference_info_payload is None:
            settings = SIPSimpleSettings()
            conference_description = ConferenceDescription(display_text='#%s on %s' % (irc_configuration.channel, irc_configuration.server[0]), free_text='Hosted by %s' % settings.user_agent)
            host_info = HostInfo(web_page=WebPage(irc_configuration.website))
            self.conference_info_payload = Conference(self.identity.uri, conference_description=conference_description, host_info=host_info, users=Users())
        self.conference_info_payload.version = next(self.conference_info_version)
        user_count = len(set(str(s.remote_identity.uri) for s in self.sessions)) + len(irc_participants)
        self.conference_info_payload.conference_state = ConferenceState(user_count=user_count, active=True)
        users = Users()
        for session in self.sessions:
            try:
                user = next(user for user in users if user.entity == str(session.remote_identity.uri))
            except StopIteration:
                user = User(str(session.remote_identity.uri), display_text=session.remote_identity.display_name)
                users.add(user)
            joining_info = JoiningInfo(when=session.start_time)
            holdable_streams = [stream for stream in session.streams if stream.hold_supported]
            session_on_hold = holdable_streams and all(stream.on_hold_by_remote for stream in holdable_streams)
            hold_status = EndpointStatus('on-hold' if session_on_hold else 'connected')
            endpoint = Endpoint(str(session._invitation.remote_contact_header.uri), display_text=session.remote_identity.display_name, joining_info=joining_info, status=hold_status)
            for stream in session.streams:
                endpoint.add(Media(id(stream), media_type=format_conference_stream_type(stream)))
            user.add(endpoint)
        for nick in irc_participants:
            irc_uri = '%s@%s' % (urllib.quote(nick), irc_configuration.server[0])
            user = User(irc_uri, display_text=nick)
            users.add(user)
            endpoint = Endpoint(irc_uri, display_text=nick)
            endpoint.add(Media(random.randint(100000000, 999999999), media_type='message'))
            user.add(endpoint)
        self.conference_info_payload.users = users
        return self.conference_info_payload.toxml()

    def add_session(self, session):
        notification_center = NotificationCenter()
        notification_center.add_observer(self, sender=session)
        self.sessions.append(session)
        try:
            chat_stream = next(stream for stream in session.streams if stream.type == 'chat')
        except StopIteration:
            pass
        else:
            notification_center.add_observer(self, sender=chat_stream)
        try:
            audio_stream = next(stream for stream in session.streams if stream.type == 'audio')
        except StopIteration:
            pass
        else:
            notification_center.add_observer(self, sender=audio_stream)
            log.msg(u'Audio stream using %s/%sHz (%s), end-points: %s:%d <-> %s:%d' % (audio_stream.codec, audio_stream.sample_rate,
                                                                                       audio_stream.local_rtp_address, audio_stream.local_rtp_port,
                                                                                       audio_stream.remote_rtp_address, audio_stream.remote_rtp_port))
            welcome_handler = WelcomeHandler(self, session)
            welcome_handler.start()
        self.get_conference_info()
        if len(self.sessions) == 1:
            log.msg(u'%s started conference %s %s' % (format_identity(session.remote_identity), self.uri, format_stream_types(session.streams)))
        else:
            log.msg(u'%s joined conference %s %s' % (format_identity(session.remote_identity), self.uri, format_stream_types(session.streams)))
        if str(session.remote_identity.uri) not in set(str(s.remote_identity.uri) for s in self.sessions if s is not session):
            self.dispatch_server_message('%s has joined the room %s' % (format_identity(session.remote_identity), format_stream_types(session.streams)), exclude=session)

    def remove_session(self, session):
        notification_center = NotificationCenter()
        try:
            chat_stream = next(stream for stream in session.streams or [] if stream.type == 'chat')
        except StopIteration:
            pass
        else:
            notification_center.remove_observer(self, sender=chat_stream)
        try:
            audio_stream = next(stream for stream in session.streams or [] if stream.type == 'audio')
        except StopIteration:
            pass
        else:
            notification_center.remove_observer(self, sender=audio_stream)
            try:
                self.audio_conference.remove(audio_stream)
            except ValueError:
                # User may hangup before getting bridged into the conference
                pass
            if len(self.audio_conference.streams) == 0:
                self.audio_conference.hold()
        notification_center.remove_observer(self, sender=session)
        self.sessions.remove(session)
        self.get_conference_info()
        log.msg(u'%s left conference %s after %s' % (format_identity(session.remote_identity), self.uri, format_session_duration(session)))
        if not self.sessions:
            log.msg(u'Last participant left conference %s' % self.uri)
        if str(session.remote_identity.uri) not in set(str(s.remote_identity.uri) for s in self.sessions if s is not session):
            self.dispatch_server_message('%s has left the room after %s' % (format_identity(session.remote_identity), format_session_duration(session)))

    def accept_proposal(self, session, streams):
        if session in self.sessions_with_proposals:
            session.accept_proposal(streams)
            self.sessions_with_proposals.remove(session)

    def handle_incoming_subscription(self, subscribe_request, data):
        if subscribe_request.event != 'conference':
            subscribe_request.reject(489)
            return
        NotificationCenter().add_observer(self, sender=subscribe_request)
        subscribe_request.accept()
        self.subscriptions.append(subscribe_request)
        self.get_conference_info()

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

    def _NH_SIPIncomingSubscriptionDidEnd(self, notification):
        subscription = notification.sender
        notification_center = NotificationCenter()
        notification_center.remove_observer(self, sender=subscription)
        self.subscriptions.remove(subscription)

    def _NH_SIPSessionDidChangeHoldState(self, notification):
        session = notification.sender
        if notification.data.originator == 'remote':
            if notification.data.on_hold:
                log.msg(u'%s has put the audio session on hold' % format_identity(session.remote_identity))
            else:
                log.msg(u'%s has taken the audio session out of hold' % format_identity(session.remote_identity))
            self.get_conference_info()

    def _NH_SIPSessionNewProposal(self, notification):
        if notification.data.originator == 'remote':
            session = notification.sender
            audio_streams = [stream for stream in notification.data.proposed_streams if stream.type=='audio']
            chat_streams = [stream for stream in notification.data.proposed_streams if stream.type=='chat']
            if not audio_streams and not chat_streams:
                session.reject_proposal()
                return
            if chat_streams:
                chat_streams[0].chatroom_capabilities = []
            streams = [streams[0] for streams in (audio_streams, chat_streams) if streams]
            self.sessions_with_proposals.append(session)
            reactor.callLater(4, self.accept_proposal, session, streams)

    def _NH_SIPSessionProposalRejected(self, notification):
        session = notification.sender
        self.sessions_with_proposals.remove(session)

    def _NH_SIPSessionDidRenegotiateStreams(self, notification):
        session = notification.sender
        for stream in notification.data.added_streams:
            notification.center.add_observer(self, sender=stream)
            log.msg(u'%s has added %s to %s' % (format_identity(session.remote_identity), stream.type, self.uri))
            self.dispatch_server_message('%s has added %s' % (format_identity(session.remote_identity), stream.type), exclude=session)
            if stream.type == 'audio':
                log.msg(u'Audio stream using %s/%sHz, end-points: %s:%d <-> %s:%d' % (stream.codec, stream.sample_rate,
                                                                                      stream.local_rtp_address, stream.local_rtp_port,
                                                                                      stream.remote_rtp_address, stream.remote_rtp_port))
                welcome_handler = WelcomeHandler(self, session)
                welcome_handler.start(welcome_prompt=False)

        for stream in notification.data.removed_streams:
            notification.center.remove_observer(self, sender=stream)
            log.msg(u'%s has removed %s from %s' % (format_identity(session.remote_identity), stream.type, self.uri))
            self.dispatch_server_message('%s has removed %s' % (format_identity(session.remote_identity), stream.type), exclude=session)
            if stream.type == 'audio':
                try:
                    self.audio_conference.remove(stream)
                except ValueError:
                    # User may hangup before getting bridged into the conference
                    pass
                if len(self.audio_conference.streams) == 0:
                    self.audio_conference.hold()
            if not session.streams:
                log.msg(u'%s has removed all streams from %s, session will be terminated' % (format_identity(session.remote_identity), self.uri))
                session.end()
        self.get_conference_info()

    def _NH_RTPStreamDidTimeout(self, notification):
        stream = notification.sender
        if stream.type != 'audio':
            return
        session = stream.session
        log.msg(u'Audio stream for session %s timed out' % format_identity(session.remote_identity))
        if session.streams == [stream]:
            session.end()

    def _NH_ChatStreamGotMessage(self, notification):
        stream = notification.sender
        message = notification.data.message
        if message.content_type not in ('text/html', 'text/plain'):
            log.msg(u'Unsupported content type: %s, ignoring message' % message.content_type)
            stream.msrp_session.send_report(notification.data.chunk, 413, 'Unwanted message')
            return
        stream.msrp_session.send_report(notification.data.chunk, 200, 'OK')
        # Send MSRP chat message to other participants
        session = stream.session
        self.incoming_message_queue.send((session, 'msrp_message', message))
        # Send MSRP chat message to IRC chat room
        if message.content_type == 'text/html':
            content = html2text(message.content)
        elif message.content_type == 'text/plain':
            content = message.content
        else:
            log.warning('unexpected message type: %s' % message.content_type)
            return
        sender = message.sender
        irc_message = '%s: %s' % (format_identity(sender), content)
        if self.irc_protocol is not None:
            self.irc_protocol.send_message(irc_message.encode('utf-8'))
        else:
            self.pending_messages.append(irc_message)

    def _NH_ChatStreamGotNicknameRequest(self, notification):
        # Discard the nickname but pretend we accept it so that XMPP clients can work
        chunk = notification.data.chunk
        notification.sender.accept_nickname(chunk)

    def _NH_IRCBotGotConnected(self, notification):
        self.irc_protocol = notification.data.protocol
        # Send enqueued messages
        while self.pending_messages:
            message = self.pending_messages.pop(0)
            self.irc_protocol.send_message(message.encode('utf-8'))
        # Update participants list
        self.get_conference_info()

    def _NH_IRCBotGotDisconnected(self, notification):
        self.irc_protocol = None

    def _NH_IRCBotGotMessage(self, notification):
        message = notification.data.message
        self.incoming_message_queue.send((None, 'irc_message', message))

    def _NH_IRCBotGotParticipantsList(self, notification):
        self.dispatch_conference_info(notification.data.participants)

    def _NH_IRCBotJoinedChannel(self, notification):
        self.get_conference_info()

    def _NH_IRCBotUserJoined(self, notification):
        self.dispatch_server_message('%s joined the IRC channel' % notification.data.user)
        self.get_conference_info()

    def _NH_IRCBotUserLeft(self, notification):
        self.dispatch_server_message('%s left the IRC channel' % notification.data.user)
        self.get_conference_info()

    def _NH_IRCBotUserQuit(self, notification):
        self.dispatch_server_message('%s quit the IRC channel: %s' % (notification.data.user, notification.data.reason))
        self.get_conference_info()

    def _NH_IRCBotUserKicked(self, notification):
        data = notification.data
        self.dispatch_server_message('%s kicked %s out of the IRC channel: %s' % (data.kicker, data.kickee, data.reason))
        self.get_conference_info()

    def _NH_IRCBotUserRenamed(self, notification):
        self.dispatch_server_message('%s changed his name to %s' % (notification.data.oldname, notification.data.newname))
        self.get_conference_info()

    def _NH_IRCBotUserAction(self, notification):
        self.dispatch_server_message('%s %s' % (notification.data.user, notification.data.action))