# 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: the hex node id (without `!`), e.g. `!67fc83cb` → `@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` (or any configured base URL)
- Looks up node metadata via `GET /nodes/{hex}` and caches it
- One Matrix user per node:
  - username: 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 /messages` returns an array of messages
  - `GET /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=@{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. `@[0-9a-f]{8}:example.org`)
  - Provides an `as_token` the bridge can use

- Network access from the bridge host to:
  - `https://potatomesh.net/api` (or your configured PotatoMesh API)
  - Your Matrix homeserver (`https://matrix.example.org`)

---

## Configuration

All configuration is in `Config.toml` in the project root.

Example:

```toml
[potatomesh]
# Base URL without trailing slash
base_url = "https://potatomesh.net/api"
# 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"
# 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"
````

### PotatoMesh API

The bridge assumes:

* Messages: `GET {base_url}/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}/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 as the Matrix localpart.

---

## 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:8080"  # not used by this bridge if it only calls out
as_token: "YOUR_APPSERVICE_AS_TOKEN"
hs_token: "SECRET_HS_TOKEN"
sender_localpart: "potatomesh-bridge"
rate_limited: false
namespaces:
  users:
    - exclusive: true
      regex: "@[0-9a-f]{8}:example.org"
```

For this bridge, only the `as_token` and `namespaces.users` actually matter. The bridge does not accept inbound events; it only uses the `as_token` to call the homeserver.

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).

---

## 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
```

---

## 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 (`@{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 (currently mostly compile checks, no real tests yet):

```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.
