# Gateway API


<!-- WARNING: THIS FILE WAS AUTOGENERATED! DO NOT EDIT! -->

``` python
from cordslite.core import *
from fastcore.utils import *

import asyncio,httpx,json,os,random
import websockets, websockets.asyncio.client
```

``` python
dc = DiscordClient()
gid = '1493461895615873044'
ch = await dc.channel('1493461896139903028')
```

The Gateway is Discord’s WebSocket connection for **real-time events**.
Unlike the REST API (request/response), the Gateway pushes events to you
as they happen—new messages, reactions, user joins, etc.

The connection lifecycle is: 1. Fetch WSS URL from `/gateway/bot` 2.
Connect to WebSocket 3. Receive **Hello** event with heartbeat interval
4. Start heartbeat loop to keep connection alive 5. Send **Identify**
with your token and intents 6. Receive **Ready** event—you’re connected!

**Intents** tell Discord which events you want. They’re bitwise flags
you OR together: - `1 << 0` = GUILDS (guild/channel events) - `1 << 9` =
GUILD_MESSAGES (message events) - `1 << 15` = MESSAGE_CONTENT
(privileged—see message content)

``` python
class GatewayClient:
    def __init__(self, intents, client, token=None):
        self.intents,self.dc = intents,client
        self.token = token or os.environ['DISCORD_BOT_TOKEN']
        self.ws = self.hb_int = self.session_id = self.seq = None
        self.running = False
        self._tries = 0
        gw_info = httpx.get(f'{client.base_url}/gateway/bot', headers={'Authorization': f'Bot {self.token}'}).json()
        if 'url' not in gw_info: raise ConnectionError(f"Gateway auth failed: {gw_info.get('message', gw_info)}")
        self.url = f"{gw_info['url']}?v=10&encoding=json"
        self.handlers = {'READY': self.on_rdy}
    
    async def on_rdy(self, evt):
        self.session_id, self.user_id, = evt['session_id'], evt['user']['id']
        self.resume_url = evt['resume_gateway_url'] + '?v=10&encoding=json' if 'resume_gateway_url' in evt else self.url
        
    def __repr__(self): return f"GatewayClient({self.intents=}, {self.url=})"
```

``` python
# For now, let's use basic intents for guilds and messages
intents = (1 << 0) | (1 << 7) | (1 << 9) | (1 << 15) # GUILDS | VOICE_EVENTS | GUILD_MESSAGES | MESSAGE_CONTENT

gc = GatewayClient(intents, dc); gc
```

    GatewayClient(self.intents=33409, self.url='wss://gateway.discord.gg?v=10&encoding=json')

