aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--discord/enums.py17
-rw-r--r--discord/gateway.py49
-rw-r--r--discord/player.py21
-rw-r--r--discord/voice_client.py23
4 files changed, 86 insertions, 24 deletions
diff --git a/discord/enums.py b/discord/enums.py
index f7df9059..f11f1ed1 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 eb17c2ef..83699a44 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', recv, len(recv) - 2)[0]
-
log.debug('detected ip: %s port: %s', state.ip, state.port)
- await self.select_protocol(state.ip, state.port)
- log.info('selected the voice protocol for use')
+
+ # there *should* always be at least one supported mode (xsalsa20_poly1305)
+ modes = [mode for mode in data['modes'] if mode in self._connection.supported_modes]
+ log.debug('received supported encryption modes: %s', ", ".join(modes))
+
+ mode = modes[0]
+ await self.select_protocol(state.ip, state.port, mode)
+ log.info('selected the voice protocol for use (%s)', mode)
+
+ await self.client_connect()
async def load_secret_key(self, data):
log.info('received secret key for voice connection')
self._connection.secret_key = data.get('secret_key')
await self.speak()
+ await self.speak(False)
async def poll_event(self):
try:
diff --git a/discord/player.py b/discord/player.py
index 74cd073f..8ee3bd13 100644
--- a/discord/player.py
+++ b/discord/player.py
@@ -27,6 +27,7 @@ DEALINGS IN THE SOFTWARE.
import threading
import subprocess
import audioop
+import asyncio
import logging
import shlex
import time
@@ -261,6 +262,7 @@ class AudioPlayer(threading.Thread):
# getattr lookup speed ups
play_audio = self.client.send_audio_packet
+ self._speak(True)
while not self._end.is_set():
# are we paused?
@@ -309,14 +311,19 @@ class AudioPlayer(threading.Thread):
def stop(self):
self._end.set()
self._resumed.set()
+ self._speak(False)
- def pause(self):
+ def pause(self, *, update_speaking=True):
self._resumed.clear()
+ if update_speaking:
+ self._speak(False)
- def resume(self):
+ def resume(self, *, update_speaking=True):
self.loops = 0
self._start = time.time()
self._resumed.set()
+ if update_speaking:
+ self._speak(True)
def is_playing(self):
return self._resumed.is_set() and not self._end.is_set()
@@ -326,6 +333,12 @@ class AudioPlayer(threading.Thread):
def _set_source(self, source):
with self._lock:
- self.pause()
+ self.pause(update_speaking=False)
self.source = source
- self.resume()
+ self.resume(update_speaking=False)
+
+ def _speak(self, speaking):
+ try:
+ asyncio.run_coroutine_threadsafe(self.client.ws.speak(speaking), self.client.loop)
+ except Exception as e:
+ log.info("Speaking call in player failed: %s", e)
diff --git a/discord/voice_client.py b/discord/voice_client.py
index 91480872..6bd293c7 100644
--- a/discord/voice_client.py
+++ b/discord/voice_client.py
@@ -102,6 +102,7 @@ class VoiceClient:
self._connected = threading.Event()
self._handshake_complete = asyncio.Event(loop=self.loop)
+ self.mode = None
self._connections = 0
self.sequence = 0
self.timestamp = 0
@@ -110,6 +111,10 @@ class VoiceClient:
self.encoder = opus.Encoder()
warn_nacl = not has_nacl
+ supported_modes = (
+ 'xsalsa20_poly1305_suffix',
+ 'xsalsa20_poly1305',
+ )
@property
def guild(self):
@@ -288,22 +293,30 @@ class VoiceClient:
def _get_voice_packet(self, data):
header = bytearray(12)
- nonce = bytearray(24)
- box = nacl.secret.SecretBox(bytes(self.secret_key))
- # Formulate header
+ # Formulate rtp header
header[0] = 0x80
header[1] = 0x78
struct.pack_into('>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`.