Metadata-Version: 2.4
Name: pararamio-bot
Version: 3.0.14
Summary: Async webhook server for pararam.io bot events
Author: Pararamio Team
License: MIT
Project-URL: Homepage, https://gitlab.com/pararam-public/py-pararamio
Project-URL: Documentation, https://gitlab.com/pararam-public/py-pararamio/-/wikis/home
Project-URL: Repository, https://gitlab.com/pararam-public/py-pararamio
Project-URL: Issues, https://gitlab.com/pararam-public/py-pararamio/-/issues
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Internet :: WWW/HTTP
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.11
Description-Content-Type: text/markdown
Requires-Dist: aiohttp>=3.9.0
Requires-Dist: pydantic>=2.0.0
Provides-Extra: dev
Requires-Dist: pytest>=8.4.2; extra == "dev"
Requires-Dist: pytest-asyncio>=1.2.0; extra == "dev"
Requires-Dist: pytest-cov>=7.0.0; extra == "dev"
Requires-Dist: ruff>=0.13.2; extra == "dev"
Requires-Dist: mypy>=1.18.2; extra == "dev"

# pararamio-bot

Async webhook server for [pararam.io](https://pararam.io) bot events.

## Installation

```bash
pip install pararamio-bot
```

## Quick Start

```python
import asyncio
import os

from pararamio_bot import WebhookMessage, WebhookServer


async def on_message(message: WebhookMessage) -> None:
    print(f"@{message.user_unique_name}: {message.text}")


async def main() -> None:
    async with WebhookServer(bot_secret=os.environ["BOT_SECRET"], port=8080) as server:
        server.on_message(on_message)
        await asyncio.Event().wait()


asyncio.run(main())
```

Configure your bot's webhook URL in pararam.io to `http://<host>:8080/<url_key>`,
where `<url_key>` is the first 8 characters of your bot secret.

## Security

Every incoming request is verified in two steps:

1. **URL key** — the first 8 chars of the bot secret must be present in the request path. Requests with a wrong key get `404`.
2. **X-Signature** — HMAC-SHA256 signature of the request body, verified against the bot secret. Missing or invalid signature returns `401`.

Both checks are always enabled and cannot be disabled.

## API

### WebhookServer

```python
server = WebhookServer(
    bot_secret="...",  # Full bot secret from pararam.io
    host="0.0.0.0",    # Bind address (default)
    port=8080,          # Listen port (default)
)
```

**Context manager:**

```python
async with WebhookServer(bot_secret="...") as server:
    server.on_message(handler)
    # server is running
```

**Manual start/stop:**

```python
await server.start()
# ...
await server.stop()
```

### Callbacks

Register async callbacks for incoming events:

```python
server.on_message(callback)  # Called for each incoming message
server.on_action(callback)   # Called for bot:// link clicks
```

Multiple callbacks can be registered; they are called sequentially.

### Models

**WebhookMessage** — incoming message from a user:

| Field             | Type                     | Description                    |
|-------------------|--------------------------|--------------------------------|
| `user_id`         | `int`                    | Sender user ID                 |
| `user_unique_name`| `str`                    | Sender unique name             |
| `post_no`         | `int`                    | Post number in the chat        |
| `chat_id`         | `int`                    | Chat ID                        |
| `text`            | `str`                    | Message text                   |
| `organization_id` | `int \| None`            | Organization ID (optional)     |
| `text_parsed`     | `list[dict] \| None`     | Parsed text structure          |
| `reply_no`        | `int \| None`            | Replied-to post number         |
| `reply_text`      | `str \| None`            | Replied-to post text           |
| `file_guid`       | `str \| None`            | Attached file GUID             |
| `file_name`       | `str \| None`            | Attached file name             |
| `attachments`     | `list[str] \| None`      | List of attachment identifiers |

**WebhookAction** — user clicked a `bot://` link:

| Field             | Type              | Description                 |
|-------------------|-------------------|-----------------------------|
| `user_id`         | `int`             | User who clicked            |
| `chat_id`         | `int`             | Chat ID                     |
| `post_no`         | `int`             | Post number                 |
| `organization_id` | `int \| None`     | Organization ID (optional)  |
| `action`          | `str`             | Action name from the link   |
| `params`          | `dict[str, str]`  | Action parameters           |

### Utility Functions

```python
from pararamio_bot import extract_url_key, extract_api_key, make_signature, verify_signature

# Get the URL key (first 8 chars) from a bot secret
url_key = extract_url_key(bot_secret)

# Get the API key portion (chars 20+ for secrets > 50 chars)
api_key = extract_api_key(bot_secret)

# Compute / verify HMAC-SHA256 signatures
sig = make_signature(body, secret)
is_valid = verify_signature(body, sig, secret)
```

## HTTP Endpoints

| Route              | Description              |
|--------------------|--------------------------|
| `POST /{url_key}`     | Receive message webhooks |
| `POST /{url_key}/act` | Receive action webhooks  |

## Requirements

- Python >= 3.11
- aiohttp >= 3.9
- pydantic >= 2.0
