Discord Bot mit Aufnahmefunktion ab Python: (5) Bedienen Sie die Discord API direkt

Einführung

Dieser Artikel ist eine Fortsetzung des vorherigen Discord Bot mit Aufnahmefunktion beginnend mit Python: (4) Abspielen von Musikdateien.

Versuchen Sie in diesem Artikel als vorbereitenden Schritt zum Starten der Implementierung der Sprachaufzeichnung, die Sprachkommunikations-API mithilfe der Discord-API zu verwenden, um zu verstehen, wie die Kommunikation ausgeführt wird.

Wir planen insgesamt 7 Artikel zu schreiben und haben bis zu 5 Artikel fertig geschrieben.

  1. Discord Bot mit Aufnahmefunktion beginnend mit Python: (1) Erste Schritte discord.py
  2. Discord Bot mit Aufnahmefunktion beginnend mit Python: (2) Praktische Funktion (Bot-Erweiterung, Cog, Embed)
  3. Discord Bot mit Aufnahmefunktion beginnend mit Python: (3) Zusammenarbeit mit der Datenbank
  4. Discord Bot mit Aufnahmefunktion beginnend mit Python: (4) Musikdateien abspielen
  5. Discord Bot mit Aufnahmefunktion ab Python: (5) Discord API direkt bedienen

Wie Sie sehen können, indem Sie "Anfänger" aus dem Tag entfernen, ist der folgende Vorgang etwas mühsam und betrifft hauptsächlich den Teil der unteren Ebene.

Kommunikationsfluss

Ein schematisches Diagramm der Kommunikation beim Senden und Empfangen von Sprache mit Discord ist unten dargestellt.

main.png

Da es sich um ein schematisches Diagramm handelt, wird eine ausführliche Erklärung weggelassen, aber ich denke, es wäre gut, wenn Sie verstehen könnten, dass die Verbindung über verschiedene Prozesse hergestellt wird.

Dieser Ablauf wird ohne Verwendung von discord.py implementiert, und das Senden und Empfangen von Discord-Audio wird detailliert behandelt.

