Metadata-Version: 2.4
Name: stapel-chat
Version: 0.8.5
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; leaving a thread, which hides it for the person who left and takes nothing away from anybody else (their messages, the other party's copy and their own history by id all stay) and un-hides on the next message written there; 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.8.5` |
| Python | `>=3.11` (3.11, 3.12, 3.13, 3.14) |
| HTTP operations | 14 |
| Config axes | 15 |
| Usage surface | 32 |
| 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`. `?search=` finds a thread by the three
  things its row draws — the counterpart's display name, the subject card's
  title and the last line — and `?unread=true` narrows to the rows with a
  badge; both filter before the page is taken, so the anchor keeps its meaning.
- **Leaving** — `DELETE /chat/api/v1/conversations/{id}` is the caller leaving,
  not a delete: the thread drops off *their* list, counts and search, their
  live subscription is revoked and a `chat.participant.left` system line
  records it — while every message, every other participant and their own
  history by id stay exactly as they were. A new message from the other side
  brings the thread back. Staff erasure is not on this surface: user data has
  one deletion path, `user.deleted`.
- **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>
