diff --git a/discord/enums.py b/discord/enums.py index f7df90594..f11f1ed11 100644 --- a/discord/enums.py +++ b/discord/enums.py @@ -26,10 +26,10 @@ DEALINGS IN THE SOFTWARE. from enum import Enum, IntEnum -__all__ = ['ChannelType', 'MessageType', 'VoiceRegion', 'VerificationLevel', - 'ContentFilter', 'Status', 'DefaultAvatar', 'RelationshipType', - 'AuditLogAction', 'AuditLogActionCategory', 'UserFlags', - 'ActivityType', 'HypeSquadHouse', 'NotificationLevel'] +__all__ = ['ChannelType', 'MessageType', 'VoiceRegion', 'SpeakingState', + 'VerificationLevel', 'ContentFilter', 'Status', 'DefaultAvatar', + 'RelationshipType', 'AuditLogAction', 'AuditLogActionCategory', + 'UserFlags', 'ActivityType', 'HypeSquadHouse', 'NotificationLevel'] class ChannelType(Enum): text = 0 @@ -75,6 +75,15 @@ class VoiceRegion(Enum): def __str__(self): return self.value +class SpeakingState(IntEnum): + none = 0 + voice = 1 + soundshare = 2 + priority = 4 + + def __str__(self): + return self.name + class VerificationLevel(IntEnum): none = 0 low = 1 diff --git a/discord/gateway.py b/discord/gateway.py index eb17c2ef1..83699a449 100644 --- a/discord/gateway.py +++ b/discord/gateway.py @@ -38,6 +38,7 @@ import websockets from . import utils from .activity import _ActivityTag +from .enums import SpeakingState from .errors import ConnectionClosed, InvalidArgument log = logging.getLogger(__name__) @@ -547,6 +548,10 @@ class DiscordVoiceWebSocket(websockets.client.WebSocketClientProtocol): Receive only. Tells you that your websocket connection was acknowledged. INVALIDATE_SESSION Sent only. Tells you that your RESUME request has failed and to re-IDENTIFY. + CLIENT_CONNECT + Indicates a user has connected to voice. + CLIENT_DISCONNECT + Receive only. Indicates a user has disconnected from voice. """ IDENTIFY = 0 @@ -559,6 +564,8 @@ class DiscordVoiceWebSocket(websockets.client.WebSocketClientProtocol): RESUME = 7 HELLO = 8 INVALIDATE_SESSION = 9 + CLIENT_CONNECT = 12 + CLIENT_DISCONNECT = 13 def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -597,7 +604,7 @@ class DiscordVoiceWebSocket(websockets.client.WebSocketClientProtocol): @classmethod async def from_client(cls, client, *, resume=False): """Creates a voice websocket for the :class:`VoiceClient`.""" - gateway = 'wss://' + client.endpoint + '/?v=3' + gateway = 'wss://' + client.endpoint + '/?v=4' ws = await websockets.connect(gateway, loop=client.loop, klass=cls, compression=None) ws.gateway = gateway ws._connection = client @@ -610,7 +617,7 @@ class DiscordVoiceWebSocket(websockets.client.WebSocketClientProtocol): return ws - async def select_protocol(self, ip, port): + async def select_protocol(self, ip, port, mode): payload = { 'op': self.SELECT_PROTOCOL, 'd': { @@ -618,18 +625,28 @@ class DiscordVoiceWebSocket(websockets.client.WebSocketClientProtocol): 'data': { 'address': ip, 'port': port, - 'mode': 'xsalsa20_poly1305' + 'mode': mode } } } await self.send_as_json(payload) - async def speak(self, is_speaking=True): + async def client_connect(self): + payload = { + 'op': self.CLIENT_CONNECT, + 'd': { + 'audio_ssrc': self._connection.ssrc + } + } + + await self.send_as_json(payload) + + async def speak(self, state=SpeakingState.voice): payload = { 'op': self.SPEAKING, 'd': { - 'speaking': is_speaking, + 'speaking': int(state), 'delay': 0 } } @@ -642,9 +659,6 @@ class DiscordVoiceWebSocket(websockets.client.WebSocketClientProtocol): data = msg.get('d') if op == self.READY: - interval = data['heartbeat_interval'] / 1000.0 - self._keep_alive = VoiceKeepAliveHandler(ws=self, interval=interval) - self._keep_alive.start() await self.initial_connection(data) elif op == self.HEARTBEAT_ACK: self._keep_alive.ack() @@ -652,7 +666,12 @@ class DiscordVoiceWebSocket(websockets.client.WebSocketClientProtocol): log.info('Voice RESUME failed.') await self.identify() elif op == self.SESSION_DESCRIPTION: + self._connection.mode = data['mode'] await self.load_secret_key(data) + elif op == self.HELLO: + interval = data['heartbeat_interval'] / 1000.0 + self._keep_alive = VoiceKeepAliveHandler(ws=self, interval=interval) + self._keep_alive.start() async def initial_connection(self, data): state = self._connection @@ -673,15 +692,23 @@ class DiscordVoiceWebSocket(websockets.client.WebSocketClientProtocol): # the port is a little endian unsigned short in the last two bytes # yes, this is different endianness from everything else state.port = struct.unpack_from('H', header, 2, self.sequence) struct.pack_into('>I', header, 4, self.timestamp) struct.pack_into('>I', header, 8, self.ssrc) - # Copy header to nonce's first 12 bytes + encrypt_packet = getattr(self, '_encrypt_' + self.mode) + return encrypt_packet(header, data) + + def _encrypt_xsalsa20_poly1305(self, header, data): + box = nacl.secret.SecretBox(bytes(self.secret_key)) + nonce = bytearray(24) nonce[:12] = header - # Encrypt and return the data return header + box.encrypt(bytes(data), bytes(nonce)).ciphertext + def _encrypt_xsalsa20_poly1305_suffix(self, header, data): + box = nacl.secret.SecretBox(bytes(self.secret_key)) + nonce = nacl.utils.random(nacl.secret.SecretBox.NONCE_SIZE) + + return header + box.encrypt(bytes(data), nonce).ciphertext + nonce + def play(self, source, *, after=None): """Plays an :class:`AudioSource`.