Alle nachfolgenden Informationen sind offizielle Referenzen (Voice Connection Gateway, Normal Gateway. Es wird basierend auf docs / topic / gateway # gateways)) beschrieben.

Verbindung zum Gateway

Discord Gateway verfügt über ein Gateway, das Informationen über normale Gateway-Sprache sendet und empfängt. Um die Endpunkt-URL für die Verbindung mit dem Voice-Gateway zu erhalten, authentifizieren Sie sich zuerst beim normalen Gateway und stellen Sie dann eine Verbindung zum Voice-Gateway her. Informationen werden gesendet.

Erstellen Sie zunächst ein Skript, um die WebSocket-Verbindung in Python zu testen.

op10 Hello

import json
import asyncio
import aiohttp
from pprint import pprint


class Gateway:
    def __init__(self, loop=None):
        if loop is None:
            loop = asyncio.get_event_loop()
        self.endpoint = 'wss://gateway.discord.gg/?v=6&encoding=json'
        loop.create_task(self.receive_data())

    async def receive_data(self):
        async with aiohttp.ClientSession() as session:
            socket = await session.ws_connect(self.endpoint)
            while True:
                packet = await socket.receive()
                if packet.type in (aiohttp.WSMsgType.CLOSED,
                                   aiohttp.WSMsgType.CLOSING,
                                   aiohttp.WSMsgType.CLOSE,
                                   aiohttp.WSMsgType.ERROR):
                    print(packet)
                    print('==Verbindungsende==')
                    break
                pprint(json.loads(packet.data))

if __name__ == "__main__":
    loop = asyncio.get_event_loop()
    ws = Gateway(loop)
    loop.run_forever()

Wenn dies ausgeführt wird, wird die Co-Routine "receive_data" zum Herstellen einer Verbindung mit dem Gateway und zum Anzeigen der vom Gateway empfangenen Daten nacheinander von der Funktion "create_task" im Gateway aufgerufen und beginnt mit der Verarbeitung. Wenn dies ausgeführt wird, werden die folgenden Daten vom Discord Gateway gesendet.

{'d': {'_trace': ['["gateway-prd-main-xwmj",{"micros":0.0}]'],
       'heartbeat_interval': 41250},
 'op': 10,
 's': None,
 't': None}

Die von Discord gesendeten Daten verwenden "d" und "op" und selten "t", um die Daten darzustellen. op speichert den Datentyp und d speichert den Dateninformationskörper. t ist im Grunde None, aber wenn Sie detailliertere Informationen übermitteln müssen, ist es eine Zeichenfolge, die die Details dieser Informationen übermittelt.

op1 Heartbeat

Hier ist "op = 10". Dies ist eine Antwort namens "Hallo". Dies sind die Daten, die gesendet werden, wenn Sie zum ersten Mal eine Verbindung herstellen, wie der Name schon sagt. Die wichtigen Daten in Hello sind heartbeat_interval. Hier ist es "41250", das einfache Daten mit dem Namen "Heartbeat" senden muss, um Ihnen mitzuteilen, dass das Gateway noch jede angegebene Millisekunde (41,25 Sekunden) verbunden ist. Es gibt. Erstellen Sie eine Klasse, die "threading.Thread" als Hilfsklasse erbt, die diese Heartbeat-Verarbeitung ausführt. Durch Schreiben des gewünschten Prozesses in die Funktion "Ausführen" und Aufrufen der Startfunktion von der Instanz aus wird der Prozess in einem anderen Thread ausgeführt.

import json
import asyncio
import aiohttp
import threading
from pprint import pprint


class HeartbeatHandler(threading.Thread):
    def __init__(self, ws, interval):
        self.ws = ws
        self.interval = interval
        self.stop_ev = threading.Event()
        super().__init__()

    def run(self):
        self.send()
        while not self.stop_ev.wait(self.interval):
            self.send()

    def send(self):
        data = self.get_payload()
        asyncio.run_coroutine_threadsafe(
            self.ws.socket.send_json(data),
            self.ws.loop
        )
        print('==Senden==')
        print(data)

    def stop(self):
        self.stop_ev.set()

    def get_payload(self):
        raise NotImplementedError


class GatewayHeartbeat(HeartbeatHandler):
    def __init__(self, ws, interval):
        super().__init__(ws, interval)

    def get_payload(self):
        return {'op': 1, 'd': None}


class Gateway:
    def __init__(self, loop=None):
        if loop is None:
            self.loop = asyncio.get_event_loop()
        else:
            self.loop = loop
        self.endpoint = 'wss://gateway.discord.gg/?v=6&encoding=json'
        self.loop.create_task(self.receive_data())

    async def receive_data(self):
        async with aiohttp.ClientSession() as session:
            self.socket = await session.ws_connect(self.endpoint)
            while True:
                packet = await self.socket.receive()
                if packet.type in (aiohttp.WSMsgType.CLOSED,
                                   aiohttp.WSMsgType.CLOSING,
                                   aiohttp.WSMsgType.CLOSE,
                                   aiohttp.WSMsgType.ERROR):
                    print(packet)
                    print('==Verbindungsende==')
                    break
                print('==Erhalten==')
                pprint(json.loads(packet.data))
                await self.handle_message(json.loads(packet.data))
            if hasattr(self, 'heartbeat'):
                self.heartbeat.stop()

    async def handle_message(self, msg):
        op = msg.get('op')
        d = msg.get('d')
        t = msg.get('t')
        if op == 10:
            self.heartbeat = GatewayHeartbeat(
                self, d['heartbeat_interval'] / 1000
            )
            self.heartbeat.start()
            return


if __name__ == "__main__":
    loop = asyncio.get_event_loop()
    ws = Gateway(loop)
    loop.run_forever()

Es heißt run_coroutine_threadsafe ** Wie auch immer! !! Da es eine ** Funktion gibt, verwenden Sie sie. Wenn dies ausgeführt wird, wird der Kommunikationszustand untereinander alle 40 Sekunden ausgegeben.

==Erhalten==
{'d': {'_trace': ['["gateway-prd-main-w7j9",{"micros":0.0}]'],
       'heartbeat_interval': 41250},
 'op': 10,
 's': None,
 't': None}
==Senden==
{'op': 1, 'd': None}
==Erhalten==
{'d': None, 'op': 11, 's': None, 't': None}
==Senden==
{'op': 1, 'd': None}
==Erhalten==
{'d': None, 'op': 11, 's': None, 't': None}
...

Andernfalls wird die Verbindung nach Ablauf von 40 Sekunden vom Discord Gateway getrennt. Wenn Sie jedoch nur Heartbeat ausführen, erhalten Sie möglicherweise eine Anfrage zur erneuten Verbindung von der Discord-Seite. Derzeit werden wir hier keine Verarbeitung wie die erneute Verbindung durchführen.

op2 Identify

Als Nächstes müssen Sie ein Bot-Token senden, um das Gateway über die Verbindungsinformationen zu informieren. Diese Informationen werden von op2 gesendet, aber zusätzlich zum Bot-Token werden einfache Eigenschaften zu den Eigenschaften in der Nutzlast hinzugefügt. Wenn Sie einen großen Bot betreiben und "Sharding" ausführen, ist zusätzliche Verarbeitung erforderlich. In diesem Fall wird die Verarbeitung jedoch unter der Annahme durchgeführt, dass ein kleiner Bot kein Sharding verwendet.

class Gateway:
    def __init__(self, loop=None):
        if loop is None:
            self.loop = asyncio.get_event_loop()
        else:
            self.loop = loop
        self.endpoint = 'wss://gateway.discord.gg/?v=6&encoding=json'
        self.loop.create_task(self.receive_data())
        self.identified = asyncio.Event()

    async def receive_data(self):
        async with aiohttp.ClientSession() as session:
            self.socket = await session.ws_connect(self.endpoint)
            while True:
                packet = await self.socket.receive()
                if packet.type in (aiohttp.WSMsgType.CLOSED,
                                   aiohttp.WSMsgType.CLOSING,
                                   aiohttp.WSMsgType.CLOSE,
                                   aiohttp.WSMsgType.ERROR):
                    print('==Verbindungsende==')
                    print(packet)
                    break
                print('==Erhalten==')
                pprint(json.loads(packet.data))
                await self.handle_message(json.loads(packet.data))
            if hasattr(self, 'heartbeat'):
                self.heartbeat.stop()

    async def identify(self):
        payload = {
            'op': 2,
            'd': {
                'token': 'BOT_TOKEN',
                'properties': {
                    '$os': 'linux',
                    '$browser': 'python',
                    '$device': 'python',
                },
                'v': 3
            }
        }
        print('==Senden==')
        print(payload)
        await self.socket.send_json(payload)
        self.identified.set()

    async def handle_message(self, msg):
        op = msg.get('op')
        d = msg.get('d')
        t = msg.get('t')
        if op == 10:
            self.heartbeat = GatewayHeartbeat(
                self, d['heartbeat_interval'] / 1000
            )
            self.heartbeat.start()
            await self.identify()
            return

Die Authentifizierung erfolgt durch Senden des Bot-Tokens. Die Bot-Informationen und die Informationen des Servers, auf dem der Bot installiert ist, werden empfangen. Es ist leicht zu übersehen, da verschiedene Informationen gesendet werden. Wenn jedoch die Informationen "t = READY" mit "op = 0" gesendet werden, bedeutet dies, dass Sie bereit sind, über Gateway miteinander zu kommunizieren. Außerdem wird die "session_id" im "d" für die Sprachverbindung verwendet. Speichern Sie sie daher.

{'d': {
       ...
       'session_id': 'f0d7bba081bc0df51e43c1eef8092adcb',
       ... 
      },
 'op': 0,
 's': 1,
 't': 'READY'}

Stellen Sie eine Verbindung zum Voice Gateway her

op4 Gateway Voice State Update

Um die Informationen für die Verbindung mit dem Voice-Gateway zu erhalten, muss die Verbindung mit "op = 4" an das normale Gateway gesendet werden.

Mit op = 4 werden die ID des Servers und des Audiokanals sowie der eigene Stummschaltungsstatus angegeben und an das Gateway gesendet. Dadurch erhalten Sie die Endpunkt-URL des vom Server verwendeten Voice-Gateways.

class Gateway:
    ...

    async def voice_state_update(self):
        payload = {
            'op': 4,
            'd': {
                'guild_id': '705...',
                'channel_id': '706...',
                "self_mute": False,  #Ob stumm geschaltet werden soll
                "self_deaf": False,  #Gibt an, ob der Lautsprecher stummgeschaltet werden soll
            }
        }
        print('==Senden==')
        print(payload)
        await self.socket.send_json(payload)

    async def handle_message(self, msg):
        op = msg.get('op')
        d = msg.get('d')
        t = msg.get('t')
        if op == 10:
            self.heartbeat = GatewayHeartbeat(
                self, d['heartbeat_interval'] / 1000
            )
            self.heartbeat.start()
            await self.identify()
            return
        if op == 0:
            if t == 'READY':
                self.session_id = d['session_id']
                await self.voice_state_update()

Wenn dies ausgeführt wird, wird der Bot mit dem Audiokanal verbunden und die folgenden zwei Daten werden empfangen.

==Erhalten==
{'d': {'channel_id': '705...',
       'deaf': False,
       'guild_id': '706...',
       'member': ...,
       'mute': False,
       'self_deaf': False,
       'self_mute': False,
       'self_video': False,
       'session_id': 'f0d7bba081bc0df51e43c1eef8092adcb',
       'suppress': False,
       'user_id': '743...'},
 'op': 0,
 's': 3,
 't': 'VOICE_STATE_UPDATE'}
==Erhalten==
{'d': {'endpoint': 'japan396.discord.media:80',
       'guild_id': '705...',
       'token': '0123456789abcdef'},
 'op': 0,
 's': 4,
 't': 'VOICE_SERVER_UPDATE'}

Der "Endpunkt" von "VOICE_SERVER_UPDATE" unten ist der Endpunkt des Sprachgateways, und "Token" wird als Authentifizierungstoken verwendet.

op3 Heartbeat

Ab hier beginnt die Kommunikation mit dem Voice Gateway.

Erstellen Sie eine neue Klasse für die WebSocket-Kommunikation mit dem zuvor erhaltenen Endpunkt.

class Gateway:
    ...

    async def handle_message(self, msg):
        op = msg.get('op')
        d = msg.get('d')
        t = msg.get('t')
        if op == 10:
            self.heartbeat = GatewayHeartbeat(
                self, d['heartbeat_interval'] / 1000
            )
            self.heartbeat.start()
            await self.identify()
            return
        if op == 0:
            if t == 'READY':
                self.session_id = d['session_id']
                await self.voice_state_update()
            if t == 'VOICE_SERVER_UPDATE':
                self.voice_endpoint = d['endpoint']
                self.token = d['token']
                self.voice_gw = VoiceGateway(self, self.loop)


class VoiceGateway:
    def __init__(self, gateway, loop=None):
        self.gateway = gateway
        if loop is None:
            self.loop = asyncio.get_event_loop()
        else:
            self.loop = loop
        self.endpoint = f'wss://{gateway.voice_endpoint.replace(":80", "")}/?v=4'
        self.loop.create_task(self.receive_data())
        self.identified = asyncio.Event()

    async def receive_data(self):
        async with aiohttp.ClientSession() as session:
            self.socket = await session.ws_connect(self.endpoint)
            while True:
                packet = await self.socket.receive()
                if packet.type in (aiohttp.WSMsgType.CLOSED,
                                   aiohttp.WSMsgType.CLOSING,
                                   aiohttp.WSMsgType.CLOSE,
                                   aiohttp.WSMsgType.ERROR):
                    print('**Verbindungsende**')
                    print(packet)
                    break
                print('**Erhalten**')
                pprint(json.loads(packet.data))
                await self.handle_message(json.loads(packet.data))

    async def handle_message(self, msg):
        pass

Wenn Sie dies tun und es funktioniert, werden das Heartbeat-Intervall sowie das erste Gateway zurückgegeben.

==Erhalten==
{'d': ...
 'op': 0,
 's': 5,
 't': 'VOICE_SERVER_UPDATE'}
**Erhalten**
{'d': {'heartbeat_interval': 13750.25, 'v': 4}, 'op': 8}

Um die Verbindung aufrechtzuerhalten, senden Sie diesmal Heartbeat mit "op = 3". Als Daten wird ein Zeitstempel angegeben.

import json
import asyncio
import aiohttp
import threading
import time # <-hinzufügen
from pprint import pprint


class VoiceGatewayHeartbeat(HeartbeatHandler):
    def __init__(self, ws, interval):
        super().__init__(ws, interval)

    def get_payload(self):
        #Zeit in Millisekunden'd'Einstellen
        return {'op': 3, 'd': time.time_ns()//1000}

class VoiceGateway:
    ...

    async def handle_message(self, msg):
        op = msg.get('op')
        d = msg.get('d')
        t = msg.get('t')
        if op == 8:
            self.heartbeat = VoiceGatewayHeartbeat(
                self, d['heartbeat_interval'] / 1000
            )
            return

Da dieser Heartbeat nach der Bot-Authentifizierung ausgeführt werden muss, wird die Kommunikation nicht gestartet und bleibt unverändert.

op0 Identify

Verwenden Sie zur Authentifizierung beim Voice Gateway die Server-ID, die Bot-Benutzer-ID, die Sitzungs-ID und das Token als Nutzdaten und senden Sie sie mit op = 0.

class VoiceGateway:
    ...

    async def identify(self):
        payload = {
            'op': 0,
            'd': {
                'token': self.gateway.token,
                'user_id': '743853432007557210',
                'server_id': '705052322761277540',
                'session_id': self.gateway.session_id,
            }
        }
        print('**Senden**')
        print(payload)
        await self.socket.send_json(payload)

    async def handle_message(self, msg):
        op = msg.get('op')
        d = msg.get('d')
        t = msg.get('t')
        if op == 8:
            await self.identify()
            self.heartbeat = VoiceGatewayHeartbeat(
                self, d['heartbeat_interval'] / 1000
            )
            self.heartbeat.start()
            return

Bei erfolgreicher Authentifizierung erhalten Sie "op2 Ready". "Ip" und "Port" entsprechen der Adresse zum Abrufen von Sprachinformationen, "Modi" entsprechen der von Discord unterstützten Sprachverschlüsselungsmethode und "ssrc" entspricht der Kennung.

**Erhalten**
{'d': {'heartbeat_interval': 13750.25, 'v': 4}, 'op': 8}
**Senden**
{'op': 0, 'd': {'token': '871d40956f7cf34a', 'user_id': '743853432007557210', 'server_id': '705052322761277540', 'session_id': 'c412a670dbed864b559a25009459f15a'}}
==Senden==
{'op': 3, 'd': 1598314493140616}
**Erhalten**
{'d': {'experiments': ['bwe_conservative_link_estimate',
                       'bwe_remote_locus_client'],
       'ip': '123.123.123.123',
       'modes': ['aead_aes256_gcm',
                 'xsalsa20_poly1305_lite',
                 'xsalsa20_poly1305_suffix',
                 'xsalsa20_poly1305'],
       'port': 50004,
       'ssrc': 364117},
 'op': 2}
**Erhalten**
{'d': 1598314493140616, 'op': 6}
==Senden==
{'op': 3, 'd': 1598314506891112}
**Erhalten**
{'d': 1598314506891112, 'op': 6}

UDP-Verbindung

Ich stelle eine UDP-Verbindung zu der in der vorherigen Kommunikation erhaltenen IP her und erhalte Sprachdaten, aber diese IP ist durch NAT verschleiert. -connections # ip-Discovery), daher müssen Sie die Adresse und den Port abrufen, die für die Öffentlichkeit zugänglich sind. Um es zu erhalten, senden Sie das folgende UDP-Paket an den Server von "ip", "port".

Feld Erläuterung Größe
Art 0x1 2 Bytes
Länge 70 2 Bytes
SSRC Ganzzahl ohne Vorzeichen 4 Bytes
IP Adresse ASCII-Code(Der Überschuss ist0x0(Nullzeichen)Pack. 0 beim Senden) 64 Bytes
Hafen Ganzzahl ohne Vorzeichen(0 beim Senden) 2 Bytes

Wenn dies gesendet wird, wird dasselbe 74-Byte-Paket gesendet, das Daten in der IP und im Port enthält, sodass die IP- und Portinformationen von diesem Paket erhalten werden.

import json
import asyncio
import aiohttp
import threading
import time
import socket # <-hinzufügen
import struct # <-hinzufügen
from pprint import pprint

class VoiceGateway:
    ...

    async def ip_discovering(self):
        self.udp = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
        self.udp.setblocking(False)
        packet = bytearray(74)
        packet[:2] = struct.pack('>H', 1)
        packet[2:4] = struct.pack('>H', 70)
        packet[4:8] = struct.pack('>I', self.ssrc)
        self.udp.sendto(bytes(packet), (self.ip, self.port))
        data = await self.loop.sock_recv(self.udp, 2048)
        self.external_ip, self.external_port = struct.unpack_from(
            '>64sH', data, 8
        )
        self.external_ip = self.external_ip.decode(encoding='ascii').rstrip('\x00')
        print(self.external_ip, self.external_port)

    async def handle_message(self, msg):
        op = msg.get('op')
        d = msg.get('d')
        t = msg.get('t')
        if op == 8:
            await self.identify()
            self.heartbeat = VoiceGatewayHeartbeat(
                self, d['heartbeat_interval'] / 1000
            )
            self.heartbeat.start()
            return
        if op == 2:
            self.ip = d['ip']
            self.port = d['port']
            self.modes = d['modes']
            self.ssrc = d['ssrc']
            await self.ip_discovering()

Struct (Standardpaket) wird zum Erstellen von UDP-Paketdaten verwendet. Wenn dies ausgeführt wird, wird ein UDP-Paket unter Verwendung der Ereignisschleife empfangen und die IP und der Port werden an die Konsole ausgegeben.

**Erhalten**
{'d': ...,
 'op': 2}
201.158.201.158 54345

Der Grund für diesen langwierigen Prozess besteht darin, den Schlüssel zum Entschlüsseln der verschlüsselten Stimme zu erhalten. Durch Senden der externen IP und des in diesem Prozess erhaltenen Ports an das Voice Gateway können Sie den Schlüssel zur Entschlüsselung als Antwort erhalten. Was als libsodium bezeichnet wird, wird für die Sprachverschlüsselung verwendet. Im Fall von Python kann die Verschlüsselung und Entschlüsselung mit libsodium durch Hinzufügen des PyNaCl-Pakets durchgeführt werden.

op1 Select Protocol

Machen Sie es möglich, den Schlüssel für die Verwendung mit libsodium zu erhalten. Für "Modus" in der Nutzlast von op1 ist es notwendig, eine der Verschlüsselungsmethoden aus den zuvor in "op2" erhaltenen Modi auszuwählen, aber hier werden wir konsequent "xsalsa20_poly1305" verwenden. .. Wenn Sie "op1" senden, wird "op4 Session Description" als Antwort gesendet. In dieser Nutzlast befindet sich ein Schlüssel zur Entschlüsselung. Nehmen Sie ihn daher heraus.

class VoiceGateway:
    ...

    async def select_protocol(self):
        payload = {
            'op': 1,
            'd': {
                'protocol': 'udp',
                'data': {
                    'address': self.external_ip,
                    'port': self.external_port,
                    'mode': 'xsalsa20_poly1305'
                }
            }
        }
        print('**Senden**')
        print(payload)
        await self.socket.send_json(payload)

    async def receive_audio_packet(self):
        while True:
            data = await self.loop.sock_recv(self.udp, 2048)
            print('**Sprachempfang**')
            print(data)

    async def handle_message(self, msg):
        op = msg.get('op')
        d = msg.get('d')
        t = msg.get('t')
        if op == 8:
            await self.identify()
            self.heartbeat = VoiceGatewayHeartbeat(
                self, d['heartbeat_interval'] / 1000
            )
            self.heartbeat.start()
            return
        if op == 2:
            self.ip = d['ip']
            self.port = d['port']
            self.modes = d['modes']
            self.ssrc = d['ssrc']
            await self.ip_discovering()
            await self.select_protocol()
        if op == 4:
            self.secret_key = d['secret_key']
            self.loop.create_task(self.receive_audio_packet())

Nach dem Empfang von "op4" werden Sprachdaten an den UDP-Socket gesendet, sodass create_task ausgeführt wird, um die Aufgabe des Empfangs von Sprachdaten zu starten.

**Senden**
{'op': 1, 'd': {'protocol': 'udp', 'data': {'address': '106.73.199.128', 'port': 42057, 'mode': 'xsalsa20_poly1305'}}}
**Erhalten**
{'d': {'audio_codec': 'opus',
       ...
       'mode': 'xsalsa20_poly1305',
       'secret_key': [244,
                      157,
                      ...
                      214],
       'video_codec': None},
 'op': 4}
**Sprachempfang**
b'\x81\xc9\x00\x07\x00\x07\xdd(\x9fI\xb9\xd6\x00G\xce\xa2\xa4\x85M[\xed\xd3\x0fu\x15\x89|\xa6W\x1e\xc3U\x06\xc8\xd5S\x8fJ\x08\xfcx\xff\xe9\x83k\xca\xa9\xec'
**Sprachempfang**
b'\x81\xc9\x00\x07\x00\x07\xdd(\x00\x9c^\x83\x90\xc5V\xafX\xff\x14\x97\xf5\xf1/\xad\x15\x89|\xa6W\x1e\xc3U\x06\xc8\xd5S\x8fJ\x08\xfcx\xff\xe9\x83k\xcb\xa9\x02'
**Sprachempfang**
b'\x81\xc9\x00\x07\x00\x07\xdd(j\x88B\\O\xd0\rs`\xc1_\x92\xc6\xe6\xe7=\x15\x89|\xa6W\x1e\xc3U\x06\xc8\xd5S\x8fJ\x08\xfcx\xff\xe9\x83k\xc8\xa9\xfd'
**Sprachempfang**
b'\x81\xc9\x00\x07\x00\x07\xdd(\x05\x02\xf56\x8a\x13\x9e\xc2\xb6\x8c,\xe6r5\x0e\n\x15\x89|\xa6W\x1e\xc3U\x06\xc8\xd5S\x8fJ\x08\xfcx\xff\xe9\x83k\xc9\xa9\x14'

Die Protokolle zum Senden und Empfangen von Discord-Audio sind RTP und RTCP. Jedes Paket, in dem Sprachdaten gespeichert sind, ist ein RTP-Paket, das jeweils 20 ms lang Sprachdaten sendet, und ein RTCP-Paket sendet zusätzliche Informationen zu diesen Sprachdaten.

Konzentrieren Sie sich zur Unterscheidung zwischen RTP und RTCP auf den Wert des zweiten Bytes des Pakets. Gemäß der Definition des Protokolls ist das zweite Byte von RTCP Bereich 200 bis 204, sodass es dort identifiziert werden kann.

Um die Länge des RTP-Headers zu berechnen, achten Sie auf X = "1. Byte 4. Bit" und CC = "1. Byte 5-8.". Ich werde nicht die Rolle jedes Bits erklären, aber

Wenn $ X = 0 $

header\\_length=12+4\times CC

Wenn $ X = 1 $

header\\_length=16+4\times CC+4\times len(EX\\_header)

Es kann wie folgt berechnet werden. len (EX_header) ist ein Wert, der die zusätzliche Headerlänge angibt, die dem Wert von 2 Bytes aus dem Byte "14 + 4 × CC" entspricht.

Einzelheiten finden Sie in der Tabelle auf Wikipedia.

Dieses Mal gibt es kein Problem, wenn Sie nur "Zeitstempel" erhalten können, dh die Sprachübertragungszeit im RTP-Header, also [API-Referenz](https://discord.com/developers/docs/topics/voice-connections#encrypting-and -sending-voice-voice-paketstruktur) und extrahiere die 4.-8. Bytes.

Jetzt können Sie die Audiodaten vorerst abrufen.

abschließend

Aus diesen Informationen ist es möglich, die Nutzlast und den Header vom RTP-Paket zu trennen, es gibt jedoch immer noch einige Probleme.

--Die gesendeten Daten müssen verschlüsselt sein

Im ersteren Fall haben Sie den Schlüssel bereits, sodass Sie ihn basierend darauf entschlüsseln können. Letzteres erfordert eine etwas komplizierte Verarbeitung und stellt eine C-Bibliothek namens "libopus" von Python zur Verfügung. Wenn Sie die Dekodierungsfunktion aufrufen, können sie als normale Wav-Daten gespeichert werden.

Wenn Sie diese beiden löschen können, können Sie die Audiodaten speichern. Das nächste Mal werde ich die vorhandene discord.py erweitern, basierend auf den Erkenntnissen aus dieser unteren Ebene, wie die Daten abgerufen und die Audiodaten gespeichert werden.

Recommended Posts

Discord Bot mit Aufnahmefunktion ab Python: (5) Bedienen Sie die Discord API direkt
Discord Bot mit Aufnahmefunktion beginnend mit Python: (3) Zusammenarbeit mit der Datenbank
Discord Bot mit Aufnahmefunktion beginnend mit Python: (1) Einführung discord.py
Discord Bot mit Aufnahmefunktion ab Python: (4) Musikdateien abspielen
So bedienen Sie die Discord API mit Python (Bot-Registrierung)
Rufen Sie die API mit python3 auf.
Klicken Sie mit Python auf die Etherpad-Lite-API
Starten Sie den Discord Python-Bot 24 Stunden lang.
[Python] Betreiben Sie den Browser automatisch mit Selenium
Discord Bot Himbeere Pi Null mit Python [Hinweis]
LINE BOT mit Python + AWS Lambda + API Gateway
Ich habe versucht, Linux mit Discord Bot zu betreiben
Versuchen Sie, direkt von Python 3 aus auf die YQL-API zuzugreifen
[Einführung in Python] Wie iteriere ich mit der Bereichsfunktion?
Kinesis mit Python betreiben
Python ab Windows 7
Crawlen mit Python und Twitter API 1-Einfache Suchfunktion
GRPC beginnend mit Python
Betreiben Sie Blender mit Python
Betreiben Sie Excel mit Python (1)
Ich habe versucht, die API mit dem Python-Client von echonest zu erreichen
[LINE Messaging API] Erstellen Sie einen Papageienrückgabe-BOT mit Python
Betreiben Sie Excel mit Python (2)
Versuchen Sie, mit Python schnell und einfach auf die Twitter-API zuzugreifen
Die Geschichte der Implementierung des Themas Facebook Messenger Bot mit Python
Vereinheitlichen Sie die Umgebung des Python-Entwicklungsteams, beginnend mit Poetry
Die erste API, die mit dem Python Djnago REST-Framework erstellt wurde
Erstellen Sie einen Mastodon-Bot mit einer Funktion, die automatisch mit Python antwortet
Lassen Sie uns den Befehl pünktlich mit dem Bot der Zwietracht ausführen
Berühren wir die API der Netatmo Weather Station mit Python. #Python #Netatmo
Erstellen Sie einen Twitter-BOT mit dem GoogleAppEngine SDK für Python
Systemhandel ab Python3: Holen Sie sich den neuesten Programmcode
[Einführung in Python] So erhalten Sie Daten mit der Funktion listdir
Betreiben Sie Excel mit Python Open Pyxl
Verwenden Sie die Trello-API mit Python
Betreiben Sie TwitterBot mit Lambda, Python
Verwenden Sie die Twitter-API mit Python
Python-Anfänger startet Discord Bot
Verbessertes Lernen ab Python
Über die Aufzählungsfunktion (Python)
[Hinweis] Betreiben Sie MongoDB mit Python
Web-API mit Python + Falcon
[Python] [SQLite3] Betreiben Sie SQLite mit Python (Basic)
Verwenden Sie die Unterschall-API mit Python3
Python beginnend mit Hallo Welt!
Führen Sie Jupyter mit der REST-API aus, um Python-Code zu extrahieren und zu speichern
Ich möchte das Wetter mit LINE bot feat.Heroku + Python wissen
[Einführung in Python] So teilen Sie eine Zeichenfolge mit der Funktion split
Crawlen mit Python und Twitter API 2-Implementierung der Benutzersuchfunktion
Erstellen Sie mit Python3 + Falcon eine REST-API, die die aktuelle Uhrzeit zurückgibt
[Python] Erklärt anhand eines Beispiels, wie die Formatierungsfunktion verwendet wird
[LINE Messaging API] Erstellen Sie einen BOT, der eine Verbindung zu jemandem mit Python herstellt
Betreiben Sie Firefox mit Selen aus Python und speichern Sie die Bildschirmaufnahme
So senden Sie eine Anfrage mit Python an die DMM (FANZA) -API
Erstellen Sie eine REST-API, um dynamodb mit dem Django REST Framework zu betreiben
Betreiben Sie Nutanix mit der REST-API Teil 2