mirror of
https://github.com/l5yth/potato-mesh.git
synced 2026-03-28 17:42:48 +01:00
382 lines
11 KiB
Markdown
382 lines
11 KiB
Markdown
# potatomesh-matrix-bridge
|
||
|
||
A small Rust daemon that bridges **PotatoMesh** LoRa messages into a **Matrix** room.
|
||
|
||

|
||
|
||
For each PotatoMesh node, the bridge creates (or uses) a **Matrix puppet user**:
|
||
|
||
- Matrix localpart: `potato_` + the hex node id (without `!`), e.g. `!67fc83cb` → `@potato_67fc83cb:example.org`
|
||
- Matrix display name: the node’s `long_name` from the PotatoMesh API
|
||
|
||
Messages from PotatoMesh are periodically fetched and forwarded to a single Matrix room as those puppet users.
|
||
|
||
---
|
||
|
||
## Features
|
||
|
||
- Polls `https://potatomesh.net/api/messages` (deriving `/api` from the configured base domain)
|
||
- Looks up node metadata via `GET /api/nodes/{hex}` and caches it
|
||
- One Matrix user per node:
|
||
- username: `potato_{hex node id}`
|
||
- display name: `long_name`
|
||
- Forwards `TEXT_MESSAGE_APP` messages into a single Matrix room
|
||
- Persists last-seen message ID to avoid duplicates across restarts
|
||
|
||
---
|
||
|
||
## Architecture Overview
|
||
|
||
- **PotatoMesh side**
|
||
- `GET /api/messages` returns an array of messages
|
||
- `GET /api/nodes/{hex}` returns node metadata (including `long_name`)
|
||
|
||
- **Matrix side**
|
||
- Uses the Matrix Client-Server API with an **appservice access token**
|
||
- Impersonates puppet users via `user_id=@potato_{hex}:{server_name}&access_token={as_token}`
|
||
- Sends `m.room.message` events into a configured room
|
||
|
||
This is **not** a full appservice framework; it just speaks the minimal HTTP needed.
|
||
|
||
---
|
||
|
||
## Requirements
|
||
|
||
- Rust (stable) and `cargo`
|
||
- A Matrix homeserver you control (e.g. Synapse)
|
||
- An **application service registration** on your homeserver that:
|
||
- Whitelists the puppet user namespace (e.g. `@potato_[0-9a-f]{8}:example.org`)
|
||
- Provides an `as_token` the bridge can use
|
||
|
||
- Network access from the bridge host to:
|
||
- `https://potatomesh.net/` (bridge appends `/api`)
|
||
- Your Matrix homeserver (`https://matrix.example.org`)
|
||
|
||
---
|
||
|
||
## Configuration
|
||
|
||
Configuration can come from a TOML file, CLI flags, environment variables, or secret files. The bridge merges inputs in this order (highest to lowest):
|
||
|
||
1. CLI flags
|
||
2. Environment variables
|
||
3. Secret files (`*_FILE` paths or container defaults)
|
||
4. TOML config file
|
||
5. Container defaults (paths + poll interval)
|
||
|
||
If no TOML file is provided, required values must be supplied via CLI/env/secret inputs.
|
||
|
||
Example TOML:
|
||
|
||
```toml
|
||
[potatomesh]
|
||
# Base domain (bridge will call {base_url}/api)
|
||
base_url = "https://potatomesh.net/"
|
||
# Poll interval in seconds
|
||
poll_interval_secs = 10
|
||
|
||
[matrix]
|
||
# Homeserver base URL (client API) without trailing slash
|
||
homeserver = "https://matrix.example.org"
|
||
# Appservice access token (from your registration.yaml)
|
||
as_token = "YOUR_APPSERVICE_AS_TOKEN"
|
||
# Appservice homeserver token (must match registration hs_token)
|
||
hs_token = "SECRET_HS_TOKEN"
|
||
# Server name (domain) part of Matrix user IDs
|
||
server_name = "example.org"
|
||
# Room ID to send into (must be joined by the appservice / puppets)
|
||
room_id = "!yourroomid:example.org"
|
||
|
||
[state]
|
||
# Where to persist last seen message id
|
||
state_file = "bridge_state.json"
|
||
````
|
||
|
||
The `hs_token` is used to validate inbound appservice transactions. Keep it identical in `Config.toml` and your Matrix appservice registration file.
|
||
|
||
### CLI Flags
|
||
|
||
Run `potatomesh-matrix-bridge --help` for the full list. Common flags:
|
||
|
||
* `--config PATH`
|
||
* `--state-file PATH`
|
||
* `--potatomesh-base-url URL`
|
||
* `--potatomesh-poll-interval-secs SECS`
|
||
* `--matrix-homeserver URL`
|
||
* `--matrix-as-token TOKEN`
|
||
* `--matrix-as-token-file PATH`
|
||
* `--matrix-hs-token TOKEN`
|
||
* `--matrix-hs-token-file PATH`
|
||
* `--matrix-server-name NAME`
|
||
* `--matrix-room-id ROOM`
|
||
* `--container` / `--no-container`
|
||
* `--secrets-dir PATH`
|
||
|
||
### Environment Variables
|
||
|
||
* `POTATOMESH_CONFIG`
|
||
* `POTATOMESH_BASE_URL`
|
||
* `POTATOMESH_POLL_INTERVAL_SECS`
|
||
* `MATRIX_HOMESERVER`
|
||
* `MATRIX_AS_TOKEN`
|
||
* `MATRIX_AS_TOKEN_FILE`
|
||
* `MATRIX_HS_TOKEN`
|
||
* `MATRIX_HS_TOKEN_FILE`
|
||
* `MATRIX_SERVER_NAME`
|
||
* `MATRIX_ROOM_ID`
|
||
* `STATE_FILE`
|
||
* `POTATOMESH_CONTAINER`
|
||
* `POTATOMESH_SECRETS_DIR`
|
||
|
||
### Secret Files
|
||
|
||
If you supply `*_FILE` values, the bridge reads the secret contents and trims whitespace. When running inside a container, the bridge also checks the default secrets directory (default: `/run/secrets`) for:
|
||
|
||
* `matrix_as_token`
|
||
* `matrix_hs_token`
|
||
|
||
### Container Defaults
|
||
|
||
Container detection checks `POTATOMESH_CONTAINER`, `CONTAINER`, and `/proc/1/cgroup`. When detected (or forced with `--container`), defaults shift to:
|
||
|
||
* Config path: `/app/Config.toml`
|
||
* State file: `/app/bridge_state.json`
|
||
* Secrets dir: `/run/secrets`
|
||
* Poll interval: 15 seconds (if not otherwise configured)
|
||
|
||
Set `POTATOMESH_CONTAINER=0` or `--no-container` to opt out of container defaults.
|
||
|
||
### Docker Compose First Run
|
||
|
||
Before starting Compose, complete this preflight checklist:
|
||
|
||
1. Ensure `matrix/Config.toml` exists as a regular file on the host (not a directory).
|
||
2. Fill required Matrix values in `matrix/Config.toml`:
|
||
- `matrix.as_token`
|
||
- `matrix.hs_token`
|
||
- `matrix.server_name`
|
||
- `matrix.room_id`
|
||
- `matrix.homeserver`
|
||
|
||
This is required because the shared Compose anchor `x-matrix-bridge-base` mounts `./matrix/Config.toml` to `/app/Config.toml`.
|
||
Then follow the token and namespace requirements in [Matrix Appservice Setup (Synapse example)](#matrix-appservice-setup-synapse-example).
|
||
|
||
#### Troubleshooting
|
||
|
||
| Symptom | Likely cause | What to check |
|
||
| --- | --- | --- |
|
||
| `Is a directory (os error 21)` | Host mount source became a directory | `matrix/Config.toml` was missing at mount time and got created as a directory on host. |
|
||
| `M_UNKNOWN_TOKEN` / `401 Unauthorized` | Matrix appservice token mismatch | Verify `matrix.as_token` matches your appservice registration and setup in [Matrix Appservice Setup (Synapse example)](#matrix-appservice-setup-synapse-example). |
|
||
|
||
#### Recovery from accidental `Config.toml` directory creation
|
||
|
||
```bash
|
||
# from repo root
|
||
rm -rf matrix/Config.toml
|
||
touch matrix/Config.toml
|
||
# then edit matrix/Config.toml and set valid matrix.as_token, matrix.hs_token,
|
||
# matrix.server_name, matrix.room_id, and matrix.homeserver before starting compose
|
||
```
|
||
|
||
### PotatoMesh API
|
||
|
||
The bridge assumes:
|
||
|
||
* Messages: `GET {base_url}/api/messages` → JSON array, for example:
|
||
|
||
```json
|
||
[
|
||
{
|
||
"id": 2947676906,
|
||
"rx_time": 1764241436,
|
||
"rx_iso": "2025-11-27T11:03:56Z",
|
||
"from_id": "!da6556d4",
|
||
"to_id": "^all",
|
||
"channel": 1,
|
||
"portnum": "TEXT_MESSAGE_APP",
|
||
"text": "Ping",
|
||
"rssi": -111,
|
||
"hop_limit": 1,
|
||
"lora_freq": 868,
|
||
"modem_preset": "MediumFast",
|
||
"channel_name": "TEST",
|
||
"snr": -9.0,
|
||
"node_id": "!06871773"
|
||
}
|
||
]
|
||
```
|
||
|
||
* Nodes: `GET {base_url}/api/nodes/{hex}` → JSON, for example:
|
||
|
||
```json
|
||
{
|
||
"node_id": "!67fc83cb",
|
||
"short_name": "83CB",
|
||
"long_name": "Meshtastic 83CB",
|
||
"role": "CLIENT_HIDDEN",
|
||
"last_heard": 1764250515,
|
||
"first_heard": 1758993817,
|
||
"last_seen_iso": "2025-11-27T13:35:15Z"
|
||
}
|
||
```
|
||
|
||
Node hex ID is derived from `node_id` by stripping the leading `!` and using the remainder inside the puppet localpart prefix (`potato_{hex}`).
|
||
|
||
---
|
||
|
||
## Matrix Appservice Setup (Synapse example)
|
||
|
||
You need an appservice registration file (e.g. `potatomesh-bridge.yaml`) configured in Synapse.
|
||
|
||
A minimal example sketch (you **must** adjust URLs, secrets, namespaces):
|
||
|
||
```yaml
|
||
id: potatomesh-bridge
|
||
url: "http://your-bridge-host:41448"
|
||
as_token: "YOUR_APPSERVICE_AS_TOKEN"
|
||
hs_token: "SECRET_HS_TOKEN"
|
||
sender_localpart: "potatomesh-bridge"
|
||
rate_limited: false
|
||
namespaces:
|
||
users:
|
||
- exclusive: true
|
||
regex: "@potato_[0-9a-f]{8}:example.org"
|
||
```
|
||
|
||
This bridge listens for Synapse appservice callbacks on port `41448` so it can log inbound transaction payloads. It still only forwards messages one way (PotatoMesh → Matrix), so inbound Matrix events are acknowledged but not bridged. The `as_token` and `namespaces.users` entries remain required for outbound calls, and the `url` should point at the listener.
|
||
|
||
In Synapse’s `homeserver.yaml`, add the registration file under `app_service_config_files`, restart, and invite a puppet user to your target room (or use room ID directly).
|
||
|
||
The bridge validates inbound appservice callbacks by comparing the `access_token` query param to `hs_token` in `Config.toml`, so keep those values in sync.
|
||
|
||
---
|
||
|
||
## Build
|
||
|
||
```bash
|
||
# clone
|
||
git clone https://github.com/YOUR_USER/potatomesh-matrix-bridge.git
|
||
cd potatomesh-matrix-bridge
|
||
|
||
# build
|
||
cargo build --release
|
||
```
|
||
|
||
The resulting binary will be at:
|
||
|
||
```bash
|
||
target/release/potatomesh-matrix-bridge
|
||
```
|
||
|
||
---
|
||
|
||
## Docker
|
||
|
||
Build the container from the repo root with the included `matrix/Dockerfile`:
|
||
|
||
```bash
|
||
docker build -f matrix/Dockerfile -t potatomesh-matrix-bridge .
|
||
```
|
||
|
||
Provide your config at `/app/Config.toml` (or use CLI/env/secret overrides) and persist the bridge state file by mounting volumes. Minimal example:
|
||
|
||
```bash
|
||
docker run --rm \
|
||
-p 41448:41448 \
|
||
-v bridge_state:/app \
|
||
-v "$(pwd)/matrix/Config.toml:/app/Config.toml:ro" \
|
||
potatomesh-matrix-bridge
|
||
```
|
||
|
||
If you prefer to isolate the state file from the config, mount it directly instead of the whole `/app` directory:
|
||
|
||
```bash
|
||
docker run --rm \
|
||
-p 41448:41448 \
|
||
-v bridge_state:/app \
|
||
-v "$(pwd)/matrix/Config.toml:/app/Config.toml:ro" \
|
||
potatomesh-matrix-bridge
|
||
```
|
||
|
||
The image ships `Config.example.toml` for reference. If `/app/Config.toml` is absent, set the required values via environment variables, CLI flags, or secrets instead.
|
||
|
||
---
|
||
|
||
## Run
|
||
|
||
Ensure `Config.toml` is present and valid, then:
|
||
|
||
```bash
|
||
./target/release/potatomesh-matrix-bridge
|
||
```
|
||
|
||
Environment variables you may care about:
|
||
|
||
* `RUST_LOG` – for logging, e.g.:
|
||
|
||
```bash
|
||
RUST_LOG=info,reqwest=warn ./target/release/potatomesh-matrix-bridge
|
||
```
|
||
|
||
The bridge will:
|
||
|
||
1. Load state from `bridge_state.json` (if present).
|
||
2. Poll PotatoMesh every `poll_interval_secs`.
|
||
3. For each new `TEXT_MESSAGE_APP`:
|
||
|
||
* Fetch node info.
|
||
* Ensure puppet is registered (`@potato_{hex}:{server_name}`).
|
||
* Set puppet display name to `long_name`.
|
||
* Send a formatted text message into `room_id` as that puppet.
|
||
* Update and persist `bridge_state.json`.
|
||
|
||
Delete `bridge_state.json` if you want it to replay all currently available messages.
|
||
|
||
---
|
||
|
||
## Development
|
||
|
||
Run tests:
|
||
|
||
```bash
|
||
cargo test
|
||
```
|
||
|
||
Format code:
|
||
|
||
```bash
|
||
cargo fmt
|
||
```
|
||
|
||
Lint (optional but recommended):
|
||
|
||
```bash
|
||
cargo clippy -- -D warnings
|
||
```
|
||
|
||
---
|
||
|
||
## GitHub Actions CI
|
||
|
||
This repository includes a GitHub Actions workflow (`.github/workflows/ci.yml`) that:
|
||
|
||
* runs on pushes and pull requests
|
||
* caches Cargo dependencies
|
||
* runs:
|
||
|
||
* `cargo fmt --check`
|
||
* `cargo clippy`
|
||
* `cargo test`
|
||
|
||
See the workflow file for details.
|
||
|
||
---
|
||
|
||
## Caveats & Future Work
|
||
|
||
* No E2EE: this bridge posts into unencrypted (or server-side managed) rooms. For encrypted rooms, you’d need real E2EE support and key management.
|
||
* No inbound Matrix → PotatoMesh direction yet. This is a one-way bridge (PotatoMesh → Matrix).
|
||
* No pagination or `since` support on the PotatoMesh API. The bridge simply deduplicates by message `id` and stores the highest seen.
|
||
|
||
If you change the PotatoMesh API, adjust the types in `src/potatomesh.rs` accordingly.
|