[`Op`](https://AnswerDotAI.github.io/cordslite/gateway.html#op) is a
thin helper for constructing Gateway opcodes. Discord’s Gateway expects
JSON payloads with an `op` field (operation code) and `d` field (payload
data). Rather than hand-building dicts each time,
[`Op.identify()`](https://AnswerDotAI.github.io/cordslite/gateway.html#op.identify)
and
[`Op.heartbeat()`](https://AnswerDotAI.github.io/cordslite/gateway.html#op.heartbeat)
return properly structured payloads ready to send over the WebSocket.

``` python
class Op(AttrDict):
    def __repr__(self): return f"Op(op={self.op}, d={self.d})"
    @classmethod
    def identify(cls, token, intents):
        d = AttrDict(token=token, intents=intents, properties=dict(os='linux',browser='discord_wrapper', device='discord_wrapper'))
        return cls(op=2, d=d)
    @classmethod
    def heartbeat(cls, seq): return cls(op=1, d=seq)
    @classmethod
    def resume(cls, token, session_id, seq): return cls(op=6, d=AttrDict(token=token, session_id=session_id, seq=seq))
```

The Gateway sends events as JSON with four fields: - **`op`** —
operation code: `0` = dispatch (real events), `1` = heartbeat request,
`10` = hello, `11` = heartbeat ACK - **`t`** — event type name (only for
dispatches): `MESSAGE_CREATE`, `GUILD_CREATE`, etc. - **`s`** — sequence
number (only for dispatches): increments per event, needed for
heartbeats and resuming - **`d`** — the payload data

We track the latest `s` value so heartbeats can tell Discord “I’ve
received everything up to here.” The
[`Event`](https://AnswerDotAI.github.io/cordslite/gateway.html#event)
class wraps raw JSON and auto-converts known dispatch types (like
`MESSAGE_CREATE`) into their corresponding wrapper classes
([`Message`](https://AnswerDotAI.github.io/cordslite/core.html#message),
[`Channel`](https://AnswerDotAI.github.io/cordslite/core.html#channel),
etc.) via `evt_typs`.

``` python
evt_typs = {'MESSAGE_CREATE': Message,
            'MESSAGE_UPDATE': Message,
            'MESSAGE_DELETE': Message,
            'GUILD_CREATE': Guild,
            'CHANNEL_CREATE': Channel}

class Event(DiscordObject):
    def __init__(self, data, client):
        super().__init__(data, client)
        typ = evt_typs.get(self.t)
        self.d = typ(self.d, client) if typ else self.d
    @property
    def type(self): return self.t
    @property
    def seq(self): return self.s
    def __repr__(self): return f"Event({self.op=}, {self.type=}, {self.seq=})"

@patch
async def send(self:websockets.asyncio.client.ClientConnection, msg, **kw):
    if isinstance(msg, dict): msg = json.dumps(msg)
    return await self._orig_send(msg, **kw)
```

`recv_evt` is the low-level building block for receiving events. It
reads one raw WebSocket message, wraps it as an
[`Event`](https://AnswerDotAI.github.io/cordslite/gateway.html#event)
(auto-converting known dispatch types), and updates the sequence
counter. You can use it directly to pull events one at a time — useful
for debugging or understanding what Discord is sending.

``` python
@patch
async def recv_evt(self:GatewayClient):
    evt = Event(json.loads(await self.ws.recv()), self.dc)
    if evt.op == 0 and evt.seq: self.seq,self._tries = evt.seq,0
    return evt
```

Rather than special-casing the READY event inside `_listen`, we register
it as a regular dispatch handler via `gc.on()`. When Discord confirms a
successful Identify, `on_rdy` caches `session_id`, `user_id`, and
`resume_gateway_url` — the three values needed to Resume after a
disconnect. Note the query params appended to `resume_gateway_url` —
Discord returns it bare, but the docs require the same version and
encoding as the initial connection.

``` python
@patch
def on(self:GatewayClient, event_type, handler): self.handlers[event_type] = handler
```

The gateway runs on two cooperating loops. `_hb` sends heartbeats at the
interval Discord specifies and watches for ACK responses — if one is
missed, the connection is zombied and gets closed with code 4000 (which
preserves the session for Resuming). `_listen` reads events and
dispatches them by opcode. On disconnect (exception from `recv_evt`), it
calls `_reconnect` to swap in a fresh WebSocket. The new connection
triggers Hello (op 10), which restarts the heartbeat and sends either
Identify (first connect) or Resume (reconnect) depending on whether a
session already exists. Op 7 (Reconnect) follows the same path — close
and reopen with a resume.

All reconnection funnels through `_reconnect`. A resume closes with code
4000 (keeping the session valid) and opens a new socket to
`resume_gateway_url`; a re-identify deliberately closes with 1000
(invalidating the session) and goes back to the original gateway URL.
The listener’s op 10 handler then takes over automatically.

``` python
@patch
async def _hb(self:GatewayClient):
    await asyncio.sleep(self.hb_int / 1_000 * random.random()) # jitter
    while self.running:
        self._got_ack = False
        try: await self.ws.send(Op.heartbeat(self.seq))
        except Exception: return
        await asyncio.sleep(self.hb_int / 1_000)
        if not self._got_ack: return await self.ws.close(code=4000)

@patch
async def _reconnect(self:GatewayClient, resume=False):
    resume = resume and bool(self.session_id)
    code, url = (4000, self.resume_url) if resume else (1000, self.url)
    if not resume: self.session_id = None
    try: await self.ws.close(code=code)
    except Exception: pass
    while self.running:
        await asyncio.sleep(min(2 ** self._tries, 60))
        self._tries += 1
        try:
            self.ws = await websockets.connect(url, max_size=None)
            return
        except OSError as e: print('gateway reconnect failed:', repr(e))

@patch
async def _listen(self:GatewayClient):
    while self.running:
        try:
            evt = await self.recv_evt()
            if self.debug: print('DEBUG: Received Opcode:', evt.op)
            if evt.op == 10:
                self.hb_int = evt.d['heartbeat_interval']
                if hasattr(self, '_hb_task') and self._hb_task: self._hb_task.cancel()
                self._hb_task = asyncio.create_task(self._hb())
                if self.session_id: await self.ws.send(Op.resume(self.token, self.session_id, self.seq))
                else: await self.ws.send(Op.identify(self.token, self.intents))
            elif evt.op == 0 and evt.type in self.handlers: asyncio.create_task(self.handlers[evt.type](evt.d))
            elif evt.op == 11: self._got_ack = True
            elif evt.op == 1: await self.ws.send(Op.heartbeat(self.seq))
            elif evt.op == 7: await self._reconnect(resume=True)
            elif evt.op == 9: await self._reconnect(resume=evt.d)
        except websockets.exceptions.ConnectionClosed as e:
            if not self.running: break
            print(f'gateway closed: {e.code} {e.reason}')
            if e.code in (4004, 4010, 4011, 4012, 4013, 4014): # fatal: bad token/shard/intents
                self.running = False
                break
            await self._reconnect(resume=e.code not in (4007, 4009))
            continue
        except Exception as e:
            print('gateway listen error:', repr(e))
            await self._reconnect(resume=True)
            continue
```

`start` opens the initial WebSocket and spawns the listener — nothing
else. The listener handles Hello, kicks off the heartbeat, and sends
Identify, so the full connection lifecycle is driven by events rather
than imperative setup.

``` python
@patch
async def start(self:GatewayClient, debug=False):
    self.debug = debug
    self.running = True
    self.ws = await websockets.connect(self.url, max_size=None)
    self._listen_task = asyncio.create_task(self._listen())
```

``` python
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
    DBuddy: Test our event listener! Otters are awesome 🦦
    gateway closed: 4000 
    DEBUG: Received Opcode: 10
    DEBUG: Received Opcode: 0
    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: 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
    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
    DEBUG: Received Opcode: 11

Let’s now test that our gateway is getting events. Here is a simple
handlers for new messages.

``` python
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)
```

Now, we can send a message and it should be printed out.

``` python
await ch.send('Test our event listener! Otters are awesome 🦦')
```

<div class="prose" markdown="1">

``` python
Message(id=1524528166285082704, author='DBuddy', content='Test our event listener! Otter…')
```

</div>

Let’s trigger and reconnection that resumes and one that doesn’t and see
if everything works correctly and we still end up with our printed
message.

``` python
await gc._reconnect(resume=True)
await asyncio.sleep(2)
await ch.send('Houston, do we have a problem?')
```

<div class="prose" markdown="1">

``` python
Message(id=1524528193283948816, author='DBuddy', content='Houston, do we have a problem?')
```

</div>

``` python
await gc._reconnect(resume=False)
await asyncio.sleep(2)
await ch.send('Did we re-identify?')
```

<div class="prose" markdown="1">

``` python
Message(id=1524528240155430924, author='DBuddy', content='Did we re-identify?')
```

</div>

Those were polite reconnects we initiated. Real disconnects arrive as a
`ConnectionClosed` in the listener, which classifies the close code per
the
[docs](https://docs.discord.com/developers/topics/opcodes-and-status-codes#gateway-gateway-close-event-codes):
transient codes resume, 4007/4009 re-identify, and 4004/4010–4014 (bad
token, bad shard, bad intents) stop rather than loop forever. We can
trigger the transient path for real by killing our own live socket with
a non-1000 code — exactly what a zombied connection’s heartbeat watchdog
does — and confirm the listener resumes on its own.

``` python
await gc.ws.close(code=4000)   # simulate a dead connection; the listener should resume unaided
await asyncio.sleep(3)
await ch.send('Back from the dead? (listener auto-resume)')
```

<div class="prose" markdown="1">

``` python
Message(id=1524528333700862142, author='DBuddy', content='Back from the dead? (listener …')
```

</div>

And the fatal path for real: identify with a corrupted token and Discord
closes us with 4004 — the client must give up instead of hammering the
gateway.

``` python
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.running   # 4004 authentication failed: no reconnect loop
```

    gateway closed: 4004 Authentication failed.

`stop` is a full shutdown — cancels both background tasks and closes the
WebSocket with the default code (1000), which tells Discord to
invalidate the session. After this, a fresh `start()` would need to
Identify from scratch.

``` python
@patch
async def stop(self:GatewayClient):
    self.running = False
    for t in (self._hb_task, self._listen_task):
        if t: t.cancel()
    if self.ws: await self.ws.close()
```

``` python
await gc.stop()
```
