Пример #1
0
    def on_voice_ready(self, data):
        self.log.info(
            '[%s] Recived Voice READY payload, attempting to negotiate voice connection w/ remote',
            self)
        self.set_state(VoiceState.CONNECTING)
        self.ssrc = data['ssrc']
        self.port = data['port']

        for mode in self.SUPPORTED_MODES:
            if mode in data['modes']:
                self.mode = mode
                self.log.debug('[%s] Selected mode %s', self, mode)
                break
        else:
            raise Exception('Failed to find a supported voice mode')

        self.log.debug('[%s] Attempting IP discovery over UDP to %s:%s', self,
                       self.endpoint, self.port)
        self.udp = UDPVoiceClient(self)
        ip, port = self.udp.connect(self.endpoint, self.port)

        if not ip:
            self.log.error(
                'Failed to discover our IP, perhaps a NAT or firewall is f*****g us'
            )
            self.disconnect()
            return

        self.log.debug(
            '[%s] IP discovery completed (ip = %s, port = %s), sending SELECT_PROTOCOL',
            self, ip, port)
        self.send(
            VoiceOPCode.SELECT_PROTOCOL, {
                'protocol': 'udp',
                'data': {
                    'port': port,
                    'address': ip,
                    'mode': self.mode,
                },
            })
