From 0183c81a965a99fc3a8e63a111f89a446c52e90e Mon Sep 17 00:00:00 2001 From: Snazzah Date: Mon, 8 Sep 2025 16:18:44 -0400 Subject: [PATCH 1/9] Add DAVE encryption --- discord/gateway.py | 134 ++++++++++++++++++++++++++++++++++++---- discord/voice_client.py | 13 +++- discord/voice_state.py | 75 ++++++++++++++++++++++ pyproject.toml | 5 +- 4 files changed, 212 insertions(+), 15 deletions(-) diff --git a/discord/gateway.py b/discord/gateway.py index 4e1f78c68..f3485686a 100644 --- a/discord/gateway.py +++ b/discord/gateway.py @@ -44,6 +44,11 @@ from .activity import BaseActivity from .enums import SpeakingState from .errors import ConnectionClosed +try: + import davey # type: ignore +except ImportError: + pass + _log = logging.getLogger(__name__) __all__ = ( @@ -812,18 +817,30 @@ class DiscordVoiceWebSocket: _max_heartbeat_timeout: float # fmt: off - IDENTIFY = 0 - SELECT_PROTOCOL = 1 - READY = 2 - HEARTBEAT = 3 - SESSION_DESCRIPTION = 4 - SPEAKING = 5 - HEARTBEAT_ACK = 6 - RESUME = 7 - HELLO = 8 - RESUMED = 9 - CLIENT_CONNECT = 12 - CLIENT_DISCONNECT = 13 + IDENTIFY = 0 + SELECT_PROTOCOL = 1 + READY = 2 + HEARTBEAT = 3 + SESSION_DESCRIPTION = 4 + SPEAKING = 5 + HEARTBEAT_ACK = 6 + RESUME = 7 + HELLO = 8 + RESUMED = 9 + CLIENTS_CONNECT = 11 + CLIENT_CONNECT = 12 + CLIENT_DISCONNECT = 13 + DAVE_PREPARE_TRANSITION = 21 + DAVE_EXECUTE_TRANSITION = 22 + DAVE_TRANSITION_READY = 23 + DAVE_PREPARE_EPOCH = 24 + MLS_EXTERNAL_SENDER = 25 + MLS_KEY_PACKAGE = 26 + MLS_PROPOSALS = 27 + MLS_COMMIT_WELCOME = 28 + MLS_ANNOUNCE_COMMIT_TRANSITION = 29 + MLS_WELCOME = 30 + MLS_INVALID_COMMIT_WELCOME = 31 # fmt: on def __init__( @@ -850,6 +867,10 @@ class DiscordVoiceWebSocket: _log.debug('Sending voice websocket frame: %s.', data) await self.ws.send_str(utils._to_json(data)) + async def send_binary(self, opcode: int, data: bytes) -> None: + _log.debug('Sending voice websocket binary frame: opcode=%s size=%d', opcode, len(data)) + await self.ws.send_bytes(bytes([opcode]) + data) + send_heartbeat = send_as_json async def resume(self) -> None: @@ -874,6 +895,7 @@ class DiscordVoiceWebSocket: 'user_id': str(state.user.id), 'session_id': state.session_id, 'token': state.token, + 'max_dave_protocol_version': state.max_dave_protocol_version, }, } await self.send_as_json(payload) @@ -943,6 +965,16 @@ class DiscordVoiceWebSocket: await self.send_as_json(payload) + async def send_transition_ready(self, transition_id: int): + payload = { + 'op': DiscordVoiceWebSocket.DAVE_TRANSITION_READY, + 'd': { + 'transition_id': transition_id, + }, + } + + await self.send_as_json(payload) + async def received_message(self, msg: Dict[str, Any]) -> None: _log.debug('Voice websocket frame received: %s', msg) op = msg['op'] @@ -959,13 +991,89 @@ class DiscordVoiceWebSocket: elif op == self.SESSION_DESCRIPTION: self._connection.mode = data['mode'] await self.load_secret_key(data) + self._connection.dave_protocol_version = data['dave_protocol_version'] + await self._connection.reinit_dave_session() elif op == self.HELLO: interval = data['heartbeat_interval'] / 1000.0 self._keep_alive = VoiceKeepAliveHandler(ws=self, interval=min(interval, 5.0)) self._keep_alive.start() + elif self._connection.dave_session: + state = self._connection + if op == self.DAVE_PREPARE_TRANSITION: + _log.debug( + 'Preparing for DAVE transition id %d for protocol version %d', + data['transition_id'], + data['protocol_version'], + ) + state.dave_pending_transition = data + if data['transition_id'] == 0: + await state._execute_transition(data['transition_id']) + else: + if data['protocol_version'] == 0 and state.dave_session: + state.dave_session.set_passthrough_mode(True, 120) + + await self.send_transition_ready(data['transition_id']) + elif op == self.DAVE_EXECUTE_TRANSITION: + _log.debug('Executing DAVE transition id %d', data['transition_id']) + await state._execute_transition(data['transition_id']) + elif op == self.DAVE_PREPARE_EPOCH: + _log.debug('Preparing for DAVE epoch %d', data['epoch']) + # When the epoch ID is equal to 1, this message indicates that a new MLS group is to be created for the given protocol version. + if data['epoch'] == 1: + state.dave_protocol_version = data['protocol_version'] + await state.reinit_dave_session() await self._hook(self, msg) + async def recieved_binary_message(self, msg: bytes) -> None: + self.seq_ack = struct.unpack_from('>H', msg, 0)[0] + op = msg[2] + _log.debug('Voice websocket binary frame received: %d bytes; seq=%s op=%s', len(msg), self.seq_ack, op) + state = self._connection + + if state.dave_session: + if op == self.MLS_EXTERNAL_SENDER: + state.dave_session.set_external_sender(msg[3:]) + _log.debug('Set MLS external sender') + elif op == self.MLS_PROPOSALS: + optype = msg[3] + result = state.dave_session.process_proposals( + davey.ProposalsOperationType.append if optype == 0 else davey.ProposalsOperationType.revoke, msg[4:] + ) + if isinstance(result, davey.CommitWelcome): + await self.send_binary( + DiscordVoiceWebSocket.MLS_KEY_PACKAGE, + result.commit + result.welcome if result.welcome else result.commit, + ) + _log.debug('MLS proposals processed') + elif op == self.MLS_ANNOUNCE_COMMIT_TRANSITION: + transition_id = struct.unpack_from('>H', msg, 3)[0] + try: + state.dave_session.process_commit(msg[5:]) + if transition_id != 0: + state.dave_pending_transition = { + 'transition_id': transition_id, + 'protocol_version': state.dave_protocol_version, + } + await self.send_transition_ready(transition_id) + _log.debug('MLS commit processed for transition id %d', transition_id) + except Exception: + await state._recover_from_invalid_commit(transition_id) + elif op == self.MLS_WELCOME: + transition_id = struct.unpack_from('>H', msg, 3)[0] + try: + state.dave_session.process_welcome(msg[5:]) + if transition_id != 0: + state.dave_pending_transition = { + 'transition_id': transition_id, + 'protocol_version': state.dave_protocol_version, + } + await self.send_transition_ready(transition_id) + _log.debug('MLS welcome processed for transition id %d', transition_id) + except Exception: + await state._recover_from_invalid_commit(transition_id) + pass + async def initial_connection(self, data: Dict[str, Any]) -> None: state = self._connection state.ssrc = data['ssrc'] @@ -1045,6 +1153,8 @@ class DiscordVoiceWebSocket: msg = await asyncio.wait_for(self.ws.receive(), timeout=30.0) if msg.type is aiohttp.WSMsgType.TEXT: await self.received_message(utils._from_json(msg.data)) + elif msg.type is aiohttp.WSMsgType.BINARY: + await self.recieved_binary_message(msg.data) elif msg.type is aiohttp.WSMsgType.ERROR: _log.debug('Received voice %s', msg) raise ConnectionClosed(self.ws, shard_id=None) from msg.data diff --git a/discord/voice_client.py b/discord/voice_client.py index b0f3e951b..ae4ba31f3 100644 --- a/discord/voice_client.py +++ b/discord/voice_client.py @@ -284,6 +284,10 @@ class VoiceClient(VoiceProtocol): def timeout(self) -> float: return self._connection.timeout + @property + def voice_privacy_code(self) -> Optional[str]: + return self._connection.dave_session.voice_privacy_code if self._connection.dave_session else None + def checked_add(self, attr: str, value: int, limit: int) -> None: val = getattr(self, attr) if val + value > limit: @@ -368,7 +372,12 @@ class VoiceClient(VoiceProtocol): # audio related - def _get_voice_packet(self, data): + def _get_voice_packet(self, data: bytes): + packet = ( + self._connection.dave_session.encrypt_opus(data) + if self._connection.dave_session and self._connection.can_encrypt + else data + ) header = bytearray(12) # Formulate rtp header @@ -379,7 +388,7 @@ class VoiceClient(VoiceProtocol): struct.pack_into('>I', header, 8, self.ssrc) encrypt_packet = getattr(self, '_encrypt_' + self.mode) - return encrypt_packet(header, data) + return encrypt_packet(header, packet) def _encrypt_aead_xchacha20_poly1305_rtpsize(self, header: bytes, data) -> bytes: # Esentially the same as _lite diff --git a/discord/voice_state.py b/discord/voice_state.py index 5e78c7851..48c9601ea 100644 --- a/discord/voice_state.py +++ b/discord/voice_state.py @@ -69,6 +69,14 @@ if TYPE_CHECKING: WebsocketHook = Optional[Callable[[DiscordVoiceWebSocket, Dict[str, Any]], Coroutine[Any, Any, Any]]] SocketReaderCallback = Callable[[bytes], Any] +has_dave: bool + +try: + import davey # type: ignore + + has_dave = True +except ImportError: + has_dave = False __all__ = ('VoiceConnectionState',) @@ -208,6 +216,10 @@ class VoiceConnectionState: self.mode: SupportedModes = MISSING self.socket: socket.socket = MISSING self.ws: DiscordVoiceWebSocket = MISSING + self.dave_session: Optional[davey.DaveSession] = None + self.dave_protocol_version: int = 0 + self.dave_pending_transition: Optional[Dict[str, Any]] = None + self.dave_downgraded: bool = False self._state: ConnectionFlowState = ConnectionFlowState.disconnected self._expecting_disconnect: bool = False @@ -252,6 +264,69 @@ class VoiceConnectionState: def self_voice_state(self) -> Optional[VoiceState]: return self.guild.me.voice + @property + def max_dave_protocol_version(self) -> int: + return davey.DAVE_PROTOCOL_VERSION + + @property + def can_encrypt(self) -> bool: + return self.dave_protocol_version != 0 and self.dave_session != None and self.dave_session.ready + + async def reinit_dave_session(self) -> None: + if self.dave_protocol_version > 0: + if self.dave_session: + self.dave_session.reinit(self.dave_protocol_version, self.user.id, self.voice_client.channel.id) + else: + self.dave_session = davey.DaveSession(self.dave_protocol_version, self.user.id, self.voice_client.channel.id) + await self.voice_client.ws.send_binary( + DiscordVoiceWebSocket.MLS_KEY_PACKAGE, self.dave_session.get_serialized_key_package() + ) + elif self.dave_session: + self.dave_session.reset() + self.dave_session.set_passthrough_mode(True, 10) + pass + + async def _recover_from_invalid_commit(self, transition_id: int) -> None: + payload = { + 'op': DiscordVoiceWebSocket.MLS_INVALID_COMMIT_WELCOME, + 'd': { + 'transition_id': transition_id, + }, + } + + await self.voice_client.ws.send_as_json(payload) + await self.reinit_dave_session() + + async def _execute_transition(self, transition_id: int) -> None: + _log.debug('Executing transition id %d', transition_id) + if not self.dave_pending_transition: + _log.warning("Received execute transition, but we don't have a pending transition for id %d", transition_id) + return + + if transition_id == self.dave_pending_transition['transition_id']: + old_version = self.dave_protocol_version + self.dave_protocol_version = self.dave_pending_transition['protocol_version'] + + if old_version != self.dave_protocol_version and self.dave_protocol_version == 0: + self.dave_downgraded = True + _log.debug('DAVE Session downgraded') + elif transition_id > 0 and self.dave_downgraded: + self.dave_downgraded = False + if self.dave_session: + self.dave_session.set_passthrough_mode(True, 10) + _log.debug('DAVE Session upgraded') + + # In the future, the session should be signaled too, but for now theres just v1 + _log.debug('Transition id %d executed', transition_id) + else: + _log.debug( + 'Received execute transition for an unexpected transition id %d when the expected transition id is %d', + transition_id, + self.dave_pending_transition['transition_id'], + ) + + self.dave_pending_transition = None + async def voice_state_update(self, data: GuildVoiceStatePayload) -> None: channel_id = data['channel_id'] diff --git a/pyproject.toml b/pyproject.toml index 20d117b01..2bac4c648 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,7 +36,10 @@ Documentation = "https://discordpy.readthedocs.io/en/latest/" dependencies = { file = "requirements.txt" } [project.optional-dependencies] -voice = ["PyNaCl>=1.5.0,<1.6"] +voice = [ + "PyNaCl>=1.5.0,<1.6", + "davey" +] docs = [ "sphinx==4.4.0", "sphinxcontrib_trio==1.1.2", From ed128a95cb0650505b1831a852d4ae3877ff6683 Mon Sep 17 00:00:00 2001 From: Snazzah Date: Tue, 9 Sep 2025 16:00:06 -0400 Subject: [PATCH 2/9] Pin dependency --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 2bac4c648..96acddd85 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,7 +38,7 @@ dependencies = { file = "requirements.txt" } [project.optional-dependencies] voice = [ "PyNaCl>=1.5.0,<1.6", - "davey" + "davey==0.1.0" ] docs = [ "sphinx==4.4.0", From 0f91ea6cbb129df4436300c95c460a9cfb5c8714 Mon Sep 17 00:00:00 2001 From: Snazzah Date: Tue, 9 Sep 2025 16:01:08 -0400 Subject: [PATCH 3/9] Throw when trying to create a session without the dependency --- discord/gateway.py | 3 ++- discord/voice_state.py | 4 +++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/discord/gateway.py b/discord/gateway.py index f3485686a..aa05cd59f 100644 --- a/discord/gateway.py +++ b/discord/gateway.py @@ -992,7 +992,8 @@ class DiscordVoiceWebSocket: self._connection.mode = data['mode'] await self.load_secret_key(data) self._connection.dave_protocol_version = data['dave_protocol_version'] - await self._connection.reinit_dave_session() + if data['dave_protocol_version'] > 0: + await self._connection.reinit_dave_session() elif op == self.HELLO: interval = data['heartbeat_interval'] / 1000.0 self._keep_alive = VoiceKeepAliveHandler(ws=self, interval=min(interval, 5.0)) diff --git a/discord/voice_state.py b/discord/voice_state.py index 48c9601ea..5dced277f 100644 --- a/discord/voice_state.py +++ b/discord/voice_state.py @@ -266,7 +266,7 @@ class VoiceConnectionState: @property def max_dave_protocol_version(self) -> int: - return davey.DAVE_PROTOCOL_VERSION + return davey.DAVE_PROTOCOL_VERSION if has_dave else 0 @property def can_encrypt(self) -> bool: @@ -274,6 +274,8 @@ class VoiceConnectionState: async def reinit_dave_session(self) -> None: if self.dave_protocol_version > 0: + if not has_dave: + raise RuntimeError('davey library needed in order to use E2EE voice') if self.dave_session: self.dave_session.reinit(self.dave_protocol_version, self.user.id, self.voice_client.channel.id) else: From d5d26bb0b461b0a6dc89875a709b239e015e138f Mon Sep 17 00:00:00 2001 From: Snazzah Date: Tue, 9 Sep 2025 16:01:36 -0400 Subject: [PATCH 4/9] Add docstring for voice_privacy_code --- discord/voice_client.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/discord/voice_client.py b/discord/voice_client.py index ae4ba31f3..8c9b4217e 100644 --- a/discord/voice_client.py +++ b/discord/voice_client.py @@ -286,6 +286,10 @@ class VoiceClient(VoiceProtocol): @property def voice_privacy_code(self) -> Optional[str]: + """:class:`float`: The voice privacy code of the current voice session. + + This can be None if there is no active DAVE session happening. + """ return self._connection.dave_session.voice_privacy_code if self._connection.dave_session else None def checked_add(self, attr: str, value: int, limit: int) -> None: From 4ee2b6b2e0add00fd6077e8aef648c8dd9b34feb Mon Sep 17 00:00:00 2001 From: Snazzah Date: Tue, 9 Sep 2025 16:05:32 -0400 Subject: [PATCH 5/9] Fix voice_privacy_code docstring --- discord/voice_client.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/discord/voice_client.py b/discord/voice_client.py index 8c9b4217e..f26d10af3 100644 --- a/discord/voice_client.py +++ b/discord/voice_client.py @@ -286,8 +286,9 @@ class VoiceClient(VoiceProtocol): @property def voice_privacy_code(self) -> Optional[str]: - """:class:`float`: The voice privacy code of the current voice session. - + """:class:`str`: Get the voice privacy code of this E2EE session's group. + + A new privacy code is created and cached each time a new transition is executed. This can be None if there is no active DAVE session happening. """ return self._connection.dave_session.voice_privacy_code if self._connection.dave_session else None From 960f936ef528f0c1581e5b99925bb423aa8cb8d4 Mon Sep 17 00:00:00 2001 From: Snazzah Date: Wed, 10 Sep 2025 13:25:34 -0400 Subject: [PATCH 6/9] Allow for multiple transitions to be handled --- discord/gateway.py | 12 +++--------- discord/voice_state.py | 39 +++++++++++++++------------------------ 2 files changed, 18 insertions(+), 33 deletions(-) diff --git a/discord/gateway.py b/discord/gateway.py index aa05cd59f..aadb516fb 100644 --- a/discord/gateway.py +++ b/discord/gateway.py @@ -1006,7 +1006,7 @@ class DiscordVoiceWebSocket: data['transition_id'], data['protocol_version'], ) - state.dave_pending_transition = data + state.dave_pending_transitions[data['transition_id']] = data['protocol_version'] if data['transition_id'] == 0: await state._execute_transition(data['transition_id']) else: @@ -1052,10 +1052,7 @@ class DiscordVoiceWebSocket: try: state.dave_session.process_commit(msg[5:]) if transition_id != 0: - state.dave_pending_transition = { - 'transition_id': transition_id, - 'protocol_version': state.dave_protocol_version, - } + state.dave_pending_transitions[transition_id] = state.dave_protocol_version await self.send_transition_ready(transition_id) _log.debug('MLS commit processed for transition id %d', transition_id) except Exception: @@ -1065,10 +1062,7 @@ class DiscordVoiceWebSocket: try: state.dave_session.process_welcome(msg[5:]) if transition_id != 0: - state.dave_pending_transition = { - 'transition_id': transition_id, - 'protocol_version': state.dave_protocol_version, - } + state.dave_pending_transitions[transition_id] = state.dave_protocol_version await self.send_transition_ready(transition_id) _log.debug('MLS welcome processed for transition id %d', transition_id) except Exception: diff --git a/discord/voice_state.py b/discord/voice_state.py index 5dced277f..cec745c86 100644 --- a/discord/voice_state.py +++ b/discord/voice_state.py @@ -218,7 +218,7 @@ class VoiceConnectionState: self.ws: DiscordVoiceWebSocket = MISSING self.dave_session: Optional[davey.DaveSession] = None self.dave_protocol_version: int = 0 - self.dave_pending_transition: Optional[Dict[str, Any]] = None + self.dave_pending_transitions: Dict[int, int] = {} self.dave_downgraded: bool = False self._state: ConnectionFlowState = ConnectionFlowState.disconnected @@ -301,33 +301,24 @@ class VoiceConnectionState: async def _execute_transition(self, transition_id: int) -> None: _log.debug('Executing transition id %d', transition_id) - if not self.dave_pending_transition: + if transition_id not in self.dave_pending_transitions: _log.warning("Received execute transition, but we don't have a pending transition for id %d", transition_id) return - if transition_id == self.dave_pending_transition['transition_id']: - old_version = self.dave_protocol_version - self.dave_protocol_version = self.dave_pending_transition['protocol_version'] - - if old_version != self.dave_protocol_version and self.dave_protocol_version == 0: - self.dave_downgraded = True - _log.debug('DAVE Session downgraded') - elif transition_id > 0 and self.dave_downgraded: - self.dave_downgraded = False - if self.dave_session: - self.dave_session.set_passthrough_mode(True, 10) - _log.debug('DAVE Session upgraded') - - # In the future, the session should be signaled too, but for now theres just v1 - _log.debug('Transition id %d executed', transition_id) - else: - _log.debug( - 'Received execute transition for an unexpected transition id %d when the expected transition id is %d', - transition_id, - self.dave_pending_transition['transition_id'], - ) + old_version = self.dave_protocol_version + self.dave_protocol_version = self.dave_pending_transitions.pop(transition_id) + + if old_version != self.dave_protocol_version and self.dave_protocol_version == 0: + self.dave_downgraded = True + _log.debug('DAVE Session downgraded') + elif transition_id > 0 and self.dave_downgraded: + self.dave_downgraded = False + if self.dave_session: + self.dave_session.set_passthrough_mode(True, 10) + _log.debug('DAVE Session upgraded') - self.dave_pending_transition = None + # In the future, the session should be signaled too, but for now theres just v1 + _log.debug('Transition id %d executed', transition_id) async def voice_state_update(self, data: GuildVoiceStatePayload) -> None: channel_id = data['channel_id'] From 1e6d85616ac7f273857b6aef56a507fc50c70148 Mon Sep 17 00:00:00 2001 From: Snazzah Date: Thu, 11 Sep 2025 08:55:19 -0400 Subject: [PATCH 7/9] Fix sending the proper proposal result code --- discord/gateway.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/gateway.py b/discord/gateway.py index aadb516fb..ccb58f0a6 100644 --- a/discord/gateway.py +++ b/discord/gateway.py @@ -1043,7 +1043,7 @@ class DiscordVoiceWebSocket: ) if isinstance(result, davey.CommitWelcome): await self.send_binary( - DiscordVoiceWebSocket.MLS_KEY_PACKAGE, + DiscordVoiceWebSocket.MLS_COMMIT_WELCOME, result.commit + result.welcome if result.welcome else result.commit, ) _log.debug('MLS proposals processed') From b81710bc068c4c5081fe34f814deaed176c87db2 Mon Sep 17 00:00:00 2001 From: Snazzah Date: Sun, 28 Sep 2025 13:45:39 -0400 Subject: [PATCH 8/9] Try and fix linting issues --- discord/voice_client.py | 2 +- discord/voice_state.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/discord/voice_client.py b/discord/voice_client.py index f26d10af3..aeb549b1b 100644 --- a/discord/voice_client.py +++ b/discord/voice_client.py @@ -287,7 +287,7 @@ class VoiceClient(VoiceProtocol): @property def voice_privacy_code(self) -> Optional[str]: """:class:`str`: Get the voice privacy code of this E2EE session's group. - + A new privacy code is created and cached each time a new transition is executed. This can be None if there is no active DAVE session happening. """ diff --git a/discord/voice_state.py b/discord/voice_state.py index cec745c86..b033e57c3 100644 --- a/discord/voice_state.py +++ b/discord/voice_state.py @@ -276,7 +276,7 @@ class VoiceConnectionState: if self.dave_protocol_version > 0: if not has_dave: raise RuntimeError('davey library needed in order to use E2EE voice') - if self.dave_session: + if self.dave_session is not None: self.dave_session.reinit(self.dave_protocol_version, self.user.id, self.voice_client.channel.id) else: self.dave_session = davey.DaveSession(self.dave_protocol_version, self.user.id, self.voice_client.channel.id) From 5f257fd016d041e7d1cfb541009c4591f4f1fa85 Mon Sep 17 00:00:00 2001 From: Snazzah Date: Sun, 28 Sep 2025 13:56:31 -0400 Subject: [PATCH 9/9] Fix reportOptionalMemberAccess error --- discord/voice_state.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/discord/voice_state.py b/discord/voice_state.py index b033e57c3..04cc11b61 100644 --- a/discord/voice_state.py +++ b/discord/voice_state.py @@ -280,9 +280,11 @@ class VoiceConnectionState: self.dave_session.reinit(self.dave_protocol_version, self.user.id, self.voice_client.channel.id) else: self.dave_session = davey.DaveSession(self.dave_protocol_version, self.user.id, self.voice_client.channel.id) - await self.voice_client.ws.send_binary( - DiscordVoiceWebSocket.MLS_KEY_PACKAGE, self.dave_session.get_serialized_key_package() - ) + + if self.dave_session is not None: + await self.voice_client.ws.send_binary( + DiscordVoiceWebSocket.MLS_KEY_PACKAGE, self.dave_session.get_serialized_key_package() + ) elif self.dave_session: self.dave_session.reset() self.dave_session.set_passthrough_mode(True, 10)