Metadata-Version: 2.4
Name: stapel-chat
Version: 0.7.4
Summary: Conversations, messaging and support chat for the Stapel framework
License: MIT
Project-URL: Homepage, https://github.com/usestapel/stapel-chat
Project-URL: Repository, https://github.com/usestapel/stapel-chat
Project-URL: Documentation, https://github.com/usestapel/stapel-chat#readme
Project-URL: Changelog, https://github.com/usestapel/stapel-chat/blob/main/CHANGELOG.md
Project-URL: Issues, https://github.com/usestapel/stapel-chat/issues
Keywords: django,stapel,chat,messaging,support
Classifier: Development Status :: 3 - Alpha
Classifier: Framework :: Django
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Typing :: Typed
Requires-Python: >=3.11
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: stapel-core<1.0,>=0.45.0
Requires-Dist: stapel-realtime<1.0,>=0.1.2
Provides-Extra: realtime
Requires-Dist: stapel-realtime[channels]<1.0,>=0.1.2; extra == "realtime"
Provides-Extra: channels
Requires-Dist: stapel-realtime[channels]<1.0,>=0.1.2; extra == "channels"
Provides-Extra: all
Requires-Dist: stapel-realtime[channels]<1.0,>=0.1.2; extra == "all"
Provides-Extra: test
Requires-Dist: pytest; extra == "test"
Requires-Dist: pytest-django; extra == "test"
Requires-Dist: pytest-asyncio; extra == "test"
Requires-Dist: pytest-cov; extra == "test"
Requires-Dist: channels; extra == "test"
Requires-Dist: daphne; extra == "test"
Requires-Dist: stapel-tools<1,>=0.9.1; extra == "test"
Requires-Dist: stapel-moderation<1.0,>=0.3.1; extra == "test"
Requires-Dist: stapel-cdn<1.0,>=0.16.0; extra == "test"
Dynamic: license-file

