Metadata-Version: 2.4
Name: abstract_chatshare
Version: 0.0.3
Summary: Login-gated chat + file-share + WebRTC video backend for abstractendeavors.com. Single-process asyncio websockets service over Postgres (auth_v2), minimal deps.
Home-page: https://github.com/AbstractEndeavors/abstract_chatshare
Author: putkoff
Author-email: partners@abstractendeavors.com
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Topic :: Communications :: Chat
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Requires-Python: >=3.10
Description-Content-Type: text/markdown
Requires-Dist: websockets>=17
Requires-Dist: psycopg[binary]>=3.1
Dynamic: author
Dynamic: author-email
Dynamic: classifier
Dynamic: description
Dynamic: description-content-type
Dynamic: home-page
Dynamic: requires-dist
Dynamic: requires-python
Dynamic: summary

# abstract_chatshare

Login-gated **chat + file-share + video** backend for abstractendeavors.com (route `/chat`).
It is the server half of the contract in [`PROTOCOL.md`](./PROTOCOL.md); the React frontend
(`@putkoff/abstract-chatshare`) is the other half.

The service is a **single asyncio process**: `websockets` for the WS/HTTP surface and `psycopg`
(3, async) over the shared `auth_v2` Postgres database. Presence, typing, call state and fan-out
live in memory; Postgres is the durable store. There is no web framework, no Redis, no connection
pooler — just a tiny internal `asyncio.Queue` pool of a few connections. The only third-party
dependencies are `websockets>=17` and `psycopg[binary]>=3.1`, plus self-hosted coturn for TURN.

## What it does
- **Discord-like rooms**: invite-only, owner/mod/member roles, bans, invite links (with optional
  guest registration through abstract_logins' `invites` table), DMs, reactions, edits, read
  markers, typing, soft-deletes, attachments backed by the existing Secure Files system.
- **Calls**: the server only relays WebRTC signaling (`rtc.signal`) between members of the same
  room call and issues ephemeral coturn REST credentials; media is a P2P mesh.
- **Bots**: a generic, Discord-style bot API (see below). The first bot, hugpy-ai, is built
  separately as a hugpy-side adapter against this API; this service knows nothing about it.

All of the wire protocol (auth handshake, objects, events, requests, rate limits) lives in
`PROTOCOL.md` and is implemented exactly.

## Install / run
```
pip install abstract_chatshare           # websockets + psycopg[binary]
abstract-chatshare migrate               # create/upgrade the chat_* tables (advisory-locked)
abstract-chatshare serve                 # listen on CHAT_HOST:CHAT_PORT (default 127.0.0.1:6016)
```
`serve` runs migrations on startup as well. Configuration is entirely environment-driven; see the
**Config** table in `PROTOCOL.md` and `deploy/chat.env.example`. The default DSN is
`postgresql:///auth_v2` (unix socket, peer auth as the `abstractendeavors` service user).

nginx proxies `location ^~ /chat/` to `127.0.0.1:6016` with the path preserved, so the service
sees `/chat/ws`, `/chat/bot`, `/chat/health`, `/chat/invite/...` and `/chat/files/...`. See
`deploy/` for the systemd unit, nginx snippet, env example and a hardened `turnserver.conf`.

## Module map (`src/abstract_chatshare/`)
| module | responsibility |
|---|---|
| `config.py` | env → immutable `Config`; wire limits + reaction set |
| `db.py` | tiny async pool, migrations (advisory-locked), every query (parameterized) |
| `auth.py` | handshake Origin/cookie/session/access checks; periodic re-check |
| `ice.py` | STUN list + coturn REST ephemeral TURN creds |
| `ratelimit.py` | per-user and per-IP sliding windows |
| `serialize.py` | rows → wire objects (User/Room/Member/Invite/Ban/Message/Bot), attachment re-resolution |
| `hub.py` | connection registry, membership cache, presence, raw fan-out, typing throttle |
| `calls.py` | in-memory call state + mesh signaling bookkeeping |
| `app.py` | shared state + high-level fan-out + the single `post_message` path |
| `handlers.py` | user request dispatch + validation |
| `bots.py` | bot invocations, bot request surface, mentions, command validation |
| `http.py` | public invite endpoints + bot-file serving (CORS, per-IP limits) |
| `server.py` | `websockets.serve` + `process_request` routing + the connection loops |
| `cli.py` | `serve`, `migrate`, `bot …` |

## Bot API (surface for the hugpy-ai adapter)
A bot is a **global** actor (available in every room and in DMs; never a `chat_members` row). It is
created by the operator and connects over a dedicated websocket. Build a hugpy-side adapter against
this surface; the service stays generic.

**Admin (operator CLI, token shown once, only its sha256 is stored):**
```
abstract-chatshare bot create <name> --display-name "hugpy-ai" [--description "..."]
    # prints  csb_<id>_<token>   (store it; it is never shown again)
abstract-chatshare bot list
abstract-chatshare bot rotate-token <name>      # prints a new token
abstract-chatshare bot enable|disable <name>
```
`name` matches `^[a-z0-9_-]{2,32}$` and is the `@name` used to mention the bot.

**Connection:** `ws://127.0.0.1:6016/chat/bot` with header `Authorization: Bot <token>`.
- A request carrying an `Origin` header is refused **403** (bot tokens must never be used from a
  browser). A bad/disabled token is **401**. One live connection per bot; a new one replaces the
  old (old closed **4409**). Max inbound frame 24 MiB (for `file.put`). Framing is identical to the
  user side (`{t, rid, ...}` requests → `{t:"res", ...}`; `{t:"ev", ...}` events).

**Privacy:** a bot receives only (a) messages that `@mention` it, (b) every message in its DM
room, and (c) slash commands addressed to it (`cmd.invoke`). It never sees other room traffic.
A bot may post in a room only in reply to an **invocation** from that room at most 30 min old, and
may edit/delete only its own messages, within 30 min of posting.

**Events to the bot:** `bot.ready`, `invoke` (`kind` ∈ mention|dm|command), `autocomplete`.
**Requests from the bot:** `commands.set`, `invocation.defer` (→ `bot.thinking`), `msg.send`
(normal or `ephemeral`), `msg.edit` (fan-out coalesced to ≤4/s), `msg.delete`, `typing`,
`file.put` (≤16 MiB, served from `GET /chat/files/{token}`), `autocomplete.result`.
See the **Bots** section of `PROTOCOL.md` for exact payloads and `CommandSpec`/`OptionSpec`.

**Note (clarification over PROTOCOL.md):** every non-ephemeral bot reply tied to an invocation
stores an `interaction` object (not only command invocations), so the frontend can clear the
`bot.thinking` placeholder for mention/dm replies too. For mention/dm, `interaction.command` is
`null`. This is a superset of the spec's requirement and was recorded in PROTOCOL.md.
