# cordslite API


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

``` python
from IPython.display import Audio
```

Discord has three main APIs: - **REST API** - for actions (send message,
get channels, fetch history). Request/response style. - **Gateway
API** - for real-time events via WebSocket (new messages, reactions,
user joins). - **Voice API** - for real-time streaming of audio via UDP

We start with the REST API. To use it, you need a **bot token** from the
[Discord Developer
Portal](https://discord.com/developers/applications): 1. Create an
application 2. Go to “Bot” section and create a bot 3. Copy the token
(keep it secret!) 4. Under “OAuth2 → URL Generator”, select `bot` scope
and choose permissions (e.g., `Send Messages`, `Read Message History`)
5. Use the generated URL to invite the bot to your server

The
[`DiscordClient`](https://AnswerDotAI.github.io/cordslite/core.html#discordclient)
wraps `httpx.Client` with Discord’s base URL
(`https://discord.com/api/v10`) and auth headers pre-configured.

## Failed to import opuslib-next

<a
href="https://github.com/AnswerDotAI/cordslite/blob/main/cordslite/core.py#L21"
target="_blank" style="float:right; font-size:smaller">source</a>

### DiscordClient

``` python

def DiscordClient(
    token:NoneType=None, user_token:NoneType=None, name:str='cordslite', ver:str='0.1'
):

```

*Initialize self. See help(type(self)) for accurate signature.*

The token defaults to the `DISCORD_BOT_TOKEN` environment variable—no
hardcoding secrets!

``` python
dc = DiscordClient()
```

## REST endpoints

Every REST call goes through `_req`, which handles two concerns
automatically: - **Rate limiting** — if Discord returns
`429 Too Many Requests`, we wait the `Retry-After` duration and retry. -
**Error handling** — non-2xx responses are raised as
[`DiscordError`](https://AnswerDotAI.github.io/cordslite/core.html#discorderror)
with Discord’s error code, message, and HTTP status, so callers don’t
need to check responses manually.

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/cordslite/blob/main/cordslite/core.py#L31"
target="_blank" style="float:right; font-size:smaller">source</a>

### DiscordError

``` python

def DiscordError(
    code, msg, status
):

```

*Common base class for all non-exit exceptions.*

Discord’s REST endpoints return JSON with many fields. Rather than
defining properties for every field, we use a flexible base class
pattern:

[`DiscordObject`](https://AnswerDotAI.github.io/cordslite/core.html#discordobject)
provides `__getitem__` and `__getattr__` so you can access data as
`obj.name` or `obj['name']`. The `__dir__` method enables autocomplete
in notebooks and solveit dialogs—just type `obj.` and see all available
fields!

This means when Discord adds new fields to their API, our code
automatically supports them without changes.

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/cordslite/blob/main/cordslite/core.py#L56"
target="_blank" style="float:right; font-size:smaller">source</a>

### DiscordObject

``` python

def DiscordObject(
    data, client
):

```

*`dict` subclass that also provides access to keys as attrs, and has a
pretty markdown repr*

Discord’s hierarchy is **Guild → Channels → Messages**. A Guild (server)
contains channels, and channels contain messages. Each has its own REST
endpoints: - `GET /guilds/{id}` - fetch guild info -
`GET /guilds/{id}/channels` - list channels -
`GET /channels/{id}/messages` - fetch messages -
`POST /channels/{id}/messages` - send a message

We build wrapper classes for each, inheriting from
[`DiscordObject`](https://AnswerDotAI.github.io/cordslite/core.html#discordobject)
and just adding a nice `__repr__`.

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/cordslite/blob/main/cordslite/core.py#L66"
target="_blank" style="float:right; font-size:smaller">source</a>

### Guild

``` python

def Guild(
    data, client
):

```

*`dict` subclass that also provides access to keys as attrs, and has a
pretty markdown repr*

We use `@patch` from fastcore to add methods incrementally—great for
interactive development where you want to test each piece as you build
it. This `guild` method fetches guild data and wraps it in our
[`Guild`](https://AnswerDotAI.github.io/cordslite/core.html#guild)
class.

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/cordslite/blob/main/cordslite/core.py#L71"
target="_blank" style="float:right; font-size:smaller">source</a>

### DiscordClient.guild

``` python

async def guild(
    guild_id
):

```

*Call self as a function.*

``` python
# gid = '1493461895615873044'
gid = '1327046393453613076'
gld = await dc.guild(gid); gld
```

``` python
Guild(id=1327046393453613076, name="natedog's server")
```

Channels have a `type` field: 0=text, 2=voice, 4=category. The
[`Channels`](https://AnswerDotAI.github.io/cordslite/core.html#channels)
class inherits from `list` and adds `_repr_html_` for nice table display
in notebooks/solveit. This pattern—a wrapper class plus a collection
class with HTML repr—makes exploring the API much more pleasant.

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/cordslite/blob/main/cordslite/core.py#L75"
target="_blank" style="float:right; font-size:smaller">source</a>

### html_table

``` python

def html_table(
    items, hdrs, fn
):

```

*Call self as a function.*

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/cordslite/blob/main/cordslite/core.py#L88"
target="_blank" style="float:right; font-size:smaller">source</a>

### Channels

``` python

def Channels(
    args:VAR_POSITIONAL, kwargs:VAR_KEYWORD
):

```

*Built-in mutable sequence.*

If no argument is given, the constructor creates a new empty list. The
argument must be an iterable if specified.

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/cordslite/blob/main/cordslite/core.py#L81"
target="_blank" style="float:right; font-size:smaller">source</a>

### Channel

``` python

def Channel(
    data, client
):

```

*`dict` subclass that also provides access to keys as attrs, and has a
pretty markdown repr*

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/cordslite/blob/main/cordslite/core.py#L93"
target="_blank" style="float:right; font-size:smaller">source</a>

### Guild.channels

``` python

async def channels(
    limit:NoneType=None
):

```

*Call self as a function.*

``` python
chs = await gld.channels(5)
chs
```

<table class="prose" data-quarto-postprocess="true">
<thead>
<tr>
<th data-quarto-table-cell-role="th">ID</th>
<th data-quarto-table-cell-role="th">Name</th>
<th data-quarto-table-cell-role="th">Type</th>
</tr>
</thead>
<tbody>
<tr>
<td>1327046393453613077</td>
<td>Text Channels</td>
<td>4</td>
</tr>
<tr>
<td>1327046393453613078</td>
<td>Voice Channels</td>
<td>4</td>
</tr>
<tr>
<td>1327046393453613079</td>
<td>general</td>
<td>0</td>
</tr>
<tr>
<td>1327046393453613080</td>
<td>General</td>
<td>2</td>
</tr>
<tr>
<td>1327954661960978512</td>
<td>private</td>
<td>0</td>
</tr>
</tbody>
</table>

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/cordslite/blob/main/cordslite/core.py#L100"
target="_blank" style="float:right; font-size:smaller">source</a>

### DiscordClient.channel

``` python

async def channel(
    channel_id
):

```

*Call self as a function.*

``` python
chid = '1327046393453613079' # '1493461896139903028'
```

``` python
ch = await dc.channel(chid)
ch
```

``` python
Channel(id=1327046393453613079, name='general', type=0)
```

Messages are the core of most bot functionality. Note that Discord
returns messages in reverse chronological order (newest first), so we
`reverse()` the data to get chronological order. The table shows a
preview of content, author, and timestamp.

Note: To read message content, your bot needs the `MESSAGE_CONTENT`
privileged intent enabled in the Developer Portal! Without it, the
`content` field will be empty for messages not sent by your bot or
mentioning it.

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/cordslite/blob/main/cordslite/core.py#L104"
target="_blank" style="float:right; font-size:smaller">source</a>

### Message

``` python

def Message(
    data, dobj
):

```

*`dict` subclass that also provides access to keys as attrs, and has a
pretty markdown repr*

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/cordslite/blob/main/cordslite/core.py#L123"
target="_blank" style="float:right; font-size:smaller">source</a>

### Messages

``` python

def Messages(
    args:VAR_POSITIONAL, kwargs:VAR_KEYWORD
):

```

*Built-in mutable sequence.*

If no argument is given, the constructor creates a new empty list. The
argument must be an iterable if specified.

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/cordslite/blob/main/cordslite/core.py#L130"
target="_blank" style="float:right; font-size:smaller">source</a>

### Channel.messages

``` python

async def messages(
    limit:int=50
):

```

*Call self as a function.*

``` python
msgs = await ch.messages(5); msgs
```

<table class="prose" data-quarto-postprocess="true">
<thead>
<tr>
<th data-quarto-table-cell-role="th">ID</th>
<th data-quarto-table-cell-role="th">Author</th>
<th data-quarto-table-cell-role="th">Content</th>
<th data-quarto-table-cell-role="th">Date</th>
</tr>
</thead>
<tbody>
<tr>
<td>1494381457928618014</td>
<td>DBuddy</td>
<td>I'm replying to myself 🤓</td>
<td>2026-04-16</td>
</tr>
<tr>
<td>1494381464177999893</td>
<td>DBuddy</td>
<td>Here is a file!</td>
<td>2026-04-16</td>
</tr>
<tr>
<td>1494381474240270511</td>
<td>DBuddy</td>
<td>Test our event listener! Otters are awesome 🦦</td>
<td>2026-04-16</td>
</tr>
<tr>
<td>1494381547586064515</td>
<td>DBuddy</td>
<td>Houston, do we have a problem?</td>
<td>2026-04-16</td>
</tr>
<tr>
<td>1494381604842504372</td>
<td>DBuddy</td>
<td>Did we re-identify?</td>
<td>2026-04-16</td>
</tr>
</tbody>
</table>

Guild search uses snowflake IDs for date filtering (`min_id`/`max_id`),
but we autoconvert `'YYYY-MM-DD'` strings for convenience for
`before/after`.

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/cordslite/blob/main/cordslite/core.py#L142"
target="_blank" style="float:right; font-size:smaller">source</a>

### Guild.search

``` python

async def search(
    content:NoneType=None, author_id:NoneType=None, channel_id:NoneType=None, mentions:NoneType=None,
    has:NoneType=None, before:NoneType=None, after:NoneType=None, pinned:NoneType=None, sort_by:NoneType=None,
    sort_order:NoneType=None, offset:NoneType=None, limit:NoneType=None, use_user:bool=False
):

```

*Search guild messages. `before`/`after` accept ‘YYYY-MM-DD’ strings or
snowflake IDs.*

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/cordslite/blob/main/cordslite/core.py#L136"
target="_blank" style="float:right; font-size:smaller">source</a>

### date2snowflake

``` python

def date2snowflake(
    date_str
):

```

*Convert ‘YYYY-MM-DD’ to a Discord snowflake ID*

``` python
await gld.search(after='2026-02-16', limit=5)
```

<table class="prose" data-quarto-postprocess="true">
<thead>
<tr>
<th data-quarto-table-cell-role="th">ID</th>
<th data-quarto-table-cell-role="th">Author</th>
<th data-quarto-table-cell-role="th">Content</th>
<th data-quarto-table-cell-role="th">Date</th>
</tr>
</thead>
<tbody>
<tr>
<td>1494381604842504372</td>
<td>DBuddy</td>
<td>Did we re-identify?</td>
<td>2026-04-16</td>
</tr>
<tr>
<td>1494381547586064515</td>
<td>DBuddy</td>
<td>Houston, do we have a problem?</td>
<td>2026-04-16</td>
</tr>
<tr>
<td>1494381474240270511</td>
<td>DBuddy</td>
<td>Test our event listener! Otters are awesome 🦦</td>
<td>2026-04-16</td>
</tr>
<tr>
<td>1494381464177999893</td>
<td>DBuddy</td>
<td>Here is a file!</td>
<td>2026-04-16</td>
</tr>
<tr>
<td>1494381457928618014</td>
<td>DBuddy</td>
<td>I'm replying to myself 🤓</td>
<td>2026-04-16</td>
</tr>
</tbody>
</table>

Sometimes you need to search by name rather than snowflake ID.
`find_member` searches the guild’s members by username, nickname, or
display name using Discord’s member search endpoint, and returns the
first match’s user ID. This makes it easy to chain into `search`.

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/cordslite/blob/main/cordslite/core.py#L156"
target="_blank" style="float:right; font-size:smaller">source</a>

### Guild.find_member

``` python

async def find_member(
    name
):

```

*Search guild members by name/nick, return first match’s user ID or
None*

``` python
uid = await gld.find_member('jeremyhoward')
assert uid
await gld.search(author_id=uid, limit=5)
```

The `members` property fetches all guild members and returns their
display names, preferring server nickname over global display name over
username. Note: this requires the **Server Members Intent** enabled in
the Developer Portal (Bot → Privileged Gateway Intents).

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/cordslite/blob/main/cordslite/core.py#L215"
target="_blank" style="float:right; font-size:smaller">source</a>

### Guild.members

``` python

async def members(
    
):

```

*List all guild members’ display names (nick \> global_name \>
username)*

``` python
(await gld.members)[:5]
```

    ['nathan', 'SearchBuddy', 'DBuddy', 'Dizcord Util Bot', 'Search Agent']

Sending messages is a POST request with JSON body. Pass `reply_id` to
thread a reply under an existing message. For file attachments, we
switch to `multipart/form-data`—Discord expects a `payload_json` field
with the message JSON, plus `files[n]` fields for each file.

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/cordslite/blob/main/cordslite/core.py#L170"
target="_blank" style="float:right; font-size:smaller">source</a>

### Channel.send

``` python

async def send(
    content:str='', files:NoneType=None, reply_id:NoneType=None
):

```

*Send a message with optional file attachments*

``` python
msg = await ch.send('Hi, from Solveit!'); msg
```

``` python
Message(id=1494383523015164087, author='DBuddy', content='Hi, from Solveit!')
```

``` python
reply_msg = await ch.send("I'm replying to myself 🤓", reply_id=msg.id); reply_msg
```

``` python
Message(id=1494383523946303661, author='DBuddy', content="I'm replying to myself 🤓")
```

``` python
await msg.channel
```

``` python
Channel(id=1327046393453613079, name='general', type=0)
```

``` python
msg = await ch.send('Here is a file!', files=['../README.md']); msg
```

``` python
Message(id=1494383525539872970, author='DBuddy', content='Here is a file!')
```

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/cordslite/blob/main/cordslite/core.py#L182"
target="_blank" style="float:right; font-size:smaller">source</a>

### Channel.search

``` python

async def search(
    content:NoneType=None, author_id:NoneType=None, mentions:NoneType=None, has:NoneType=None, before:NoneType=None,
    after:NoneType=None, pinned:NoneType=None, sort_by:NoneType=None, sort_order:NoneType=None, offset:NoneType=None,
    limit:NoneType=None, use_user:bool=False
):

```

*Call self as a function.*

``` python
await ch.search(has='file', limit=1)
```

<table class="prose" data-quarto-postprocess="true">
<thead>
<tr>
<th data-quarto-table-cell-role="th">ID</th>
<th data-quarto-table-cell-role="th">Author</th>
<th data-quarto-table-cell-role="th">Content</th>
<th data-quarto-table-cell-role="th">Date</th>
</tr>
</thead>
<tbody>
<tr>
<td>1494381464177999893</td>
<td>DBuddy</td>
<td>Here is a file!</td>
<td>2026-04-16</td>
</tr>
</tbody>
</table>

Discord messages can have file attachments. The
[`Attachment`](https://AnswerDotAI.github.io/cordslite/core.html#attachment)
class wraps them as
[`DiscordObject`](https://AnswerDotAI.github.io/cordslite/core.html#discordobject)s—giving
you attribute access to `filename`, `size`, `content_type`, `url`, etc.
The `fetch` method downloads the file content using the existing httpx
client.

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/cordslite/blob/main/cordslite/core.py#L194"
target="_blank" style="float:right; font-size:smaller">source</a>

### Message.attachments

``` python

def attachments(
    
):

```

*Call self as a function.*

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/cordslite/blob/main/cordslite/core.py#L185"
target="_blank" style="float:right; font-size:smaller">source</a>

### Attachment

``` python

def Attachment(
    data, client
):

```

*`dict` subclass that also provides access to keys as attrs, and has a
pretty markdown repr*

``` python
atts = msg.attachments; atts
```

    [Attachment(filename='README.md', size=28163, type=text/markdown; charset=utf-8)]

``` python
readme = (await atts[0].fetch()).decode()
print(readme[:16])
```

    # cordslite 🍺

DMs (Direct Messages) are just regular channels in Discord’s API — no
special handling needed! To start a DM conversation, POST to
`/users/@me/channels` with a `recipient_id`. Discord returns a standard
channel object, so `send()` and `messages()` work exactly as they do for
guild channels.

To detect DMs in the gateway, check for the absence of `guild_id` — DM
messages don’t belong to any guild, so this field is missing or `None`.
This makes it easy to route DM vs guild messages in your bot’s handler.

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/cordslite/blob/main/cordslite/core.py#L198"
target="_blank" style="float:right; font-size:smaller">source</a>

### DiscordClient.create_dm

``` python

async def create_dm(
    user_id
):

```

*Call self as a function.*

``` python
# # Commented out so we don't spam Nate
# dm = await dc.create_dm('346450717025894400')  # nathan's user ID
# await dm.send('Hello from DMs!')
```

Members vs Users: A **User** is a global Discord account. A **Member**
is a user *within a specific guild*—it has guild-specific data like
nickname, roles, and join date. The `nick or user['username']` pattern
shows the server nickname if set, otherwise falls back to the global
username.

Note: The members endpoint requires the `GUILD_MEMBERS` privileged
intent enabled in your bot settings on the Developer Portal!

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/cordslite/blob/main/cordslite/core.py#L215"
target="_blank" style="float:right; font-size:smaller">source</a>

### Guild.members

``` python

async def members(
    limit:int=100
):

```

*Call self as a function.*

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/cordslite/blob/main/cordslite/core.py#L211"
target="_blank" style="float:right; font-size:smaller">source</a>

### Members

``` python

def Members(
    args:VAR_POSITIONAL, kwargs:VAR_KEYWORD
):

```

*Built-in mutable sequence.*

If no argument is given, the constructor creates a new empty list. The
argument must be an iterable if specified.

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/cordslite/blob/main/cordslite/core.py#L206"
target="_blank" style="float:right; font-size:smaller">source</a>

### Member

``` python

def Member(
    data, client
):

```

*`dict` subclass that also provides access to keys as attrs, and has a
pretty markdown repr*

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/cordslite/blob/main/cordslite/core.py#L203"
target="_blank" style="float:right; font-size:smaller">source</a>

### User

``` python

def User(
    data, client
):

```

*`dict` subclass that also provides access to keys as attrs, and has a
pretty markdown repr*

``` python
mems = await gld.members(5); mems
```

<table class="prose" data-quarto-postprocess="true">
<thead>
<tr>
<th data-quarto-table-cell-role="th">ID</th>
<th data-quarto-table-cell-role="th">Name</th>
<th data-quarto-table-cell-role="th">Joined</th>
<th data-quarto-table-cell-role="th">Roles</th>
</tr>
</thead>
<tbody>
<tr>
<td>346450717025894400</td>
<td>nathan</td>
<td>2025-01-09</td>
<td>0</td>
</tr>
<tr>
<td>1327047896436178954</td>
<td>SearchBuddy</td>
<td>2025-01-09</td>
<td>1</td>
</tr>
<tr>
<td>1361823507679543306</td>
<td>DBuddy</td>
<td>2025-04-15</td>
<td>1</td>
</tr>
<tr>
<td>1448038710229733398</td>
<td>Dizcord Util Bot</td>
<td>2025-12-09</td>
<td>1</td>
</tr>
<tr>
<td>1467222191182712986</td>
<td>Search Agent</td>
<td>2026-01-31</td>
<td>1</td>
</tr>
</tbody>
</table>

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/cordslite/blob/main/cordslite/core.py#L239"
target="_blank" style="float:right; font-size:smaller">source</a>

### Channel.search_all

``` python

async def search_all(
    limit:int=500, delay:float=1.0, max_age_days:NoneType=None, show:bool=False
):

```

*Call self as a function.*

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/cordslite/blob/main/cordslite/core.py#L221"
target="_blank" style="float:right; font-size:smaller">source</a>

### Guild.search_all

``` python

async def search_all(
    limit:int=500, delay:float=1.0, max_age_days:NoneType=None, show:bool=False, kwargs:VAR_KEYWORD
):

```

*Paginated search returning up to `limit` messages*

``` python
# r = await ch.search_all()
# len(r)
```

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/cordslite/blob/main/cordslite/core.py#L255"
target="_blank" style="float:right; font-size:smaller">source</a>

### Channel.bulk_delete

``` python

async def bulk_delete(
    message_ids
):

```

*Bulk delete messages (must be \<14 days old)*

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/cordslite/blob/main/cordslite/core.py#L250"
target="_blank" style="float:right; font-size:smaller">source</a>

### Channel.delete_message

``` python

async def delete_message(
    message_id
):

```

*Delete a message by ID*

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/cordslite/blob/main/cordslite/core.py#L245"
target="_blank" style="float:right; font-size:smaller">source</a>

### Message.delete

``` python

async def delete(
    
):

```

*Delete this message*

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/cordslite/blob/main/cordslite/core.py#L267"
target="_blank" style="float:right; font-size:smaller">source</a>

### DiscordClient.thread

``` python

async def thread(
    thread_id
):

```

*Fetch a thread (which is a Channel)*

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/cordslite/blob/main/cordslite/core.py#L261"
target="_blank" style="float:right; font-size:smaller">source</a>

### Message.create_thread

``` python

async def create_thread(
    name
):

```

*Create a thread from this message*

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/cordslite/blob/main/cordslite/core.py#L283"
target="_blank" style="float:right; font-size:smaller">source</a>

### Channel.search_and_delete_all

``` python

async def search_and_delete_all(
    content, delay:int=2, show:bool=False, kwargs:VAR_KEYWORD
):

```

*Bulk delete recent msgs, individually delete older ones*

## Gateway API

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)

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/cordslite/blob/main/cordslite/core.py#L300"
target="_blank" style="float:right; font-size:smaller">source</a>

### GatewayClient

``` python

def GatewayClient(
    intents, client, token:NoneType=None
):

```

*Initialize self. See help(type(self)) for accurate signature.*

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

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

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

[`Op`](https://AnswerDotAI.github.io/cordslite/core.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/core.html#op.identify)
and
[`Op.heartbeat()`](https://AnswerDotAI.github.io/cordslite/core.html#op.heartbeat)
return properly structured payloads ready to send over the WebSocket.

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/cordslite/blob/main/cordslite/core.py#L318"
target="_blank" style="float:right; font-size:smaller">source</a>

### Op

``` python

def Op(
    args:VAR_POSITIONAL, kwargs:VAR_KEYWORD
):

```

*`dict` subclass that also provides access to keys as attrs, and has a
pretty markdown repr*

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/core.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`.

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/cordslite/blob/main/cordslite/core.py#L336"
target="_blank" style="float:right; font-size:smaller">source</a>

### Event

``` python

def Event(
    data, client
):

```

*`dict` subclass that also provides access to keys as attrs, and has a
pretty markdown repr*

------------------------------------------------------------------------

### ClientConnection.send

``` python

async def send(
    msg, kw:VAR_KEYWORD
):

```

*Call self as a function.*

`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/core.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.

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/cordslite/blob/main/cordslite/core.py#L355"
target="_blank" style="float:right; font-size:smaller">source</a>

### GatewayClient.recv_evt

``` python

async def recv_evt(
    
):

```

*Call self as a function.*

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.

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/cordslite/blob/main/cordslite/core.py#L362"
target="_blank" style="float:right; font-size:smaller">source</a>

### GatewayClient.on

``` python

def on(
    event_type, handler
):

```

*Call self as a function.*

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.

All reconnection funnels through `_reconnect`, which closes with code
4000 (keeping the session valid) and opens a new socket to
`resume_gateway_url`. The listener’s op 10 handler then takes over
automatically.

`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.

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/cordslite/blob/main/cordslite/core.py#L411"
target="_blank" style="float:right; font-size:smaller">source</a>

### GatewayClient.start

``` python

async def start(
    debug:bool=False
):

```

*Call self as a function.*

``` 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: 11
    sent 4000 (private use); then received 4000 (private use)
    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: Houston, do we have a problem?
    sent 1000 (OK); then received 1000 (OK)
    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?
    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 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 🦦')
```

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

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?')
```

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

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

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

`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.

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/cordslite/blob/main/cordslite/core.py#L419"
target="_blank" style="float:right; font-size:smaller">source</a>

### GatewayClient.stop

``` python

async def stop(
    
):

```

*Call self as a function.*

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

## Voice API

``` python
await gc.start()
vch = chs[3]; vch
```

    Connected! Session: 071532487214aa5877a8dcc277014457, heartbeat: 41250ms
    Gateway started!

    Channel(id=1327046393453613080, name='General', type=2)

Voice requires **three simultaneous connections**:

1.  **Main Gateway WebSocket** (`gc`) — to request joining a voice
    channel and receive voice server info
2.  **Voice Gateway WebSocket** — a *separate* WebSocket to a dedicated
    voice server for session coordination
3.  **Voice UDP** — a UDP (datagram) connection for actual audio data.
    UDP is used over TCP because real-time audio needs low latency more
    than guaranteed delivery

[`VoiceClient`](https://AnswerDotAI.github.io/cordslite/core.html#voiceclient)
manages the voice gateway WebSocket and UDP connections for a single
voice channel, coordinated through the main
[`GatewayClient`](https://AnswerDotAI.github.io/cordslite/core.html#gatewayclient).

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/cordslite/blob/main/cordslite/core.py#L426"
target="_blank" style="float:right; font-size:smaller">source</a>

### VoiceClient

``` python

def VoiceClient(
    gateway, channel
):

```

*Initialize self. See help(type(self)) for accurate signature.*

``` python
vc = VoiceClient(gc, vch); vc
```

    VoiceClient(self.ch=Channel(id=1327046393453613080, name='General', type=2), self.running=False)

Voice requires additional
[`Op`](https://AnswerDotAI.github.io/cordslite/core.html#op) opcodes
beyond the main gateway’s identify/heartbeat. These handle the
voice-specific protocol: requesting to join a channel (`voice_state`),
authenticating with the voice server (`voice_identify`), negotiating
encryption (`select_protocol`), signaling audio intent (`speaking`), and
keeping the voice connection alive (`voice_heartbeat`).

The voice heartbeat uses a different format from the main gateway — v8
requires `seq_ack` tracking the last received sequence number. It must
start **immediately** after receiving Hello, before UDP setup, or the
voice WebSocket will time out (close code 4006).

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/cordslite/blob/main/cordslite/core.py#L447"
target="_blank" style="float:right; font-size:smaller">source</a>

### Op.voice_heartbeat

``` python

def voice_heartbeat(
    seq_ack:int=-1
):

```

*Call self as a function.*

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/cordslite/blob/main/cordslite/core.py#L444"
target="_blank" style="float:right; font-size:smaller">source</a>

### Op.speaking

``` python

def speaking(
    ssrc, speaking:int=0
):

```

*Call self as a function.*

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/cordslite/blob/main/cordslite/core.py#L441"
target="_blank" style="float:right; font-size:smaller">source</a>

### Op.select_protocol

``` python

def select_protocol(
    ip, port, mode:str='aead_xchacha20_poly1305_rtpsize'
):

```

*Call self as a function.*

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/cordslite/blob/main/cordslite/core.py#L438"
target="_blank" style="float:right; font-size:smaller">source</a>

### Op.voice_identify

``` python

def voice_identify(
    server_id, user_id, session_id, token
):

```

*Call self as a function.*

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/cordslite/blob/main/cordslite/core.py#L435"
target="_blank" style="float:right; font-size:smaller">source</a>

### Op.voice_state

``` python

def voice_state(
    guild_id, channel_id
):

```

*Call self as a function.*

`_connect` handles the voice WebSocket handshake: request to join via
the main gateway, wait for Discord to assign a voice server, connect to
it, authenticate, and start the voice heartbeat. The heartbeat must
begin immediately after Hello — before UDP setup — or the voice
WebSocket times out (close code 4006).

``` python
await vc._connect(); vc
```

**IP Discovery**: Discord needs your *external* IP/port (what the
internet sees after NAT), not your local one. We send a 74-byte UDP
request containing our SSRC, and Discord responds with our external
address filled in:

<table>
<thead>
<tr>
<th>Field</th>
<th>Size</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td>Type</td>
<td>2 bytes</td>
<td>1=request, 2=response</td>
</tr>
<tr>
<td>Length</td>
<td>2 bytes</td>
<td>70 (size of remaining fields)</td>
</tr>
<tr>
<td>SSRC</td>
<td>4 bytes</td>
<td>Our audio stream identifier</td>
</tr>
<tr>
<td>Address</td>
<td>64 bytes</td>
<td>Blank in request; our external IP in response</td>
</tr>
<tr>
<td>Port</td>
<td>2 bytes</td>
<td>Blank in request; our external port in response</td>
</tr>
</tbody>
</table>

Note: Discord requires a **Speaking** event (even with `speaking=0`)
before it will send audio packets to you.

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/cordslite/blob/main/cordslite/core.py#L478"
target="_blank" style="float:right; font-size:smaller">source</a>

### get_ip

``` python

async def get_ip(
    trans, proto, ssrc
):

```

*Call self as a function.*

`_udp` sets up the audio transport: open UDP socket, discover our
external IP, tell Discord which encryption mode to use
(`select_protocol`), receive the secret key, and signal that we’re ready
to speak.

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/cordslite/blob/main/cordslite/core.py#L486"
target="_blank" style="float:right; font-size:smaller">source</a>

### VoiceUDP

``` python

def VoiceUDP(
    
):

```

*Interface for datagram protocol.*

``` python
await vc._udp()
```

``` python
# vc.secret
```

`join` ties the three phases together into a single call: connect to
voice server, set up UDP, ready to receive audio.

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/cordslite/blob/main/cordslite/core.py#L505"
target="_blank" style="float:right; font-size:smaller">source</a>

### VoiceClient.join

``` python

async def join(
    
):

```

*Call self as a function.*

``` python
await vc.join()
```

    Voice ready!

Audio arrives as UDP packets using the **RTP (Real-time Transport
Protocol)** format. Each packet contains: - **RTP header** (12+ bytes) —
version, sequence number, timestamp, SSRC (identifies which user is
speaking) - **Encrypted payload** — the Opus-encoded audio, encrypted
with the `secret_key` - **Nonce** (4 bytes at end) — used for decryption

Decryption with `aead_xchacha20_poly1305_rtpsize`: - The cipher expects
a 24-byte nonce, but only 4 bytes are transmitted — we pad with 20 zero
bytes - The unencrypted RTP header is used as AAD (Additional
Authenticated Data) — it’s verified but not encrypted - For `rtpsize`
mode, the AAD includes the 12-byte base header, any CSRC entries (4
bytes each, usually 0), and only the 4-byte extension *preamble* — the
extension *data* is part of the encrypted payload - After decryption, we
skip past the extension data (its length is in bytes 14-15 of the
original packet, multiplied by 4) to reach the raw Opus audio

Packets with byte\[1\] == `0x78` are RTP voice data. Other packets (like
`0xC9`) are RTCP control packets used for connection quality reporting —
we skip those.

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/cordslite/blob/main/cordslite/core.py#L511"
target="_blank" style="float:right; font-size:smaller">source</a>

### decrypt_pkt

``` python

def decrypt_pkt(
    pkt, secret
):

```

*Call self as a function.*

Recording writes each speaker to a separate file
(`recording_<ssrc>.mp3`) rather than mixing in real-time. This keeps the
code simple and gives us per-speaker files — ideal for transcription.
Each speaker gets their own Opus decoder because Opus is *stateful* —
feeding multiple speakers through one decoder corrupts its internal
state and produces garbled audio.

Silence padding ensures all files stay time-aligned: at the start
(padding from `_rec_start` to first packet), and during gaps (using RTP
timestamps, which Discord increments continuously even during silence).
Without this, files would be compressed in time and impossible to merge
or correlate.

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/cordslite/blob/main/cordslite/core.py#L524"
target="_blank" style="float:right; font-size:smaller">source</a>

### silence

``` python

def silence(
    n_smpls:int, # 2 bytes per sample (s16le)
):

```

*Call self as a function.*

`start_recording` drains any previously queued packets so recordings
start clean. `stop_recording` calls `communicate()` on each ffmpeg
process to flush buffered audio and cleanly close the output files —
without this, recordings may be truncated or corrupted.

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/cordslite/blob/main/cordslite/core.py#L570"
target="_blank" style="float:right; font-size:smaller">source</a>

### VoiceClient.stop_recording

``` python

def stop_recording(
    
):

```

*Call self as a function.*

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/cordslite/blob/main/cordslite/core.py#L561"
target="_blank" style="float:right; font-size:smaller">source</a>

### VoiceClient.start_recording

``` python

def start_recording(
    path:str='recording.mp3'
):

```

*Call self as a function.*

``` python
pth = '/tmp/recording.mp3'
vc.start_recording(path=pth)
await asyncio.sleep(10)
rpths = vc.stop_recording(); rpths
```

``` python
# Audio(rpths[0])
```

To leave a voice channel cleanly: 1. Stop the heartbeat loop 2. Close
the Voice WebSocket 3. Close the UDP transport 4. Send a Voice State
Update with `channel_id=None` on the main gateway — this tells Discord
to remove the bot from the voice channel. Without this, the bot appears
to stay in the channel until it times out.

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/cordslite/blob/main/cordslite/core.py#L578"
target="_blank" style="float:right; font-size:smaller">source</a>

### VoiceClient.leave

``` python

async def leave(
    
):

```

*Call self as a function.*

``` python
await vc.leave()
```

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

    Gateway stopped!

## Bots

[`Bot`](https://AnswerDotAI.github.io/cordslite/core.html#bot) ties
together
[`DiscordClient`](https://AnswerDotAI.github.io/cordslite/core.html#discordclient)
(REST) and
[`GatewayClient`](https://AnswerDotAI.github.io/cordslite/core.html#gatewayclient)
(events) into a single object with a decorator-based command router.
Commands are registered with `@bot.cmd` — the function name becomes the
command name, prefixed with `!` in Discord. So `def echo` responds to
`!echo`. Every command handler takes two arguments: the
[`Message`](https://AnswerDotAI.github.io/cordslite/core.html#message)
object and a string of everything the user typed after the command name
(empty string if nothing).

The `_on_msg` handler ignores messages from the bot itself to prevent
infinite loops — a common gotcha with Discord bots. It splits the
message into command name + args, so `!echo hello world` passes
`"hello world"` as the `args` string to the handler.

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/cordslite/blob/main/cordslite/core.py#L585"
target="_blank" style="float:right; font-size:smaller">source</a>

### Bot

``` python

def Bot(
    intents, kw:VAR_KEYWORD
):

```

*Discord bot with command routing*

``` python
bot = Bot(intents)
await bot.start()
```

    Connected! Session: aebd851f6f7e226c5f982f03cf7d27ef, heartbeat: 41250ms
    Gateway started!

Commands can be registered at any time — even after `bot.start()`. This
works because `@bot.cmd` just adds the function to a dict; the message
handler looks up commands dynamically on each message.

``` python
@bot.cmd
async def echo(msg, args): await (await msg.channel()).send(f'You said: {args}')
```

``` python
bot
```

    Bot(cmds=['echo'])

Errors in command handlers are caught and stored in `bot.errors` —
useful for debugging in dynamic environments like solveit where you can
inspect the list after the fact. For real-time handling (e.g. notifying
the user in Discord), register a handler with `@bot.on_error`. Both
mechanisms work simultaneously.

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/cordslite/blob/main/cordslite/core.py#L622"
target="_blank" style="float:right; font-size:smaller">source</a>

### Bot.on_error

``` python

def on_error(
    f
):

```

*Call self as a function.*

``` python
@bot.on_error
async def handle_err(msg, e): print('error')
```

``` python
@bot.cmd
def err(msg): raise Exception('test')
```

``` python
bot.errors
```

    []

Voice integration reuses the existing
[`VoiceClient`](https://AnswerDotAI.github.io/cordslite/core.html#voiceclient)
— [`Bot`](https://AnswerDotAI.github.io/cordslite/core.html#bot) just
provides convenience methods to manage the lifecycle. The bot can only
be in one voice channel at a time.

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/cordslite/blob/main/cordslite/core.py#L635"
target="_blank" style="float:right; font-size:smaller">source</a>

### Bot.leave_voice

``` python

async def leave_voice(
    
):

```

*Leave the current voice channel*

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/cordslite/blob/main/cordslite/core.py#L628"
target="_blank" style="float:right; font-size:smaller">source</a>

### Bot.join_voice

``` python

async def join_voice(
    channel
):

```

*Join a voice channel and return VoiceClient*

``` python
vc = await bot.join_voice(vch); vc
```

    Voice ready!

    VoiceClient(self.ch=Channel(id=1327046393453613080, name='General', type=2), self.running=True)

``` python
vc.start_recording()
```

    'recording.mp3'

``` python
pth = vc.stop_recording()
await bot.leave_voice()
```

``` python
# Audio(pth)
```
