* matrix: create potato-matrix-bridge
* matrix: add unit tests
* matrix: address review comments
* ci: condition github actions to only run on paths affected...
* Add comprehensive unit tests for config, matrix, potatomesh, and main modules
* Revert "Add comprehensive unit tests for config, matrix, potatomesh, and main modules"
This reverts commit 212522b4a2.
* matrix: add unit tests
* matrix: add unit tests
* matrix: add unit tests
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_namefrom 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_APPmessages into a single Matrix room - Persists last-seen message ID to avoid duplicates across restarts
Architecture Overview
-
PotatoMesh side
GET /messagesreturns an array of messagesGET /nodes/{hex}returns node metadata (includinglong_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.messageevents 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_tokenthe bridge can use
- Whitelists the puppet user namespace (e.g.
-
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:
[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:[ { "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:{ "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):
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
# 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:
target/release/potatomesh-matrix-bridge
Run
Ensure Config.toml is present and valid, then:
./target/release/potatomesh-matrix-bridge
Environment variables you may care about:
-
RUST_LOG– for logging, e.g.:RUST_LOG=info,reqwest=warn ./target/release/potatomesh-matrix-bridge
The bridge will:
-
Load state from
bridge_state.json(if present). -
Poll PotatoMesh every
poll_interval_secs. -
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_idas 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):
cargo test
Format code:
cargo fmt
Lint (optional but recommended):
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 --checkcargo clippycargo 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
sincesupport on the PotatoMesh API. The bridge simply deduplicates by messageidand stores the highest seen.
If you change the PotatoMesh API, adjust the types in src/potatomesh.rs accordingly.