Skip to content

Commit

Permalink
Updates from upstream API updates:
Browse files Browse the repository at this point in the history
- disco.api.http: map `GUILDS_ROLES_GET`, `GUILDS_VOICE_STATES_ME_GET`, `GUILDS_VOICE_STATES_GET`;
- disco.types.oauth: map `approximate_user_install_count` and `integration_types_config` on `Application` object;
- disco.voice.client: update `VOICE_GATEWAY_VERSION` to version 8, remove deprecated encryption modes, add support for new `seq` on voice gateway events, update structure of heartbeat and resume payloads, reset seq on fresh connections;
- disco.voice.udp: remove support for deprecated encryption modes;
Updated requirements.txt:
- requests (2.32.2 >> 2.32.3)
  • Loading branch information
elderlabs committed Aug 19, 2024
1 parent e6034ad commit 7146ffc
Show file tree
Hide file tree
Showing 7 changed files with 49 additions and 64 deletions.
3 changes: 3 additions & 0 deletions disco/api/http.py
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@ class Routes:
GUILDS_PREVIEW_GET = (HTTPMethod.GET, GUILDS + '/preview')
GUILDS_PRUNE_COUNT = (HTTPMethod.GET, GUILDS + '/prune')
GUILDS_PRUNE_CREATE = (HTTPMethod.POST, GUILDS + '/prune')
GUILDS_ROLES_GET = (HTTPMethod.GET, GUILDS + '/roles/{role}')
GUILDS_ROLES_BATCH_MODIFY = (HTTPMethod.PATCH, GUILDS + '/roles')
GUILDS_ROLES_CREATE = (HTTPMethod.POST, GUILDS + '/roles')
GUILDS_ROLES_DELETE = (HTTPMethod.DELETE, GUILDS + '/roles/{role}')
Expand All @@ -174,7 +175,9 @@ class Routes:
GUILDS_THREADS_LIST = (HTTPMethod.GET, GUILDS_THREADS + '/active')
GUILDS_VANITY_URL_GET = (HTTPMethod.GET, GUILDS + '/vanity-url')
GUILDS_VOICE_REGIONS_LIST = (HTTPMethod.GET, GUILDS + '/regions')
GUILDS_VOICE_STATES_ME_GET = (HTTPMethod.GET, GUILDS + '/voice-states/@me')
GUILDS_VOICE_STATES_ME_MODIFY = (HTTPMethod.PATCH, GUILDS + '/voice-states/@me')
GUILDS_VOICE_STATES_GET = (HTTPMethod.GET, GUILDS + '/voice-states/{member}')
GUILDS_VOICE_STATES_MODIFY = (HTTPMethod.PATCH, GUILDS + '/voice-states/{member}')
GUILDS_WEBHOOKS_LIST = (HTTPMethod.GET, GUILDS + '/webhooks')
GUILDS_WELCOME_SCREEN_GET = (HTTPMethod.GET, GUILDS + '/welcome-screen')
Expand Down
6 changes: 3 additions & 3 deletions disco/bot/bot.py
Original file line number Diff line number Diff line change
Expand Up @@ -195,13 +195,13 @@ def __init__(self, client, config=None):
# Stores a giant regex matcher for all commands
self.command_matches_re = None

# Finally, load all the plugin modules that where passed with the config
# Finally, load all the plugin modules that were passed with the config
for plugin_mod in self.config.plugins:
self.add_plugin_module(plugin_mod)

# Convert our configured mapping of entities to levels into something
# we can actually use. This ensures IDs are converted properly, and maps
# any level names (e.g. `role_id: admin`) map to their numerical values.
# we can actually use. This ensures IDs are converted properly, and maps
# any level names (e.g. `role_id: admin`) to their numerical values.
for entity_id, level in tuple(self.config.levels.items()):
del self.config.levels[entity_id]
entity_id = int(entity_id) if str(entity_id).isdigit() else entity_id
Expand Down
11 changes: 11 additions & 0 deletions disco/types/oauth.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,15 @@ class ApplicationFlagsValue(BitsetValue):
map = ApplicationFlags


class ApplicationIntegrationType:
GUILD_INSTALL = 0
USER_INSTALL = 1


class ApplicationIntegrationTypeConfiguration(SlottedModel):
oauth2_install_params = Field(ApplicationInstallParams)


class Application(SlottedModel):
id = Field(snowflake)
name = Field(text)
Expand All @@ -67,11 +76,13 @@ class Application(SlottedModel):
cover_image = Field(text)
flags = Field(ApplicationFlagsValue)
approximate_guild_count = Field(int)
approximate_user_install_count = Field(int)
redirect_uris = ListField(str)
interactions_endpoint_url = Field(str)
role_connections_verification_url = Field(str)
tags = ListField(str)
install_params = Field(ApplicationInstallParams)
integration_types_config = Field(dict) # TODO: this is a dumpster-fire
custom_install_url = Field(str)

