Example #1
0
 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()
Example #2
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()
Example #3
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))
Example #4
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))