Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions Meshflow/ws/serializers.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from rest_framework import serializers

from common.protocol import Protocol
from nodes.models import ObservedNode
from text_messages.models import TextMessage

Expand All @@ -12,17 +13,27 @@ class Meta:

class TextMessageWSSerializer(serializers.ModelSerializer):
id = serializers.UUIDField(format="hex", read_only=True)
protocol = serializers.SerializerMethodField()
sender = ObservedNodeWSBriefSerializer(read_only=True)
channel = serializers.PrimaryKeyRelatedField(read_only=True)
sent_at = serializers.DateTimeField(format="%Y-%m-%dT%H:%M:%S.%fZ", read_only=True)
original_packet_id = serializers.SerializerMethodField()
original_mc_packet_id = serializers.SerializerMethodField()
heard = serializers.SerializerMethodField()

def get_protocol(self, obj):
return Protocol(obj.protocol).label.lower()

def get_original_mc_packet_id(self, obj):
return str(obj.original_mc_packet_id) if obj.original_mc_packet_id else None

class Meta:
model = TextMessage
fields = [
"id",
"protocol",
"original_packet_id",
"original_mc_packet_id",
"sender",
"recipient_meshtastic_node_id",
"channel",
Expand Down
52 changes: 52 additions & 0 deletions Meshflow/ws/tests/test_text_message_ws_serializer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
"""TextMessage WebSocket payload serialization."""

from django.utils import timezone

import pytest

from common.protocol import Protocol
from constellations.models import MessageChannel
from text_messages.models import TextMessage
from ws.serializers import TextMessageWSSerializer


@pytest.mark.django_db
def test_ws_serializer_includes_meshtastic_protocol(create_constellation, create_observed_node):
constellation = create_constellation()
channel = MessageChannel.objects.create(name="LongFast", constellation=constellation)
sender = create_observed_node()
message = TextMessage.objects.create(
protocol=Protocol.MESHTASTIC,
sender=sender,
channel=channel,
sent_at=timezone.now(),
message_text="hello mt",
)

data = TextMessageWSSerializer(message).data

assert data["protocol"] == "meshtastic"
assert data["channel"] == channel.id


@pytest.mark.django_db
def test_ws_serializer_includes_meshcore_protocol(create_constellation):
constellation = create_constellation()
channel = MessageChannel.objects.create(
name="Public",
constellation=constellation,
protocol=Protocol.MESHCORE,
)
message = TextMessage.objects.create(
protocol=Protocol.MESHCORE,
sender=None,
channel=channel,
sent_at=timezone.now(),
message_text="hello mc",
)

data = TextMessageWSSerializer(message).data

assert data["protocol"] == "meshcore"
assert data["channel"] == channel.id
assert data["original_mc_packet_id"] is None
3 changes: 3 additions & 0 deletions docs/features/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,9 @@ the mesh. Because feeders are geographically distributed, Meshflow can show
both sides of conversations even when no single radio could hear them all.
Realtime updates are pushed over WebSocket.

Reference: [text-messages/](text-messages/) (REST, WS fan-out, unread/badge
contract with meshflow-ui); MeshCore ingest [meshcore/text-message-channels.md](meshcore/text-message-channels.md).

### Mesh infrastructure monitoring

A dedicated page surfacing the health of every node with an infrastructure
Expand Down
80 changes: 80 additions & 0 deletions docs/features/text-messages/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# Text messages

Meshflow stores normalised **text messages** from Meshtastic and MeshCore feeders: public channel chat, emoji reactions, and (Meshtastic) threaded replies. Rows live in the `text_messages` Django app; ingest paths differ by protocol but both emit the same `text_message_received` signal for WebSocket fan-out.

This folder documents cross-cutting text-message behaviour. MeshCore-specific ingest and channel modelling remain under [meshcore/text-message-channels.md](../meshcore/text-message-channels.md) and [meshcore/mc-channel-sync/](../meshcore/mc-channel-sync/).

## Implementation status

| Area | Status | Notes |
| --- | --- | --- |
| REST list/detail `GET /api/messages/text/` | Shipped | `protocol`, `channel_id`, `constellation_id`, pagination |
| `TextMessage.protocol` on DB row | Shipped | `Protocol.MESHTASTIC` / `Protocol.MESHCORE` |
| WebSocket `/ws/messages/` | Shipped | JWT; Redis group `text_messages` |
| WS payload `protocol` | Shipped | `TextMessageWSSerializer` (see [unread-count.md](unread-count.md)) |
| Server-side unread / read receipts | Not implemented | Unread is entirely client-side in meshflow-ui |

## Documentation map

| Doc | Purpose |
| --- | --- |
| [unread-count.md](unread-count.md) | Realtime push, WS serializer fields, UI nav badges ([#279](https://github.com/pskillen/meshflow-ui/issues/279)) |
| [../meshcore/text-message-channels.md](../meshcore/text-message-channels.md) | MC ingest, channels, sender inference |
| [../packet-ingestion/meshtastic.md](../packet-ingestion/meshtastic.md) | MT `TEXT_MESSAGE_APP` → `TextMessage` |

## Concepts

- **`TextMessage`** — business row with `protocol`, optional `sender`, `channel` FK, provenance via `original_packet` (MT) or `original_mc_packet` (MC).
- **`text_message_received`** — Django signal (`packets.signals`) fired after a row is created; `ws.receivers` pushes to connected UI clients.
- **Unread** — not stored in Postgres; the SPA keeps an in-memory list keyed by protocol (see UI doc).

## Flow (high level)

```mermaid
sequenceDiagram
participant Ingest as packets_or_meshcore_packets
participant TM as TextMessage
participant Sig as text_message_received
participant WS as TextMessageWebSocketNotifier
participant Redis as channel_layer_text_messages
participant UI as meshflow_ui

Ingest->>TM: create row (protocol set on model)
Ingest->>Sig: send(message, observer)
Sig->>WS: notify(message)
WS->>Redis: group_send text_messages
Redis->>UI: JSON frame on /ws/messages/
```

## HTTP API

- **List:** `GET /api/messages/text/` — filter `protocol=meshtastic|meshcore`, `channel_id`, `constellation_id`, `sender_node_id`; paginated.
- **OpenAPI:** `TextMessage` schema includes `protocol` (`MeshProtocol` string).
- **Auth:** JWT (same as rest of API).

## WebSocket

- **Path:** `/ws/messages/?token={jwt}` — see [openapi.yaml](../../../openapi.yaml) and [REDIS.md](../../REDIS.md) (Channels group `text_messages`).
- **Consumer:** `ws.consumers.TextMessageConsumer` — adds socket to group `text_messages`; handler `text_message` forwards `event["message"]` as JSON.
- **Notifier:** `ws.services.text_message.TextMessageWebSocketNotifier` serializes with `TextMessageWSSerializer`.

## Consumers

| Consumer | Use |
| --- | --- |
| meshflow-ui `WebSocketProvider` | Nav unread badges, toasts when off messages page |
| meshflow-ui `useMessagesWithWebSocket` | Prepend to active channel list on messages page |
| (none server-side) | No unread persistence |

## Related issues

| Issue | Repo | Topic |
| --- | --- | --- |
| [#279](https://github.com/pskillen/meshflow-ui/issues/279) | meshflow-ui | Protocol-scoped unread nav badges |
| [#341](https://github.com/pskillen/meshflow-api/issues/341) | meshflow-api | Messages UI epic (parent) |
| [#277](https://github.com/pskillen/meshflow-ui/issues/277)–[#281](https://github.com/pskillen/meshflow-ui/issues/281) | meshflow-ui | Picker / layout rework |

## Cross-repo docs

- UI messages feature hub: [meshflow-ui `docs/features/messages/README.md`](https://github.com/pskillen/meshflow-ui/blob/main/docs/features/messages/README.md)
- UI legacy overview: [meshflow-ui `docs/messages/`](https://github.com/pskillen/meshflow-ui/blob/main/docs/messages/README.md)
55 changes: 55 additions & 0 deletions docs/features/text-messages/unread-count.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# Text messages — unread count (WebSocket → UI)

**Purpose:** How new text messages reach the browser for realtime UI and unread badges. Unread state is **client-only** in meshflow-ui.

**UI counterpart:** [meshflow-ui `docs/features/messages/unread-count.md`](https://github.com/pskillen/meshflow-ui/blob/main/docs/features/messages/unread-count.md)

**Tracking:** [meshflow-ui#279](https://github.com/pskillen/meshflow-ui/issues/279). Deferred UI rollup: [meshflow-api#396](https://github.com/pskillen/meshflow-api/issues/396). Epic: [#341](https://github.com/pskillen/meshflow-api/issues/341).

---

## What the API does (and does not do)

| Responsibility | Owner |
| --- | --- |
| Persist `TextMessage` with correct `protocol` | `text_messages` + ingest |
| Broadcast new rows to UI clients | `ws` via Channels group `text_messages` |
| Unread / read receipts | **Not implemented** — SPA only |

---

## Code anchors

| Piece | Path |
| --- | --- |
| Model `protocol` | `Meshflow/text_messages/models.py` |
| REST serializer | `Meshflow/text_messages/serializers.py` |
| WS serializer | `Meshflow/ws/serializers.py` — `TextMessageWSSerializer` (includes `protocol`) |
| WS notify | `Meshflow/ws/services/text_message.py` |
| Tests | `Meshflow/ws/tests/test_text_message_ws_serializer.py` |

---

## WebSocket payload shape

`TextMessageWSSerializer` includes `protocol` (`meshtastic` / `meshcore`, same labels as REST). Also: `id`, `channel`, `sender`, `sent_at`, `message_text`, `is_emoji`, `reply_to_meshtastic_packet_id`, `original_packet_id`, `heard` (MT prefetch when available).

Still narrower than REST for MC helper fields (`mc_sender_label`, `mc_sender_candidates`).

OpenAPI documents WS frames as the `TextMessage` schema; `protocol` is required for correct UI classification ([#279](https://github.com/pskillen/meshflow-ui/issues/279)).

---

## Known gaps

- MC sender helper fields not on WS payload.
- `TextMessageConsumer` uses `print` when forwarding (log noise).
- UI dedupes by message `id`; API does not suppress duplicate pushes.

---

## Related

- [README.md](README.md)
- [meshflow-ui unread-count.md](https://github.com/pskillen/meshflow-ui/blob/main/docs/features/messages/unread-count.md)
- [#396](https://github.com/pskillen/meshflow-api/issues/396) — multi-constellation unread rollup (UI)