# potatomesh-matrix-bridge

A small Rust daemon that bridges **PotatoMesh** LoRa messages into a **Matrix** room.

![matrix bridge](../scrot-0.6.png)

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.
