dc = DiscordClient()
gid = '1493461895615873044'
ch = await dc.channel('1493461896139903028')Gateway API
The Gateway is Discord’s websocket for real-time events. The REST API answers requests. The Gateway pushes new messages, reactions, member joins, and other events as they happen.
The connection lifecycle:
- Fetch the websocket URL from
/gateway/bot. - Connect.
- Receive Hello with the heartbeat interval.
- Start the heartbeat loop.
- Send Identify with the token and intents.
- Receive Ready.
Intents tell Discord which events to send. They are bit flags combined with |. Intent names the full set from the gateway docs. GUILD_MEMBERS, GUILD_PRESENCES, and MESSAGE_CONTENT are privileged and must also be enabled in the Developer Portal.
Intent
def Intent(
*args, **kwds
):Gateway intents: OR members together and pass to GatewayClient
intents = Intent.GUILDS | Intent.GUILD_VOICE_STATES | Intent.GUILD_MESSAGES | Intent.MESSAGE_CONTENT
intents<Intent.GUILDS|GUILD_VOICE_STATES|GUILD_MESSAGES|MESSAGE_CONTENT: 33409>
GatewayClient holds one gateway connection. It fetches the websocket URL from /gateway/bot when constructed, and dispatches each event to the handler registered for its type with on.
gc = GatewayClient(intents, dc)
gcGatewayClient(self.intents=<Intent.GUILDS|GUILD_VOICE_STATES|GUILD_MESSAGES|MESSAGE_CONTENT: 33409>, self.url='wss://gateway.discord.gg?v=10&encoding=json')
Op builds gateway payloads. Discord expects JSON with an op field (operation code) and a d field (data). Op.identify() and Op.heartbeat() return payloads ready to send.
Op
def Op(
*args, **kwargs
):dict subclass that also provides access to keys as attrs, and has a pretty markdown repr
A gateway event is JSON with four fields. op is the operation code: 0 dispatch, 1 heartbeat request, 10 hello, 11 heartbeat ACK. t is the event type name, such as MESSAGE_CREATE, present only on dispatches. s is the sequence number, present only on dispatches. Heartbeats and resumes send it back. d is the payload.
The client tracks the latest s. Event wraps the JSON and converts known dispatch types to their wrapper classes through evt_typs.
ClientConnection.send
async def send(
msg, **kw
):Event
def Event(
data, client
):dict subclass that also provides access to keys as attrs, and has a pretty markdown repr
recv_evt reads one websocket message, wraps it as an Event, and updates the sequence counter. Call it directly to pull events one at a time when debugging.
gc.on registers the READY handler like any other dispatch handler. on_rdy stores session_id, user_id, and resume_gateway_url, the three values a Resume needs. Discord returns resume_gateway_url bare. The client appends the same version and encoding query as the initial connection, as the docs require.
The gateway runs two loops. _hb sends heartbeats at the interval Discord specifies and watches for ACKs. A missed ACK means the connection is dead. _hb closes it with code 4000. That code keeps the session valid for a Resume. _listen reads events and dispatches them by opcode. On disconnect it calls _reconnect to open a fresh websocket. The new connection sends Hello (op 10). The Hello handler restarts the heartbeat and sends Identify on a first connect or Resume when a session exists. Op 7 (Reconnect) takes the same path.
All reconnection goes through _reconnect. A resume closes with code 4000 and opens a socket to resume_gateway_url. A re-identify closes with 1000 and returns to the original gateway URL. Code 1000 invalidates the session. The Hello handler then takes over.
start opens the websocket and starts the listener. The listener handles Hello, starts the heartbeat, and sends Identify. Events drive the whole connection lifecycle.
await gc.start(debug=True)DEBUG: Received Opcode: 10
DEBUG: Received Opcode: 0
DEBUG: Received Opcode: 0
DEBUG: Received Opcode: 0
DEBUG: Received Opcode: 0
DEBUG: Received Opcode: 0
DEBUG: Received Opcode: 0
DEBUG: Received Opcode: 0
DBuddy: Test our event listener! Otters are awesome 🦦
gateway closed: 4000
DEBUG: Received Opcode: 10
DEBUG: Received Opcode: 0
DEBUG: Received Opcode: 0
DBuddy: Houston, do we have a problem?
gateway closed: 1000
DEBUG: Received Opcode: 10
DEBUG: Received Opcode: 0
DEBUG: Received Opcode: 0
DEBUG: Received Opcode: 0
DEBUG: Received Opcode: 0
DEBUG: Received Opcode: 0
DEBUG: Received Opcode: 0
DEBUG: Received Opcode: 0
DBuddy: Did we re-identify?
gateway closed: 4000
DEBUG: Received Opcode: 10
DEBUG: Received Opcode: 0
DEBUG: Received Opcode: 0
DBuddy: Back from the dead? (listener auto-resume)
DEBUG: Received Opcode: 0
Let’s now test that our gateway is getting events. Here is a simple handlers for new messages.
async def on_msg(msg):
if msg.get('guild_id') != gid: return
if not msg.author.get('bot'): return
print(f"{msg.author['username']}: {msg.content}")
gc.on('MESSAGE_CREATE', on_msg)The handler prints a message sent to the channel.
await ch.send('Test our event listener! Otters are awesome 🦦')Message(id=1544802822153969789, author='DBuddy', content='Test our event listener! Otter…')Next, a reconnect that resumes and one that re-identifies. The handler should still print after each.
await gc._reconnect(resume=True)
await asyncio.sleep(2)
await ch.send('Houston, do we have a problem?')Message(id=1544802837282689168, author='DBuddy', content='Houston, do we have a problem?')await gc._reconnect(resume=False)
await asyncio.sleep(2)
await ch.send('Did we re-identify?')Message(id=1544802851937722498, author='DBuddy', content='Did we re-identify?')Those reconnects were polite ones the client started. A real disconnect arrives as ConnectionClosed in the listener. The listener classifies the close code per the docs. Transient codes resume. 4007 and 4009 re-identify. 4004 and 4010 to 4014 (bad token, bad shard, bad intents) stop. Closing the live socket with a non-1000 code triggers the transient path. The heartbeat watchdog does the same on a dead connection. The listener should resume without help.
await gc.ws.close(code=4000)
await asyncio.sleep(3)
await ch.send('Back from the dead? (listener auto-resume)')Message(id=1544802865732657222, author='DBuddy', content='Back from the dead? (listener …')Identifying with a corrupted token gets close code 4004. The client must stop instead of retrying.
gc_bad = GatewayClient(intents, dc)
gc_bad.token = gc_bad.token[:-4] + 'zzzz'
await gc_bad.start()
for _ in range(30):
if not gc_bad.running: break
await asyncio.sleep(0.5)
assert not gc_bad.runninggateway closed: 4004 Authentication failed.
stop cancels both background tasks and closes the websocket with code 1000. That tells Discord to invalidate the session. A later start identifies from scratch.
await gc.stop()