Пример #2
0
class VoiceClient(LoggingClass):
    VOICE_GATEWAY_VERSION = 3

    SUPPORTED_MODES = {
        'xsalsa20_poly1305_lite',
        'xsalsa20_poly1305_suffix',
        'xsalsa20_poly1305',
    }

    def __init__(self, channel, encoder=None, max_reconnects=5):
        super(VoiceClient, self).__init__()

        if not channel.is_voice:
            raise ValueError(
                'Cannot spawn a VoiceClient for a non-voice channel')

        self.channel = channel
        self.client = self.channel.client
        self.encoder = encoder or JSONEncoder
        self.max_reconnects = max_reconnects

        # Bind to some WS packets
        self.packets = Emitter()
        self.packets.on(VoiceOPCode.HELLO, self.on_voice_hello)
        self.packets.on(VoiceOPCode.READY, self.on_voice_ready)
        self.packets.on(VoiceOPCode.RESUMED, self.on_voice_resumed)
        self.packets.on(VoiceOPCode.SESSION_DESCRIPTION, self.on_voice_sdp)

        # State + state change emitter
        self.state = VoiceState.DISCONNECTED
        self.state_emitter = Emitter()

        # Connection metadata
        self.token = None
        self.endpoint = None
        self.ssrc = None
        self.port = None
        self.mode = None
        self.udp = None

        # Websocket connection
        self.ws = None

        self._session_id = None
        self._reconnects = 0
        self._update_listener = None
        self._heartbeat_task = None

    def __repr__(self):
        return u'<VoiceClient {}>'.format(self.channel)

    def set_state(self, state):
        self.log.debug('[%s] state %s -> %s', self, self.state, state)
        prev_state = self.state
        self.state = state
        self.state_emitter.emit(state, prev_state)

    def _connect_and_run(self):
        self.ws = Websocket('wss://' + self.endpoint +
                            '/v={}'.format(self.VOICE_GATEWAY_VERSION))
        self.ws.emitter.on('on_open', self.on_open)
        self.ws.emitter.on('on_error', self.on_error)
        self.ws.emitter.on('on_close', self.on_close)
        self.ws.emitter.on('on_message', self.on_message)
        self.ws.run_forever()

    def _heartbeat(self, interval):
        while True:
            self.send(VoiceOPCode.HEARTBEAT, time.time())
            gevent.sleep(interval / 1000)

    def set_speaking(self, value):
        self.send(VoiceOPCode.SPEAKING, {
            'speaking': value,
            'delay': 0,
        })

    def send(self, op, data):
        self.log.debug('[%s] sending OP %s (data = %s)', self, op, data)
        self.ws.send(self.encoder.encode({
            'op': op.value,
            'd': data,
        }), self.encoder.OPCODE)

    def on_voice_hello(self, data):
        self.log.info(
            '[%s] Recieved Voice HELLO payload, starting heartbeater', self)
        self._heartbeat_task = gevent.spawn(self._heartbeat,
                                            data['heartbeat_interval'] * 0.75)
        self.set_state(VoiceState.AUTHENTICATED)

    def on_voice_ready(self, data):
        self.log.info(
            '[%s] Recived Voice READY payload, attempting to negotiate voice connection w/ remote',
            self)
        self.set_state(VoiceState.CONNECTING)
        self.ssrc = data['ssrc']
        self.port = data['port']

        for mode in self.SUPPORTED_MODES:
            if mode in data['modes']:
                self.mode = mode
                self.log.debug('[%s] Selected mode %s', self, mode)
                break
        else:
            raise Exception('Failed to find a supported voice mode')

        self.log.debug('[%s] Attempting IP discovery over UDP to %s:%s', self,
                       self.endpoint, self.port)
        self.udp = UDPVoiceClient(self)
        ip, port = self.udp.connect(self.endpoint, self.port)

        if not ip:
            self.log.error(
                'Failed to discover our IP, perhaps a NAT or firewall is f*****g us'
            )
            self.disconnect()
            return

        self.log.debug(
            '[%s] IP discovery completed (ip = %s, port = %s), sending SELECT_PROTOCOL',
            self, ip, port)
        self.send(
            VoiceOPCode.SELECT_PROTOCOL, {
                'protocol': 'udp',
                'data': {
                    'port': port,
                    'address': ip,
                    'mode': self.mode,
                },
            })

    def on_voice_resumed(self, data):
        self.log.info('[%s] Recieved resumed', self)
        self.set_state(VoiceState.CONNECTED)

    def on_voice_sdp(self, sdp):
        self.log.info(
            '[%s] Recieved session description, connection completed', self)

        # Create a secret box for encryption/decryption
        self.udp.setup_encryption(bytes(bytearray(sdp['secret_key'])))

        # Toggle speaking state so clients learn of our SSRC
        self.set_speaking(True)
        self.set_speaking(False)
        gevent.sleep(0.25)

        self.set_state(VoiceState.CONNECTED)

    def on_voice_server_update(self, data):
        if self.channel.guild_id != data.guild_id or not data.token:
            return

        if self.token and self.token != data.token:
            return

        self.log.info(
            '[%s] Recieved VOICE_SERVER_UPDATE (state = %s / endpoint = %s)',
            self, self.state, data.endpoint)

        self.token = data.token
        self.set_state(VoiceState.AUTHENTICATING)

        self.endpoint = data.endpoint.split(':', 1)[0]

        self._connect_and_run()

    def on_message(self, msg):
        try:
            data = self.encoder.decode(msg)
            self.packets.emit(VoiceOPCode[data['op']], data['d'])
        except Exception:
            self.log.exception('Failed to parse voice gateway message: ')

    def on_error(self, err):
        self.log.error('[%s] Voice websocket error: %s', self, err)

    def on_open(self):
        if self._session_id:
            return self.send(
                VoiceOPCode.RESUME, {
                    'server_id': self.channel.guild_id,
                    'user_id': self.client.state.me.id,
                    'session_id': self._session_id,
                    'token': self.token,
                })

        self._session_id = self.client.gw.session_id

        self.send(
            VoiceOPCode.IDENTIFY, {
                'server_id': self.channel.guild_id,
                'user_id': self.client.state.me.id,
                'session_id': self._session_id,
                'token': self.token,
            })

    def on_close(self, code, reason):
        self.log.warning('[%s] Voice websocket closed: [%s] %s (%s)', self,
                         code, reason, self._reconnects)

        if self._heartbeat_task:
            self._heartbeat_task.kill()

        # If we're not in a connected state, don't try to resume/reconnect
        if self.state != VoiceState.CONNECTED:
            return

        self.log.info('[%s] Attempting Websocket Resumption', self)
        self._reconnects += 1

        if self.max_reconnects and self._reconnects > self.max_reconnects:
            raise VoiceException(
                'Failed to reconnect after {} attempts, giving up'.format(
                    self.max_reconnects))

        self.set_state(VoiceState.RECONNECTING)

        # Don't resume for these error codes:
        if code and 4000 <= code <= 4016:
            self._session_id = None

            if self.udp and self.udp.connected:
                self.udp.disconnect()

        wait_time = (self._reconnects - 1) * 5
        self.log.info('[%s] Will attempt to %s after %s seconds', self,
                      'resume' if self._session_id else 'reconnect', wait_time)
        gevent.sleep(wait_time)

        self._connect_and_run()

    def connect(self, timeout=5, mute=False, deaf=False):
        self.log.debug('[%s] Attempting connection', self)
        self.set_state(VoiceState.AWAITING_ENDPOINT)

        self._update_listener = self.client.events.on(
            'VoiceServerUpdate', self.on_voice_server_update)

        self.client.gw.send(
            OPCode.VOICE_STATE_UPDATE, {
                'self_mute': mute,
                'self_deaf': deaf,
                'guild_id': int(self.channel.guild_id),
                'channel_id': int(self.channel.id),
            })

        if not self.state_emitter.once(VoiceState.CONNECTED, timeout=timeout):
            self.disconnect()
            raise VoiceException('Failed to connect to voice', self)

    def disconnect(self):
        self.log.debug('[%s] disconnect called', self)
        self.set_state(VoiceState.DISCONNECTED)

        if self._heartbeat_task:
            self._heartbeat_task.kill()
            self._heartbeat_task = None

        if self.ws and self.ws.sock and self.ws.sock.connected:
            self.ws.close()

        if self.udp and self.udp.connected:
            self.udp.disconnect()

        self.client.gw.send(
            OPCode.VOICE_STATE_UPDATE, {
                'self_mute': False,
                'self_deaf': False,
                'guild_id': int(self.channel.guild_id),
                'channel_id': None,
            })

    def send_frame(self, *args, **kwargs):
        self.udp.send_frame(*args, **kwargs)

    def increment_timestamp(self, *args, **kwargs):
        self.udp.increment_timestamp(*args, **kwargs)