<!-- Generated by stapel-readme from docs/readme.md + docs/*.json. Do not edit this file; edit docs/readme.md and re-run `make readme`. -->

# stapel-chat

[![CI](https://img.shields.io/github/actions/workflow/status/usestapel/stapel-chat/ci.yml?branch=main&logo=github&label=CI)](https://github.com/usestapel/stapel-chat/actions/workflows/ci.yml?query=branch%3Amain)
[![coverage](https://img.shields.io/codecov/c/github/usestapel/stapel-chat?branch=main&logo=codecov&label=coverage)](https://app.codecov.io/gh/usestapel/stapel-chat)
[![pypi](https://img.shields.io/pypi/v/stapel-chat?logo=pypi&logoColor=white&label=pypi)](https://pypi.org/project/stapel-chat/)
[![downloads](https://static.pepy.tech/badge/stapel-chat/month)](https://pepy.tech/project/stapel-chat)
[![python](https://img.shields.io/pypi/pyversions/stapel-chat?logo=python&logoColor=white)](https://pypi.org/project/stapel-chat/)
[![license](https://img.shields.io/github/license/usestapel/stapel-chat)](https://github.com/usestapel/stapel-chat/blob/main/LICENSE)
[![llms.txt](https://img.shields.io/badge/llms.txt-blue)](https://github.com/usestapel/stapel-chat/blob/main/docs/llms.txt)

> Conversations and messaging over WebSocket: direct (1:1, idempotent by participant pair), group and support threads on one model; realtime send/edit/delete with a monotonic per-conversation seq for order and a separate revision sequence for resume, so an edit or a deletion made while a client was offline reaches it on reconnect; deletion is a tombstone the id keeps arriving under, so client caches and offline databases learn what to purge; attachments carry the render metadata a bubble needs on first paint (aspect, byte size, a 16px base64 thumbnail, voice duration and waveform, document mime and extension) behind an OPEN type registry; typing and activity states, read and delivery receipts as ephemeral signals; a live inbox stream so the conversation list does not poll either; anchor-paginated history and lists; and a support layer (queue, first-come assignment, open/pending/resolved with reopen).

Part of the [Stapel framework](https://github.com/usestapel) — composable Django apps that deploy as a monolith or as microservices without changing module code.

## Install

```bash
pip install stapel-chat
```

## At a glance

| Fact | Value |
|---|---|
| Version | `0.7.4` |
| Python | `>=3.11` (3.11, 3.12, 3.13, 3.14) |
| HTTP operations | 13 |
| Config axes | 15 |
| Usage surface | 21 |
| Extension points | 9 |
| Error codes | 65 |
| Fleet dependencies | [`stapel-auth`](https://github.com/usestapel/stapel-auth) (optional) · [`stapel-cdn`](https://github.com/usestapel/stapel-cdn) (optional) · [`stapel-core`](https://github.com/usestapel/stapel-core) · [`stapel-realtime`](https://github.com/usestapel/stapel-realtime) |

## Documentation

[OpenAPI](https://github.com/usestapel/stapel-chat/blob/main/docs/schema.json) · [capabilities.json](https://github.com/usestapel/stapel-chat/blob/main/docs/capabilities.json) · [llms.txt (for agents)](https://github.com/usestapel/stapel-chat/blob/main/docs/llms.txt)

One model backs three kinds of thread: **direct** (1:1, idempotent by
participant pair), **group**, and **support** (a customer↔operator thread with a
queue and assignment lifecycle).

**Realtime is the module, not a mode of it.** Messages are sent and received
over a WebSocket; REST serves history, hydration and the support lifecycle.
A deployment that cannot serve the socket fails `manage.py check` rather than
degrading into a product that refreshes on a timer — because a polling fallback
a product can end up in silently is exactly how "websockets are done" became a
false claim once already.

## Quick start

```python
INSTALLED_APPS = [
    # ...
    "stapel_core.django.apps.CommonDjangoConfig",
    "stapel_core.django.users",
    "rest_framework",
    "stapel_realtime",
    "stapel_chat",
]

CHANNEL_LAYERS = {"default": {
    "BACKEND": "channels_redis.core.RedisChannelLayer",
    "CONFIG": {"hosts": [REDIS_URL]},
}}
STAPEL_COMM = {"SIGNAL_TRANSPORT": "channels"}
STAPEL_REALTIME = {"ALLOWED_ORIGINS": ["https://app.example.com"]}  # with the port

# urls.py
urlpatterns = [path("chat/", include("stapel_chat.urls"))]
```

```python
# asgi.py — the whole file
from django.core.asgi import get_asgi_application
from stapel_realtime.asgi import build_websocket_application

application = build_websocket_application(http_application=get_asgi_application())
```

```
pip install 'stapel-chat[realtime]'
```

## What you get

- **Two sockets.** `ws/chat/<conversation_id>` is the resumable journal —
  `hello{last_seq}` → replay → live, with `send` / `edit` / `delete` / `read` /
  `delivered` / `activity` frames going through the same service layer the REST
  views call. `ws/chat/inbox` keeps the conversation list live, because a list
  with no socket refreshes on a timer forever however live the open thread is.
- **Two sequences.** `seq` is a message's immutable place in the thread — the
  sort key and the history anchor. `rev_seq` is its place in the revision
  journal, re-allocated on every edit and delete, and it is what realtime
  replay is anchored on: an edit made while a client was offline arrives in the
  catch-up. A client upserts by id, sorts by `seq`, and remembers `rev_seq` as
  its cursor.
- **Edit and delete.** An edit sets `edited` / `edited_at`. A delete leaves a
  **tombstone**: the id keeps being delivered with `body: ""`,
  `attachments: []` and `deleted: true`, so a client cache learns which id to
  purge. An id that stops arriving is an id nobody can purge. Retention is
  permanent.
- **Attachments that render on first paint** — aspect, byte size and a ~16px
  base64 thumbnail for images and GIFs; duration and a waveform image for audio;
  mime and extension for documents; poster and duration for video. The type set
  is an **open registry** — stickers are a settings line — and the metadata comes
  from `stapel-cdn` by comm, once, at send time.
- **Receipts and activity.** Separate delivery and read markers, both durable
  and both fanned out live; `typing` / `recording_audio` / `sending_video` /
  `uploading_file` as ephemeral signals with a TTL, from another open registry.
- **Conversations** — `POST /chat/api/v1/conversations` (`direct` / `group` /
  `support`); direct is get-or-create by participant pair. `GET` lists yours
  (anchor-paginated) with `unread_count`, and every row carries its own
  `stream_key` and `socket_path`.
- **Messages** — `GET/POST /chat/api/v1/conversations/{id}/messages`,
  `PATCH/DELETE .../messages/{message_id}`. History is anchored on `seq`,
  newest-first, both directions.
- **Support** — `GET /chat/api/v1/support/queue`,
  `POST .../support/conversations/{id}/{assign,resolve,reopen}`.

## Configuration (`STAPEL_CHAT`)

| Key | Default | Meaning |
|---|---|---|
| `CHAT_KINDS` | `["direct","group","support"]` | Enabled thread kinds |
| `ATTACHMENTS` | `True` | Allow attachments on messages |
| `MAX_BODY_LENGTH` | `4000` | Hard cap on a text body |
| `ATTACHMENT_TYPES` | `{}` | Open registry, merged over `image/gif/video/audio/file` — the same names stapel-cdn uses |
| `ACTIVITY_STATES` | `{}` | Open registry, merged over `typing/recording_audio/…` |
| `ATTACHMENT_METADATA` | `"cdn"` | Ask `cdn.describe`, or trust the client |
| `MAX_ATTACHMENTS` | `10` | Attachments per message |
| `MAX_PREVIEW_B64_BYTES` | `8192` | Ceiling on an inline `data:` preview |
| `EDIT_WINDOW_S` | `0` | Seconds a message stays editable (0 = forever) |
| `SCOPE_PROVIDER` | `stapel_chat.scope.DefaultScopeProvider` | Resolve/enforce the opaque `scope_key` |

There is no key that turns realtime off. See
[MODULE.md](https://github.com/usestapel/stapel-chat/blob/main/MODULE.md) for
the full wire contract, the extension seams and the anti-patterns.

## License

MIT — see [LICENSE](https://github.com/usestapel/stapel-chat/blob/main/LICENSE).

---

<sub>This page is assembled by `stapel-readme` from `docs/readme.md` plus the contract artifacts in `docs/`. Edit the prose in `docs/readme.md`; the badges, facts and links above and below it are generated — do not hand-edit `README.md`.</sub>
