Metadata-Version: 2.4
Name: wechatanybot-sdk
Version: 1.0.0
Summary: WeChat iLink Bot SDK for Python (wechatanybot) — async, typed, production-grade
License: MIT
Requires-Python: >=3.9
Requires-Dist: aiohttp>=3.9
Requires-Dist: cryptography>=42.0
Provides-Extra: dev
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
Requires-Dist: pytest>=8.0; extra == 'dev'
Description-Content-Type: text/markdown

# wechatanybot-sdk — Python SDK

WeChat iLink Bot SDK for Python — async, typed, production-grade.

## Install

```bash
pip install wechatanybot-sdk
```

Requires Python ≥ 3.9. Dependencies: `aiohttp`, `cryptography`.

## Quick Start

```python
from wechatanybot import WeChatAnyBot

bot = WeChatAnyBot()

@bot.on_message
async def handle(msg):
    await bot.send_typing(msg.user_id)
    await bot.reply(msg, f"Echo: {msg.text}")

bot.run()  # login + start in one call
```

Or with async control:

```python
import asyncio
from wechatanybot import WeChatAnyBot

async def main():
    bot = WeChatAnyBot()
    await bot.login()

    @bot.on_message
    async def handle(msg):
        await bot.reply(msg, f"Echo: {msg.text}")

    await bot.start()

asyncio.run(main())
```

## Configuration

```python
bot = WeChatAnyBot(
    base_url="https://ilinkai.weixin.qq.com",   # default
    cred_path="~/.wechatanybot/credentials.json", # default
    on_qr_url=lambda url: print(f"Scan: {url}"),
    on_scanned=lambda: print("Scanned!"),
    on_expired=lambda: print("Expired..."),
    on_error=lambda err: print(f"Error: {err}"),
)
```

## API Reference

| Method | Description |
|---|---|
| `await bot.login(force=False)` | QR login (auto-skips if creds exist) |
| `await bot.start()` | Start long-poll loop |
| `bot.run()` | Sync: login + start |
| `bot.stop()` | Stop gracefully |
| `bot.on_message(handler)` | Register handler (also works as decorator) |
| `await bot.reply(msg, text)` | Reply (auto context_token + stop typing) |
| `await bot.send(user_id, text)` | Send to user (needs prior context) |
| `await bot.send_typing(user_id)` | Show "typing..." indicator |
| `await bot.stop_typing(user_id)` | Cancel typing indicator |
| `await bot.reply_media(msg, content)` | Reply with media (image, file, video) |
| `await bot.send_media(user_id, content)` | Send media to user (needs prior context) |
| `await bot.download(msg)` | Download media from incoming message |
| `await bot.download_raw(media, aeskey)` | Download and decrypt a raw CDN media reference |
| `await bot.upload(data, user_id, media_type)` | Upload file to CDN (does not send) |

## Media Operations

### Sending Media

```python
# Reply with an image
await bot.reply_media(msg, {"image": png_bytes})

# Reply with a file
await bot.reply_media(msg, {"file": data, "file_name": "report.pdf"})

# Reply with a video
await bot.reply_media(msg, {"video": mp4_bytes, "caption": "Check this"})

# Send media proactively (needs prior context_token)
await bot.send_media(user_id, {"image": png_bytes})
```

### Downloading Media

```python
@bot.on_message
async def handle(msg):
    # Auto-detect and download (priority: image > file > video > voice)
    media = await bot.download(msg)
    if media:
        print(f"Type: {media.type}, Size: {len(media.data)} bytes")
        if media.file_name:
            print(f"Filename: {media.file_name}")

    # Or download a raw CDN reference directly
    if msg.images:
        raw = await bot.download_raw(msg.images[0].media, msg.images[0].aes_key)
```

### Uploading to CDN

```python
# Upload without sending — returns UploadResult with CDN metadata
result = await bot.upload(file_bytes, user_id, media_type=3)
```

## Message Types

```python
@dataclass
class IncomingMessage:
    user_id: str
    text: str
    type: Literal["text", "image", "voice", "file", "video"]
    timestamp: datetime
    images: list[ImageContent]
    voices: list[VoiceContent]
    files: list[FileContent]
    videos: list[VideoContent]
    quoted_message: QuotedMessage | None
    raw: dict
```

## AES-128-ECB Crypto

```python
from wechatanybot import (
    generate_aes_key, encrypt_aes_ecb, decrypt_aes_ecb, decode_aes_key
)

key = generate_aes_key()
ct = encrypt_aes_ecb(b"Hello", key)
pt = decrypt_aes_ecb(ct, key)

# Decode protocol key (all 3 formats)
k = decode_aes_key("ABEiM0RVZneImaq7zN3u/w==")       # base64(raw)
k = decode_aes_key("00112233445566778899aabbccddeeff") # hex
```

## Project Structure

```
python/
├── wechatanybot/
│   ├── __init__.py      ← Public exports
│   ├── client.py        ← WeChatAnyBot (login, start, reply, send)
│   ├── protocol.py      ← Raw iLink API calls
│   ├── auth.py          ← QR login + credential persistence
│   ├── types.py         ← All types (dataclasses)
│   ├── errors.py        ← Error hierarchy
│   └── crypto.py        ← AES-128-ECB encrypt/decrypt
├── examples/
│   └── echo_bot.py
├── tests/
│   ├── test_crypto.py   ← 10 tests
│   └── test_client.py   ← 8 tests
└── pyproject.toml
```

## Testing

```bash
pip install -e ".[dev]"
pytest
```

## License

MIT
