Metadata-Version: 2.4
Name: cordless
Version: 0.2.1
Summary: Serverless Discord interactions framework for AWS Lambda
Author: borhara
Project-URL: Homepage, https://github.com/borhara/cordless
Classifier: Programming Language :: Python :: 3
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Requires-Python: >=3.10
Description-Content-Type: text/markdown
Provides-Extra: dev
Requires-Dist: pytest>=8.0; extra == "dev"
Requires-Dist: pynacl>=1.5; extra == "dev"

# 📡 Cordless

> **A serverless Discord interactions framework for AWS Lambda**

Cordless lets you build Discord bots without running a server — just functions, deployed to Lambda.

* **No WebSockets**
* **No stateful runtime**
* **No gateway sharding**

Just **HTTP → functions → responses**.

## ✨ Why Cordless?

Traditional Discord bots require:
* persistent servers
* WebSocket connections
* intent configuration
* runtime state management

Cordless flips that model:
> Discord sends events → AWS Lambda runs your code → you return a response

## ⚡ Core Idea

```text
Discord Interaction
      │
      ▼
API Gateway
      │
      ▼
 AWS Lambda
      │
      ▼
Cordless Router
      │
      ▼
 Your Functions
      │
      ▼
JSON Response back to Discord
```

## 🚀 Quickstart

### Install

```bash
pip install cordless
```

### Create your first bot

```python
from cordless import Cordless

bot = Cordless()

@bot.command("ping")
async def ping(ctx):
    await ctx.send("pong")
```

### Lambda entry point

```python
import os
from cordless import Cordless

bot = Cordless(public_key=os.environ["DISCORD_PUBLIC_KEY"])

@bot.command("ping")
async def ping(ctx):
    await ctx.send("pong")

def handler(event, context):
    return bot.handle(event)
```

## 🔒 Request verification

Every request Discord sends to your endpoint is signed with Ed25519. Pass your
application's **public key** (from the Discord Developer Portal) to `Cordless()`
and every incoming request is verified before your handlers ever run —
requests with a missing or invalid signature are rejected with `401` and never
reach your code.

```python
bot = Cordless(public_key=os.environ["DISCORD_PUBLIC_KEY"])
```

`PING` interactions, which Discord sends when you first configure your
endpoint URL, are answered automatically.

> Omitting `public_key` skips verification — useful for local testing, but
> **never deploy without it**: anyone who finds your Lambda URL could otherwise
> forge interactions.

## 🗒️ Registering commands with Discord

`@bot.command(...)` only wires up local dispatch — Discord also needs to know
your commands exist so it can show them in the client. Give each command a
description (and options, if it takes arguments):

```python
@bot.command(
    "echo",
    description="Repeats what you say",
    options=[
        {"name": "text", "description": "Text to repeat", "type": 3, "required": True},
    ],
)
async def echo(ctx):
    await ctx.send(ctx.options["text"])
```

Then sync them to Discord with the `cordless` CLI (installed alongside the
package) — run this once after deploying, and again whenever a command's
shape changes. Point it at `MODULE:ATTRIBUTE`, wherever your `Cordless()`
instance lives:

```bash
export DISCORD_BOT_TOKEN=...

cordless register app:bot                       # global — every authorized guild, every user
cordless register app:bot --guild-id 123456789   # a single guild, for instant updates while developing
```

The application id is resolved from the bot token, so that's all you need to
provide. Omit `--guild-id` to register **globally**; global commands can take
up to an hour to propagate, so use `--guild-id` while iterating.

### No bot token? Use client credentials instead

If your app never needs a bot user — it only ever responds to HTTP
interactions, like everything cordless does — you don't need a bot token at
all. Discord also accepts an OAuth2 **client credentials** grant for managing
commands, authenticated with just your app's client ID and secret (from the
Developer Portal's OAuth2 page):

```bash
export DISCORD_CLIENT_ID=...
export DISCORD_CLIENT_SECRET=...

cordless register app:bot
```

If both a bot token and client credentials are available, the bot token
takes precedence.

Prefer calling it from code instead (e.g. inside a deploy script)? Use
`bot.sync_commands(bot_token=..., guild_id=...)` or
`bot.sync_commands(client_id=..., client_secret=..., guild_id=...)` directly
— it's what the CLI calls under the hood.

Command arguments show up on `ctx.options` as a plain dict, e.g. `ctx.options["text"]`.

## 🧩 Commands & Interactivity

### Commands

```python
@bot.command("hello")
async def hello(ctx):
    await ctx.send("Hello world!")
```

### Buttons

> **Note:** the `@bot.button(...)` decorator below works today. Sending
> button components (the `components=` argument and `cordless.ui.Button`
> class) is still in active development.

Send a button:

```python
from cordless.ui import Button

@bot.command("ping")
async def ping(ctx):
    await ctx.send(
        "pong",
        components=[
            Button(label="Edit", custom_id="edit_ping")
        ]
    )
```

Handle button clicks:

```python
@bot.button("edit_ping")
async def edit_ping(ctx):
    await ctx.edit("edited")
```

## 🧠 Key concepts

Stateless by design:
* **interaction payload**
* **custom_id routing**
* **Lambda invocation context**

No WebSocket required.

## 📦 Architecture

```text
src/cordless/
├── __init__.py
├── app.py
├── router.py
├── context.py
├── verify.py
├── register.py
├── errors.py
└── response/
    └── responder.py
```

## 💡 Philosophy

Cordless is built around one idea:

> Discord apps should feel like serverless functions, not servers.