def user_is_owner(self, user):
Expand Down
2 changes: 1 addition & 1 deletion disco/util/crypto.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
try:
from nacl.utils import EncryptedMessage
except ImportError:
pass
warnings_warn('nacl is not installed, voice support is disabled')

try:
from libnacl import crypto_aead_xchacha20poly1305_ietf_encrypt, crypto_aead_xchacha20poly1305_ietf_decrypt, crypto_aead_aes256gcm_encrypt, crypto_aead_aes256gcm_decrypt
Expand Down
16 changes: 8 additions & 8 deletions disco/voice/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,16 +68,11 @@ def __init__(self, msg, client):


class VoiceClient(LoggingClass):
VOICE_GATEWAY_VERSION = 7
VOICE_GATEWAY_VERSION = 8

SUPPORTED_MODES = {
'aead_aes256_gcm_rtpsize',
'aead_aes256_gcm',
'aead_xchacha20_poly1305_rtpsize',
'xsalsa20_poly1305_lite_rtpsize',
'xsalsa20_poly1305_lite',
'xsalsa20_poly1305_suffix',
'xsalsa20_poly1305',
}

def __init__(self, client, server_id, is_dm=False, max_reconnects=5, encoder='json', video_enabled=False):
Expand Down Expand Up @@ -131,6 +126,7 @@ def __init__(self, client, server_id, is_dm=False, max_reconnects=5, encoder='js
self.video_codec = None
self.transport_id = None
self.secure_frames_version = None
self.seq = -1

# Websocket connection
self.ws = None
Expand Down Expand Up @@ -235,12 +231,12 @@ def heartbeat_task(self, interval):
return
self._last_heartbeat = time_perf_counter()

self.send(VoiceOPCode.HEARTBEAT, time())
self.send(VoiceOPCode.HEARTBEAT, {'seq_ack': self.seq, 't': int(time())})
self._heartbeat_acknowledged = False
gevent_sleep(interval / 1000)

def handle_heartbeat(self, _):
self.send(VoiceOPCode.HEARTBEAT, int(time()))
self.send(VoiceOPCode.HEARTBEAT, {'seq_ack': self.seq, 't': int(time())})

def handle_heartbeat_acknowledge(self, _):
self.log.debug('[{}] Received WS HEARTBEAT_ACK'.format(self.channel_id))
Expand Down Expand Up @@ -500,6 +496,8 @@ def on_message(self, msg):
try:
data = self.encoder.decode(msg)
self.packets.emit(data['op'], data['d'])
if 'seq' in data:
self.seq = data['seq']
except Exception:
self.log.exception('Failed to parse voice gateway message: ')

Expand All @@ -515,8 +513,10 @@ def on_open(self):
'server_id': self.server_id,
'session_id': self._session_id,
'token': self.token,
'seq_ack': self.seq,
})
else:
self.seq = -1
self.send(VoiceOPCode.IDENTIFY, {
'server_id': self.server_id,
'user_id': self.user_id,
Expand Down
73 changes: 22 additions & 51 deletions disco/voice/udp.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,6 @@
from struct import pack_into as struct_pack_into, unpack_from as struct_unpack_from, unpack as struct_unpack
from socket import socket, gethostbyname as socket_gethostbyname, AF_INET as SOCKET_AF_INET, SOCK_DGRAM as SOCKET_SOCK_DGRAM
from gevent import spawn as gevent_spawn, Timeout as GeventTimeout
from warnings import warn as warnings_warn

try:
from nacl.secret import SecretBox
except ImportError:
warnings_warn('nacl is not installed, voice support is disabled')

from disco.util.crypto import AEScrypt
from disco.util.enum import Enum
Expand Down Expand Up @@ -138,45 +132,31 @@ def increment_timestamp(self, by):
self.timestamp = 0

def setup_encryption(self, encryption_key):
if 'xsalsa20' in self.vc.mode:
self._secret_box = SecretBox(encryption_key)
elif self.vc.mode in ('aead_xchacha20_poly1305_rtpsize', 'aead_aes256_gcm', 'aead_aes256_gcm_rtpsize'):
self._secret_box = AEScrypt(encryption_key, self.vc.mode)
self._secret_box = AEScrypt(encryption_key, self.vc.mode)

def send_frame(self, frame, sequence=None, timestamp=None, incr_timestamp=None):
# Pack the RTC header into our buffer (a list of numbers)
struct_pack_into('>H', self._rtp_audio_header, 2, sequence or self.sequence) # BE, unsigned short
struct_pack_into('>I', self._rtp_audio_header, 4, timestamp or self.timestamp) # BE, unsigned int
struct_pack_into('>i', self._rtp_audio_header, 8, self.vc.ssrc_audio) # BE, int

if self.vc.mode in ('aead_aes256_gcm', 'aead_aes256_gcm_rtpsize'):
if self.vc.mode == 'aead_aes256_gcm_rtpsize':
nonce = bytearray(12) # 96-bits
else:
nonce = bytearray(24) # 192-bits is 24 bytes

if self.vc.mode != 'xsalsa20_poly1305':
# Use an incrementing number as a nonce, only first 4 bytes of the nonce is padded on
self._nonce += 1
if self._nonce > MAX_UINT32:
self._nonce = 0
struct_pack_into('>I', nonce, 0, self._nonce) # BE, unsigned int
if self.vc.mode in ('xsalsa20_poly1305_lite', 'xsalsa20_poly1305_lite_rtpsize', 'aead_xchacha20_poly1305_rtpsize', 'aead_aes256_gcm', 'aead_aes256_gcm_rtpsize'):
nonce_padding = nonce[:4]
elif self.vc.mode == 'xsalsa20_poly1305_suffix':
nonce_padding = nonce
elif self.vc.mode == 'xsalsa20_poly1305':
# Nonce is the header
nonce[:12] = self._rtp_audio_header
nonce_padding = None
else:
raise Exception('Voice mode `{}` is not supported.'.format(self.vc.mode))
# Use an incrementing number as a nonce, only first 4 bytes of the nonce is padded on
self._nonce += 1
if self._nonce > MAX_UINT32:
self._nonce = 0
struct_pack_into('>I', nonce, 0, self._nonce) # BE, unsigned int
nonce_padding = nonce[:4]

# Encrypt the payload with the nonce
if self.vc.mode in ('aead_xchacha20_poly1305_rtpsize', 'aead_aes256_gcm', 'aead_aes256_gcm_rtpsize'):
payload = self._secret_box.encrypt(plaintext=frame, nonce=bytes(nonce), aad=bytes(self._rtp_audio_header))
else:
payload = self._secret_box.encrypt(plaintext=frame, nonce=bytes(nonce))
if self.vc.mode not in ('aead_xchacha20_poly1305_rtpsize', 'aead_aes256_gcm_rtpsize'):
raise Exception(f'Voice mode `{self.vc.mode}` is not supported.')

# Encrypt the payload with the nonce
payload = self._secret_box.encrypt(plaintext=frame, nonce=bytes(nonce), aad=bytes(self._rtp_audio_header))
payload = payload.ciphertext

# Pad the payload with the nonce, if applicable
Expand Down Expand Up @@ -263,35 +243,26 @@ def run(self):
self.log.debug('[{}] [VoiceData] Received unsupported payload type, {}'.format(self.vc.channel_id, rtp.payload_type))
continue

if self.vc.mode in ('aead_aes256_gcm', 'aead_aes256_gcm_rtpsize'):
if self.vc.mode == 'aead_aes256_gcm_rtpsize':
nonce = bytearray(12) # 96-bits
else:
nonce = bytearray(24) # 192-bits is 24 bytes

if self.vc.mode in ('xsalsa20_poly1305_lite', 'xsalsa20_poly1305_lite_rtpsize', 'aead_xchacha20_poly1305_rtpsize', 'aead_aes256_gcm', 'aead_aes256_gcm_rtpsize'):
nonce[:4] = data[-4:]
data = data[:-4]
elif self.vc.mode == 'xsalsa20_poly1305_suffix':
nonce[:24] = data[-24:]
data = data[:-24]
elif self.vc.mode == 'xsalsa20_poly1305':
nonce[:12] = data[:12]
else:
self.log.debug('[{}] [VoiceData] Unsupported Encryption Mode, {}'.format(self.vc.channel_id, self.vc.mode))
nonce[:4] = data[-4:]
data = data[:-4]

if self.vc.mode not in ('aead_xchacha20_poly1305_rtpsize', 'aead_aes256_gcm_rtpsize'):
self.log.debug(f'[{self.vc.channel_id}] [VoiceData] Unsupported Encryption Mode, {self.vc.mode}')
continue

header_size = 12
if '_rtpsize' in self.vc.mode:
header_size += (rtp.csrc_count * 4)
if rtp.extension:
header_size += 4
header_size += (rtp.csrc_count * 4)
if rtp.extension:
header_size += 4
ctxt = data[header_size:] # plus strip whatever additional bs is before the payload

try:
if self.vc.mode in ('aead_xchacha20_poly1305_rtpsize', 'aead_aes256_gcm', 'aead_aes256_gcm_rtpsize'):
data = self._secret_box.decrypt(ciphertext=bytes(ctxt), nonce=bytes(nonce), aad=bytes(data[:header_size]))
else:
data = self._secret_box.decrypt(ciphertext=bytes(ctxt), nonce=bytes(nonce))
data = self._secret_box.decrypt(ciphertext=bytes(ctxt), nonce=bytes(nonce), aad=bytes(data[:header_size]))
except Exception as e:
self.log.debug('[{}] [VoiceData] Failed to decode data from ssrc {}: {} - {}'.format(self.vc.channel_id, rtp.ssrc, e.__class__.__name__, e))
continue
Expand Down
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
gevent==24.2.1
requests==2.32.2
requests==2.32.3
websocket-client==1.8.0

0 comments on commit 7146ffc

Please sign in to comment.