Пример #3
0
class VoiceClient(LoggingClass):
    VOICE_GATEWAY_VERSION = 4

    SUPPORTED_MODES = {
        'xsalsa20_poly1305_lite',
        'xsalsa20_poly1305_suffix',
        'xsalsa20_poly1305',
    }

    def __init__(self,
                 client,
                 server_id,
                 is_dm=False,
                 encoder=None,
                 max_reconnects=5):
        super(VoiceClient, self).__init__()

        self.client = client
        self.server_id = server_id
        self.channel_id = None
        self.is_dm = is_dm
        self.encoder = encoder or JSONEncoder
        self.max_reconnects = max_reconnects
        self.video_enabled = False

        # Set the VoiceClient in the state's voice clients
        self.client.state.voice_clients[self.server_id] = self

        # Bind to some WS packets
        self.packets = Emitter()
        self.packets.on(VoiceOPCode.HELLO, self.on_voice_hello)
        self.packets.on(VoiceOPCode.READY, self.on_voice_ready)
        self.packets.on(VoiceOPCode.RESUMED, self.on_voice_resumed)
        self.packets.on(VoiceOPCode.SESSION_DESCRIPTION, self.on_voice_sdp)
        self.packets.on(VoiceOPCode.SPEAKING, self.on_voice_speaking)
        self.packets.on(VoiceOPCode.CLIENT_CONNECT,
                        self.on_voice_client_connect)
        self.packets.on(VoiceOPCode.CLIENT_DISCONNECT,
                        self.on_voice_client_disconnect)
        self.packets.on(VoiceOPCode.CODECS, self.on_voice_codecs)

        # State + state change emitter
        self.state = VoiceState.DISCONNECTED
        self.state_emitter = Emitter()

        # Connection metadata
        self.token = None
        self.endpoint = None
        self.ssrc = None
        self.ip = None
        self.port = None
        self.mode = None
        self.udp = None
        self.audio_codec = None
        self.video_codec = None
        self.transport_id = None

        # Websocket connection
        self.ws = None

        self._session_id = self.client.gw.session_id
        self._reconnects = 0
        self._heartbeat_task = None
        self._identified = False

        # SSRCs
        self.audio_ssrcs = {}

    def __repr__(self):
        return u'<VoiceClient {}>'.format(self.server_id)

    @cached_property
    def guild(self):
        return self.client.state.guilds.get(
            self.server_id) if not self.is_dm else None

    @cached_property
    def channel(self):
        return self.client.state.channels.get(self.channel_id)

    @property
    def user_id(self):
        return self.client.state.me.id

    @property
    def ssrc_audio(self):
        return self.ssrc

    @property
    def ssrc_video(self):
        return self.ssrc + 1

    @property
    def ssrc_rtx(self):
        return self.ssrc + 2

    @property
    def ssrc_rtcp(self):
        return self.ssrc + 3

    def set_state(self, state):
        self.log.debug('[%s] state %s -> %s', self, self.state, state)
        prev_state = self.state
        self.state = state
        self.state_emitter.emit(state, prev_state)

    def set_endpoint(self, endpoint):
        endpoint = endpoint.split(':', 1)[0]
        if self.endpoint == endpoint:
            return

        self.log.info(
            '[%s] Set endpoint from VOICE_SERVER_UPDATE (state = %s / endpoint = %s)',
            self, self.state, endpoint)

        self.endpoint = endpoint

        if self.ws and self.ws.sock and self.ws.sock.connected:
            self.ws.close()
            self.ws = None

        self._identified = False

    def set_token(self, token):
        if self.token == token:
            return
        self.token = token
        if not self._identified:
            self._connect_and_run()

    def _connect_and_run(self):
        self.ws = Websocket('wss://' + self.endpoint +
                            '/?v={}'.format(self.VOICE_GATEWAY_VERSION))
        self.ws.emitter.on('on_open', self.on_open)
        self.ws.emitter.on('on_error', self.on_error)
        self.ws.emitter.on('on_close', self.on_close)
        self.ws.emitter.on('on_message', self.on_message)
        self.ws.run_forever()

    def _heartbeat(self, interval):
        while True:
            self.send(VoiceOPCode.HEARTBEAT, time.time())
            gevent.sleep(interval / 1000)

    def set_speaking(self,
                     voice=False,
                     soundshare=False,
                     priority=False,
                     delay=0):
        value = SpeakingFlags.NONE.value
        if voice:
            value |= SpeakingFlags.VOICE.value
        if soundshare:
            value |= SpeakingFlags.SOUNDSHARE.value
        if priority:
            value |= SpeakingFlags.PRIORITY.value

        self.send(VoiceOPCode.SPEAKING, {
            'speaking': value,
            'delay': delay,
            'ssrc': self.ssrc,
        })

    def set_voice_state(self, channel_id, mute=False, deaf=False, video=False):
        self.client.gw.send(
            OPCode.VOICE_STATE_UPDATE, {
                'self_mute': bool(mute),
                'self_deaf': bool(deaf),
                'self_video': bool(video),
                'guild_id': None if self.is_dm else self.server_id,
                'channel_id': channel_id,
            })

    def send(self, op, data):
        if self.ws and self.ws.sock and self.ws.sock.connected:
            self.log.debug('[%s] sending OP %s (data = %s)', self, op, data)
            self.ws.send(self.encoder.encode({
                'op': op.value,
                'd': data,
            }), self.encoder.OPCODE)
        else:
            self.log.debug(
                '[%s] dropping because ws is closed OP %s (data = %s)', self,
                op, data)

    def on_voice_client_connect(self, data):
        user_id = int(data['user_id'])

        self.audio_ssrcs[data['audio_ssrc']] = user_id
        # ignore data['voice_ssrc'] for now

    def on_voice_client_disconnect(self, data):
        user_id = int(data['user_id'])

        for ssrc in self.audio_ssrcs.keys():
            if self.audio_ssrcs[ssrc] == user_id:
                del self.audio_ssrcs[ssrc]
                break

    def on_voice_codecs(self, data):
        self.audio_codec = data['audio_codec']
        self.video_codec = data['video_codec']
        self.transport_id = data['media_session_id']

        # Set the UDP's RTP Audio Header's Payload Type
        self.udp.set_audio_codec(data['audio_codec'])

    def on_voice_hello(self, data):
        self.log.info(
            '[%s] Received Voice HELLO payload, starting heartbeater', self)
        self._heartbeat_task = gevent.spawn(self._heartbeat,
                                            data['heartbeat_interval'])
        self.set_state(VoiceState.AUTHENTICATED)

    def on_voice_ready(self, data):
        self.log.info(
            '[%s] Received Voice READY payload, attempting to negotiate voice connection w/ remote',
            self)
        self.set_state(VoiceState.CONNECTING)
        self.ssrc = data['ssrc']
        self.ip = data['ip']
        self.port = data['port']
        self._identified = True

        for mode in self.SUPPORTED_MODES:
            if mode in data['modes']:
                self.mode = mode
                self.log.debug('[%s] Selected mode %s', self, mode)
                break
        else:
            raise Exception('Failed to find a supported voice mode')

        self.log.debug('[%s] Attempting IP discovery over UDP to %s:%s', self,
                       self.ip, self.port)
        self.udp = UDPVoiceClient(self)
        ip, port = self.udp.connect(self.ip, self.port)

        if not ip:
            self.log.error(
                'Failed to discover our IP, perhaps a NAT or firewall is f*****g us'
            )
            self.disconnect()
            return

        codecs = []

        # Sending discord our available codecs and rtp payload type for it
        for idx, codec in enumerate(AudioCodecs):
            codecs.append({
                'name': codec,
                'type': 'audio',
                'priority': (idx + 1) * 1000,
                'payload_type': RTPPayloadTypes.get(codec).value,
            })

        self.log.debug(
            '[%s] IP discovery completed (ip = %s, port = %s), sending SELECT_PROTOCOL',
            self, ip, port)
        self.send(
            VoiceOPCode.SELECT_PROTOCOL, {
                'protocol': 'udp',
                'data': {
                    'port': port,
                    'address': ip,
                    'mode': self.mode,
                },
                'codecs': codecs,
            })
        self.send(VoiceOPCode.CLIENT_CONNECT, {
            'audio_ssrc': self.ssrc,
            'video_ssrc': 0,
            'rtx_ssrc': 0,
        })

    def on_voice_resumed(self, data):
        self.log.info('[%s] Received resumed', self)
        self.set_state(VoiceState.CONNECTED)

    def on_voice_sdp(self, sdp):
        self.log.info(
            '[%s] Received session description, connection completed', self)

        self.mode = sdp['mode']
        self.audio_codec = sdp['audio_codec']
        self.video_codec = sdp['video_codec']
        self.transport_id = sdp['media_session_id']

        # Set the UDP's RTP Audio Header's Payload Type
        self.udp.set_audio_codec(sdp['audio_codec'])

        # Create a secret box for encryption/decryption
        self.udp.setup_encryption(bytes(bytearray(sdp['secret_key'])))

        self.set_state(VoiceState.CONNECTED)

    def on_voice_speaking(self, data):
        user_id = int(data['user_id'])

        self.audio_ssrcs[data['ssrc']] = user_id

        # Maybe rename speaking to voice in future
        payload = VoiceSpeaking(
            client=self,
            user_id=user_id,
            speaking=bool(data['speaking'] & SpeakingFlags.VOICE.value),
            soundshare=bool(data['speaking'] & SpeakingFlags.SOUNDSHARE.value),
            priority=bool(data['speaking'] & SpeakingFlags.PRIORITY.value),
        )

        self.client.gw.events.emit('VoiceSpeaking', payload)

    def on_message(self, msg):
        try:
            data = self.encoder.decode(msg)
            self.packets.emit(VoiceOPCode[data['op']], data['d'])
        except Exception:
            self.log.exception('Failed to parse voice gateway message: ')

    def on_error(self, err):
        self.log.error('[%s] Voice websocket error: %s', self, err)

    def on_open(self):
        if self._identified:
            self.send(
                VoiceOPCode.RESUME, {
                    'server_id': self.server_id,
                    'session_id': self._session_id,
                    'token': self.token,
                })
        else:
            self.send(
                VoiceOPCode.IDENTIFY, {
                    'server_id': self.server_id,
                    'user_id': self.user_id,
                    'session_id': self._session_id,
                    'token': self.token,
                    'video': self.video_enabled,
                })

    def on_close(self, code, reason):
        self.log.warning('[%s] Voice websocket closed: [%s] %s (%s)', self,
                         code, reason, self._reconnects)

        if self._heartbeat_task:
            self._heartbeat_task.kill()
            self._heartbeat_task = None

        self.ws = None

        # If we killed the connection, don't try resuming
        if self.state == VoiceState.DISCONNECTED:
            return

        self.log.info('[%s] Attempting Websocket Resumption', self)

        self.set_state(VoiceState.RECONNECTING)

        # Check if code is not None, was not from us
        if code is not None:
            self._reconnects += 1

            if self.max_reconnects and self._reconnects > self.max_reconnects:
                raise VoiceException(
                    'Failed to reconnect after {} attempts, giving up'.format(
                        self.max_reconnects), self)

            # Don't resume for these error codes:
            if 4000 <= code <= 4016:
                self._identified = False

                if self.udp and self.udp.connected:
                    self.udp.disconnect()

            wait_time = 5
        else:
            wait_time = 1

        self.log.info('[%s] Will attempt to %s after %s seconds', self,
                      'resume' if self._identified else 'reconnect', wait_time)
        gevent.sleep(wait_time)

        self._connect_and_run()

    def connect(self, channel_id, timeout=10, **kwargs):
        if self.is_dm:
            channel_id = self.server_id

        if not channel_id:
            raise VoiceException(
                '[{}] cannot connect to an empty channel id'.format(self))

        if self.channel_id == channel_id:
            if self.state == VoiceState.CONNECTED:
                self.log.debug('[%s] Already connected to %s, returning', self,
                               self.channel)
                return self
        else:
            if self.state == VoiceState.CONNECTED:
                self.log.debug('[%s] Moving to channel %s', self, channel_id)
            else:
                self.log.debug('[%s] Attempting connection to channel id %s',
                               self, channel_id)
                self.set_state(VoiceState.AWAITING_ENDPOINT)

        self.set_voice_state(channel_id, **kwargs)

        if not self.state_emitter.once(VoiceState.CONNECTED, timeout=timeout):
            self.disconnect()
            raise VoiceException('Failed to connect to voice', self)
        else:
            return self

    def disconnect(self):
        if self.state == VoiceState.DISCONNECTED:
            return

        self.log.debug('[%s] disconnect called', self)
        self.set_state(VoiceState.DISCONNECTED)

        del self.client.state.voice_clients[self.server_id]

        if self._heartbeat_task:
            self._heartbeat_task.kill()
            self._heartbeat_task = None

        if self.ws and self.ws.sock and self.ws.sock.connected:
            self.ws.close()
            self.ws = None

        if self.udp and self.udp.connected:
            self.udp.disconnect()

        if self.channel_id:
            self.set_voice_state(None)

        self.client.gw.events.emit('VoiceDisconnect', self)

    def send_frame(self, *args, **kwargs):
        self.udp.send_frame(*args, **kwargs)

    def increment_timestamp(self, *args, **kwargs):
        self.udp.increment_timestamp(*args, **kwargs)