Metadata-Version: 2.5
Name: meshcore-irc-bridge
Version: 0.1.2
Summary: A one-way bridge relaying MeshCore companion radio channel messages into IRC channels
Project-URL: Homepage, https://github.com/Faradome/meshcore-irc-bridge
Project-URL: Issues, https://github.com/Faradome/meshcore-irc-bridge/issues
Author-email: William Canterbury <william.canterbury@gmail.com>
License-Expression: MIT
License-File: LICENSE
Classifier: Environment :: Console
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Topic :: Communications :: Chat :: Internet Relay Chat
Classifier: Topic :: Communications :: Ham Radio
Requires-Python: >=3.10
Requires-Dist: bleak>=0.22
Requires-Dist: meshcore>=2.3.9
Requires-Dist: pyserial>=3.5
Requires-Dist: pyyaml>=6.0
Provides-Extra: dev
Requires-Dist: build>=1.0; extra == 'dev'
Requires-Dist: mypy>=1.10; extra == 'dev'
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
Requires-Dist: pytest-cov>=5.0; extra == 'dev'
Requires-Dist: pytest>=8.0; extra == 'dev'
Requires-Dist: ruff>=0.6; extra == 'dev'
Requires-Dist: types-pyserial; extra == 'dev'
Requires-Dist: types-pyyaml; extra == 'dev'
Description-Content-Type: text/markdown

# meshcore-irc-bridge

> **This is an AI-generated application.** The design, code, tests, and documentation in this
> repository were produced by an AI coding agent (Claude), directed and reviewed by a human
> maintainer.

A one-way bridge: it connects to a [MeshCore](https://meshcore.io/) companion
radio, listens for channel messages, and relays them into IRC channels — one
mesh channel mapped to one IRC channel. It never sends anything back to the
mesh; IRC messages are received and ignored.

Built on the [`meshcore`](https://pypi.org/project/meshcore/) Python package
for the radio side (BLE, serial, or TCP companion connection). The IRC side is
a small hand-rolled asyncio client with first-class
[IRCv3 SASL](https://ircv3.net/specs/extensions/sasl-3.1) support, a NickServ
`IDENTIFY` fallback (with a configurable wait before joining channels), and
support for connecting with a fully unregistered nickname.

## Install

Published on PyPI as [`meshcore-irc-bridge`](https://pypi.org/project/meshcore-irc-bridge/).
The recommended way to install it is [pipx](https://pipx.pypa.io/), which puts
the `meshcore-irc-bridge` command on your PATH in its own isolated
environment, without touching your system Python packages:

```bash
pipx install meshcore-irc-bridge
```

Plain `pip` works too, ideally in a virtual environment:

```bash
python3 -m venv .venv
source .venv/bin/activate
pip install meshcore-irc-bridge
```

To install from a checkout for development instead, see [Development](#development) below.

## Configure

Copy [`config.example.yaml`](config.example.yaml) to `config.yaml` and edit it:

```bash
cp config.example.yaml config.yaml
chmod 600 config.yaml
```

`cp` creates the copy with your umask's default permissions, typically
world-readable -- worth locking down since the file commonly ends up
holding plaintext SASL/NickServ passwords (the bridge warns at startup if
it finds `config.yaml` still readable by group/other).

- `mesh.connection` — how to reach your companion radio: `type: ble` (with
  `address`), `type: serial` (with `port`/`baudrate`), or `type: tcp` (with
  `host`/`port`).
- `irc` — the IRC server and how to authenticate. `irc.auth.mode` is one of:
  - `sasl` — authenticate with `AUTHENTICATE PLAIN` before registration
    completes. Requires `irc.auth.sasl.username`/`password`.
  - `nickserv` — connect without SASL, wait for the welcome (`001`), send
    `PRIVMSG NickServ :IDENTIFY <password>`, then wait
    `irc.auth.nickserv.join_wait_seconds` before joining channels (long
    enough for services to apply your cloak/account before you join gated
    channels). Requires `irc.auth.nickserv.password`.
  - `none` — connect with a fully unregistered nickname and join immediately.

  The auth mode is fixed by config — if it fails (e.g. the server doesn't
  offer SASL, or the password is rejected), the bridge logs the failure and
  retries the *same* mode on reconnect rather than silently switching
  methods.

  Secrets support `${ENV_VAR}` interpolation so passwords don't need to sit
  in the YAML file in plaintext, e.g. `password: "${IRC_SASL_PASSWORD}"`.

- `channels` — the mesh-channel-to-IRC-channel mapping, e.g.:

  ```yaml
  channels:
    - mesh_channel: 0
      irc_channel: "#mesh-general"
    - mesh_channel: 1
      irc_channel: "#mesh-emergency"
  ```

  Several mesh channels may map to the same IRC channel. When they do,
  each relayed line is prefixed with `[<mesh_channel>] ` so messages from
  either stay attributable once interleaved there (e.g. `[0] hello`,
  `[1] hi`); a mesh channel with an IRC channel all to itself is left
  unprefixed.

### Setting up a channel on the radio itself

This bridge only *reads* channel messages — it never creates, renames, or
rekeys a channel on the companion radio. To set one up (or check what's
already configured), use [`meshcorectl`](https://github.com/Faradome/meshcorectl),
a companion CLI for MeshCore radios:

```bash
meshcorectl create channel 0 "General"   # create/rename channel 0
meshcorectl get channels                  # list what's configured on the radio
```

## Run

```bash
meshcore-irc-bridge --config config.yaml
# or
python -m meshcore_irc_bridge --config config.yaml
```

`--log-level` (default `INFO`) controls verbosity.

## Development

```bash
python3 -m venv .venv
source .venv/bin/activate
pip install -e ".[dev]"
pytest                # 100% line+branch coverage enforced
ruff check .
mypy
```

Tests never touch real hardware or a real IRC network: the mesh side is
exercised against a hardware-free double of `meshcore.MeshCore`
(`tests/fakes/meshcore_double.py`), and the IRC client is exercised against a
real (loopback-only) asyncio TCP server that scripts IRC protocol exchanges
(`tests/fakes/fake_irc_server.py`).

Real end-to-end verification against an actual radio and IRC network is out
of scope for the automated test suite — run the bridge against your own
setup once installed to confirm it end-to-end.

## License

[MIT](LICENSE)
