meshcore-irc-bridge 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- meshcore_irc_bridge-0.1.0/.github/workflows/publish.yml +64 -0
- meshcore_irc_bridge-0.1.0/.gitignore +14 -0
- meshcore_irc_bridge-0.1.0/LICENSE +21 -0
- meshcore_irc_bridge-0.1.0/PKG-INFO +147 -0
- meshcore_irc_bridge-0.1.0/README.md +117 -0
- meshcore_irc_bridge-0.1.0/config.example.yaml +91 -0
- meshcore_irc_bridge-0.1.0/pyproject.toml +92 -0
- meshcore_irc_bridge-0.1.0/src/meshcore_irc_bridge/__init__.py +0 -0
- meshcore_irc_bridge-0.1.0/src/meshcore_irc_bridge/__main__.py +4 -0
- meshcore_irc_bridge-0.1.0/src/meshcore_irc_bridge/bridge.py +287 -0
- meshcore_irc_bridge-0.1.0/src/meshcore_irc_bridge/cli.py +91 -0
- meshcore_irc_bridge-0.1.0/src/meshcore_irc_bridge/config.py +397 -0
- meshcore_irc_bridge-0.1.0/src/meshcore_irc_bridge/formatting.py +86 -0
- meshcore_irc_bridge-0.1.0/src/meshcore_irc_bridge/irc/__init__.py +0 -0
- meshcore_irc_bridge-0.1.0/src/meshcore_irc_bridge/irc/client.py +320 -0
- meshcore_irc_bridge-0.1.0/src/meshcore_irc_bridge/irc/protocol.py +183 -0
- meshcore_irc_bridge-0.1.0/src/meshcore_irc_bridge/irc/sasl.py +46 -0
- meshcore_irc_bridge-0.1.0/src/meshcore_irc_bridge/mesh_connection.py +87 -0
- meshcore_irc_bridge-0.1.0/tests/__init__.py +0 -0
- meshcore_irc_bridge-0.1.0/tests/cli/__init__.py +0 -0
- meshcore_irc_bridge-0.1.0/tests/cli/test_cli.py +188 -0
- meshcore_irc_bridge-0.1.0/tests/conftest.py +0 -0
- meshcore_irc_bridge-0.1.0/tests/fakes/__init__.py +0 -0
- meshcore_irc_bridge-0.1.0/tests/fakes/fake_irc_server.py +85 -0
- meshcore_irc_bridge-0.1.0/tests/fakes/meshcore_double.py +108 -0
- meshcore_irc_bridge-0.1.0/tests/unit/__init__.py +0 -0
- meshcore_irc_bridge-0.1.0/tests/unit/irc/__init__.py +0 -0
- meshcore_irc_bridge-0.1.0/tests/unit/irc/test_client.py +711 -0
- meshcore_irc_bridge-0.1.0/tests/unit/irc/test_protocol.py +316 -0
- meshcore_irc_bridge-0.1.0/tests/unit/irc/test_sasl.py +47 -0
- meshcore_irc_bridge-0.1.0/tests/unit/test_bridge.py +763 -0
- meshcore_irc_bridge-0.1.0/tests/unit/test_config.py +708 -0
- meshcore_irc_bridge-0.1.0/tests/unit/test_formatting.py +142 -0
- meshcore_irc_bridge-0.1.0/tests/unit/test_mesh_connection.py +139 -0
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
test:
|
|
9
|
+
runs-on: ubuntu-latest
|
|
10
|
+
steps:
|
|
11
|
+
- uses: actions/checkout@v4
|
|
12
|
+
- uses: actions/setup-python@v5
|
|
13
|
+
with:
|
|
14
|
+
python-version: "3.12"
|
|
15
|
+
- run: pip install -e .[dev]
|
|
16
|
+
- run: ruff check .
|
|
17
|
+
- run: mypy
|
|
18
|
+
- run: pytest
|
|
19
|
+
|
|
20
|
+
build:
|
|
21
|
+
needs: test
|
|
22
|
+
runs-on: ubuntu-latest
|
|
23
|
+
outputs:
|
|
24
|
+
version: ${{ steps.version.outputs.version }}
|
|
25
|
+
steps:
|
|
26
|
+
- uses: actions/checkout@v4
|
|
27
|
+
- uses: actions/setup-python@v5
|
|
28
|
+
with:
|
|
29
|
+
python-version: "3.12"
|
|
30
|
+
- run: pip install build
|
|
31
|
+
- run: python -m build
|
|
32
|
+
- id: version
|
|
33
|
+
run: |
|
|
34
|
+
echo "version=$(python -c 'import tomllib; print(tomllib.load(open("pyproject.toml", "rb"))["project"]["version"])')" >> "$GITHUB_OUTPUT"
|
|
35
|
+
- uses: actions/upload-artifact@v4
|
|
36
|
+
with:
|
|
37
|
+
name: dist
|
|
38
|
+
path: dist/
|
|
39
|
+
|
|
40
|
+
publish:
|
|
41
|
+
needs: build
|
|
42
|
+
runs-on: ubuntu-latest
|
|
43
|
+
environment: pypi
|
|
44
|
+
permissions:
|
|
45
|
+
id-token: write
|
|
46
|
+
steps:
|
|
47
|
+
- uses: actions/download-artifact@v4
|
|
48
|
+
with:
|
|
49
|
+
name: dist
|
|
50
|
+
path: dist/
|
|
51
|
+
- name: Check whether this version is already on PyPI
|
|
52
|
+
id: check
|
|
53
|
+
run: |
|
|
54
|
+
VERSION="${{ needs.build.outputs.version }}"
|
|
55
|
+
STATUS=$(curl -s -o /dev/null -w "%{http_code}" "https://pypi.org/pypi/meshcore-irc-bridge/${VERSION}/json")
|
|
56
|
+
if [ "$STATUS" = "200" ]; then
|
|
57
|
+
echo "Version ${VERSION} is already published; skipping upload."
|
|
58
|
+
echo "exists=true" >> "$GITHUB_OUTPUT"
|
|
59
|
+
else
|
|
60
|
+
echo "exists=false" >> "$GITHUB_OUTPUT"
|
|
61
|
+
fi
|
|
62
|
+
- name: Publish to PyPI
|
|
63
|
+
if: steps.check.outputs.exists == 'false'
|
|
64
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Faradome
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: meshcore-irc-bridge
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A one-way bridge relaying MeshCore companion radio channel messages into IRC channels
|
|
5
|
+
Project-URL: Homepage, https://github.com/Faradome/meshcore-irc-bridge
|
|
6
|
+
Project-URL: Issues, https://github.com/Faradome/meshcore-irc-bridge/issues
|
|
7
|
+
Author-email: William Canterbury <william.canterbury@gmail.com>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Classifier: Environment :: Console
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Topic :: Communications :: Chat :: Internet Relay Chat
|
|
14
|
+
Classifier: Topic :: Communications :: Ham Radio
|
|
15
|
+
Requires-Python: >=3.10
|
|
16
|
+
Requires-Dist: bleak>=0.22
|
|
17
|
+
Requires-Dist: meshcore>=2.3.9
|
|
18
|
+
Requires-Dist: pyserial>=3.5
|
|
19
|
+
Requires-Dist: pyyaml>=6.0
|
|
20
|
+
Provides-Extra: dev
|
|
21
|
+
Requires-Dist: build>=1.0; extra == 'dev'
|
|
22
|
+
Requires-Dist: mypy>=1.10; extra == 'dev'
|
|
23
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
24
|
+
Requires-Dist: pytest-cov>=5.0; extra == 'dev'
|
|
25
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
26
|
+
Requires-Dist: ruff>=0.6; extra == 'dev'
|
|
27
|
+
Requires-Dist: types-pyserial; extra == 'dev'
|
|
28
|
+
Requires-Dist: types-pyyaml; extra == 'dev'
|
|
29
|
+
Description-Content-Type: text/markdown
|
|
30
|
+
|
|
31
|
+
# meshcore-irc-bridge
|
|
32
|
+
|
|
33
|
+
> **This is an AI-generated application.** The design, code, tests, and documentation in this
|
|
34
|
+
> repository were produced by an AI coding agent (Claude), directed and reviewed by a human
|
|
35
|
+
> maintainer.
|
|
36
|
+
|
|
37
|
+
A one-way bridge: it connects to a [MeshCore](https://meshcore.co.uk/) companion
|
|
38
|
+
radio, listens for channel messages, and relays them into IRC channels — one
|
|
39
|
+
mesh channel mapped to one IRC channel. It never sends anything back to the
|
|
40
|
+
mesh; IRC messages are received and ignored.
|
|
41
|
+
|
|
42
|
+
Built on the [`meshcore`](https://pypi.org/project/meshcore/) Python package
|
|
43
|
+
for the radio side (BLE, serial, or TCP companion connection). The IRC side is
|
|
44
|
+
a small hand-rolled asyncio client with first-class
|
|
45
|
+
[IRCv3 SASL](https://ircv3.net/specs/extensions/sasl-3.1) support, a NickServ
|
|
46
|
+
`IDENTIFY` fallback (with a configurable wait before joining channels), and
|
|
47
|
+
support for connecting with a fully unregistered nickname.
|
|
48
|
+
|
|
49
|
+
## Install
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
python3 -m venv .venv
|
|
53
|
+
source .venv/bin/activate
|
|
54
|
+
pip install -e ".[dev]" # from a checkout, until this is published
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Configure
|
|
58
|
+
|
|
59
|
+
Copy [`config.example.yaml`](config.example.yaml) to `config.yaml` and edit it:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
cp config.example.yaml config.yaml
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
- `mesh.connection` — how to reach your companion radio: `type: ble` (with
|
|
66
|
+
`address`), `type: serial` (with `port`/`baudrate`), or `type: tcp` (with
|
|
67
|
+
`host`/`port`).
|
|
68
|
+
- `irc` — the IRC server and how to authenticate. `irc.auth.mode` is one of:
|
|
69
|
+
- `sasl` — authenticate with `AUTHENTICATE PLAIN` before registration
|
|
70
|
+
completes. Requires `irc.auth.sasl.username`/`password`.
|
|
71
|
+
- `nickserv` — connect without SASL, wait for the welcome (`001`), send
|
|
72
|
+
`PRIVMSG NickServ :IDENTIFY <password>`, then wait
|
|
73
|
+
`irc.auth.nickserv.join_wait_seconds` before joining channels (long
|
|
74
|
+
enough for services to apply your cloak/account before you join gated
|
|
75
|
+
channels). Requires `irc.auth.nickserv.password`.
|
|
76
|
+
- `none` — connect with a fully unregistered nickname and join immediately.
|
|
77
|
+
|
|
78
|
+
The auth mode is fixed by config — if it fails (e.g. the server doesn't
|
|
79
|
+
offer SASL, or the password is rejected), the bridge logs the failure and
|
|
80
|
+
retries the *same* mode on reconnect rather than silently switching
|
|
81
|
+
methods.
|
|
82
|
+
|
|
83
|
+
Secrets support `${ENV_VAR}` interpolation so passwords don't need to sit
|
|
84
|
+
in the YAML file in plaintext, e.g. `password: "${IRC_SASL_PASSWORD}"`.
|
|
85
|
+
|
|
86
|
+
- `channels` — the mesh-channel-to-IRC-channel mapping, e.g.:
|
|
87
|
+
|
|
88
|
+
```yaml
|
|
89
|
+
channels:
|
|
90
|
+
- mesh_channel: 0
|
|
91
|
+
irc_channel: "#mesh-general"
|
|
92
|
+
- mesh_channel: 1
|
|
93
|
+
irc_channel: "#mesh-emergency"
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Several mesh channels may map to the same IRC channel. When they do,
|
|
97
|
+
each relayed line is prefixed with `[<mesh_channel>] ` so messages from
|
|
98
|
+
either stay attributable once interleaved there (e.g. `[0] hello`,
|
|
99
|
+
`[1] hi`); a mesh channel with an IRC channel all to itself is left
|
|
100
|
+
unprefixed.
|
|
101
|
+
|
|
102
|
+
### Setting up a channel on the radio itself
|
|
103
|
+
|
|
104
|
+
This bridge only *reads* channel messages — it never creates, renames, or
|
|
105
|
+
rekeys a channel on the companion radio. To set one up (or check what's
|
|
106
|
+
already configured), use [`meshcorectl`](https://github.com/Faradome/meshcorectl),
|
|
107
|
+
a companion CLI for MeshCore radios:
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
meshcorectl create channel 0 "General" # create/rename channel 0
|
|
111
|
+
meshcorectl get channels # list what's configured on the radio
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## Run
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
meshcore-irc-bridge --config config.yaml
|
|
118
|
+
# or
|
|
119
|
+
python -m meshcore_irc_bridge --config config.yaml
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
`--log-level` (default `INFO`) controls verbosity.
|
|
123
|
+
|
|
124
|
+
## Development
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
python3 -m venv .venv
|
|
128
|
+
source .venv/bin/activate
|
|
129
|
+
pip install -e ".[dev]"
|
|
130
|
+
pytest # 100% line+branch coverage enforced
|
|
131
|
+
ruff check .
|
|
132
|
+
mypy
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Tests never touch real hardware or a real IRC network: the mesh side is
|
|
136
|
+
exercised against a hardware-free double of `meshcore.MeshCore`
|
|
137
|
+
(`tests/fakes/meshcore_double.py`), and the IRC client is exercised against a
|
|
138
|
+
real (loopback-only) asyncio TCP server that scripts IRC protocol exchanges
|
|
139
|
+
(`tests/fakes/fake_irc_server.py`).
|
|
140
|
+
|
|
141
|
+
Real end-to-end verification against an actual radio and IRC network is out
|
|
142
|
+
of scope for the automated test suite — run the bridge against your own
|
|
143
|
+
setup once installed to confirm it end-to-end.
|
|
144
|
+
|
|
145
|
+
## License
|
|
146
|
+
|
|
147
|
+
[MIT](LICENSE)
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# meshcore-irc-bridge
|
|
2
|
+
|
|
3
|
+
> **This is an AI-generated application.** The design, code, tests, and documentation in this
|
|
4
|
+
> repository were produced by an AI coding agent (Claude), directed and reviewed by a human
|
|
5
|
+
> maintainer.
|
|
6
|
+
|
|
7
|
+
A one-way bridge: it connects to a [MeshCore](https://meshcore.co.uk/) companion
|
|
8
|
+
radio, listens for channel messages, and relays them into IRC channels — one
|
|
9
|
+
mesh channel mapped to one IRC channel. It never sends anything back to the
|
|
10
|
+
mesh; IRC messages are received and ignored.
|
|
11
|
+
|
|
12
|
+
Built on the [`meshcore`](https://pypi.org/project/meshcore/) Python package
|
|
13
|
+
for the radio side (BLE, serial, or TCP companion connection). The IRC side is
|
|
14
|
+
a small hand-rolled asyncio client with first-class
|
|
15
|
+
[IRCv3 SASL](https://ircv3.net/specs/extensions/sasl-3.1) support, a NickServ
|
|
16
|
+
`IDENTIFY` fallback (with a configurable wait before joining channels), and
|
|
17
|
+
support for connecting with a fully unregistered nickname.
|
|
18
|
+
|
|
19
|
+
## Install
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
python3 -m venv .venv
|
|
23
|
+
source .venv/bin/activate
|
|
24
|
+
pip install -e ".[dev]" # from a checkout, until this is published
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Configure
|
|
28
|
+
|
|
29
|
+
Copy [`config.example.yaml`](config.example.yaml) to `config.yaml` and edit it:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
cp config.example.yaml config.yaml
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
- `mesh.connection` — how to reach your companion radio: `type: ble` (with
|
|
36
|
+
`address`), `type: serial` (with `port`/`baudrate`), or `type: tcp` (with
|
|
37
|
+
`host`/`port`).
|
|
38
|
+
- `irc` — the IRC server and how to authenticate. `irc.auth.mode` is one of:
|
|
39
|
+
- `sasl` — authenticate with `AUTHENTICATE PLAIN` before registration
|
|
40
|
+
completes. Requires `irc.auth.sasl.username`/`password`.
|
|
41
|
+
- `nickserv` — connect without SASL, wait for the welcome (`001`), send
|
|
42
|
+
`PRIVMSG NickServ :IDENTIFY <password>`, then wait
|
|
43
|
+
`irc.auth.nickserv.join_wait_seconds` before joining channels (long
|
|
44
|
+
enough for services to apply your cloak/account before you join gated
|
|
45
|
+
channels). Requires `irc.auth.nickserv.password`.
|
|
46
|
+
- `none` — connect with a fully unregistered nickname and join immediately.
|
|
47
|
+
|
|
48
|
+
The auth mode is fixed by config — if it fails (e.g. the server doesn't
|
|
49
|
+
offer SASL, or the password is rejected), the bridge logs the failure and
|
|
50
|
+
retries the *same* mode on reconnect rather than silently switching
|
|
51
|
+
methods.
|
|
52
|
+
|
|
53
|
+
Secrets support `${ENV_VAR}` interpolation so passwords don't need to sit
|
|
54
|
+
in the YAML file in plaintext, e.g. `password: "${IRC_SASL_PASSWORD}"`.
|
|
55
|
+
|
|
56
|
+
- `channels` — the mesh-channel-to-IRC-channel mapping, e.g.:
|
|
57
|
+
|
|
58
|
+
```yaml
|
|
59
|
+
channels:
|
|
60
|
+
- mesh_channel: 0
|
|
61
|
+
irc_channel: "#mesh-general"
|
|
62
|
+
- mesh_channel: 1
|
|
63
|
+
irc_channel: "#mesh-emergency"
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Several mesh channels may map to the same IRC channel. When they do,
|
|
67
|
+
each relayed line is prefixed with `[<mesh_channel>] ` so messages from
|
|
68
|
+
either stay attributable once interleaved there (e.g. `[0] hello`,
|
|
69
|
+
`[1] hi`); a mesh channel with an IRC channel all to itself is left
|
|
70
|
+
unprefixed.
|
|
71
|
+
|
|
72
|
+
### Setting up a channel on the radio itself
|
|
73
|
+
|
|
74
|
+
This bridge only *reads* channel messages — it never creates, renames, or
|
|
75
|
+
rekeys a channel on the companion radio. To set one up (or check what's
|
|
76
|
+
already configured), use [`meshcorectl`](https://github.com/Faradome/meshcorectl),
|
|
77
|
+
a companion CLI for MeshCore radios:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
meshcorectl create channel 0 "General" # create/rename channel 0
|
|
81
|
+
meshcorectl get channels # list what's configured on the radio
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Run
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
meshcore-irc-bridge --config config.yaml
|
|
88
|
+
# or
|
|
89
|
+
python -m meshcore_irc_bridge --config config.yaml
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
`--log-level` (default `INFO`) controls verbosity.
|
|
93
|
+
|
|
94
|
+
## Development
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
python3 -m venv .venv
|
|
98
|
+
source .venv/bin/activate
|
|
99
|
+
pip install -e ".[dev]"
|
|
100
|
+
pytest # 100% line+branch coverage enforced
|
|
101
|
+
ruff check .
|
|
102
|
+
mypy
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Tests never touch real hardware or a real IRC network: the mesh side is
|
|
106
|
+
exercised against a hardware-free double of `meshcore.MeshCore`
|
|
107
|
+
(`tests/fakes/meshcore_double.py`), and the IRC client is exercised against a
|
|
108
|
+
real (loopback-only) asyncio TCP server that scripts IRC protocol exchanges
|
|
109
|
+
(`tests/fakes/fake_irc_server.py`).
|
|
110
|
+
|
|
111
|
+
Real end-to-end verification against an actual radio and IRC network is out
|
|
112
|
+
of scope for the automated test suite — run the bridge against your own
|
|
113
|
+
setup once installed to confirm it end-to-end.
|
|
114
|
+
|
|
115
|
+
## License
|
|
116
|
+
|
|
117
|
+
[MIT](LICENSE)
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# meshcore-irc-bridge configuration.
|
|
2
|
+
#
|
|
3
|
+
# Copy this file to config.yaml and edit it for your setup:
|
|
4
|
+
# cp config.example.yaml config.yaml
|
|
5
|
+
#
|
|
6
|
+
# Secrets support ${ENV_VAR} interpolation, so passwords don't need to sit
|
|
7
|
+
# in this file in plaintext -- e.g. password: "${IRC_SASL_PASSWORD}".
|
|
8
|
+
|
|
9
|
+
mesh:
|
|
10
|
+
connection:
|
|
11
|
+
# One of: ble | serial | tcp
|
|
12
|
+
type: ble
|
|
13
|
+
|
|
14
|
+
# --- ble ---
|
|
15
|
+
address: "AA:BB:CC:DD:EE:FF"
|
|
16
|
+
|
|
17
|
+
# --- serial (uncomment and remove the ble fields above) ---
|
|
18
|
+
# type: serial
|
|
19
|
+
# port: /dev/ttyUSB0
|
|
20
|
+
# baudrate: 115200
|
|
21
|
+
|
|
22
|
+
# --- tcp (uncomment and remove the ble fields above) ---
|
|
23
|
+
# type: tcp
|
|
24
|
+
# host: 192.168.1.50
|
|
25
|
+
# port: 5000
|
|
26
|
+
|
|
27
|
+
# Let the meshcore library itself retry a dropped transport before this
|
|
28
|
+
# bridge gives up and reconnects from scratch.
|
|
29
|
+
auto_reconnect: true
|
|
30
|
+
max_reconnect_attempts: 5
|
|
31
|
+
|
|
32
|
+
irc:
|
|
33
|
+
server: irc.libera.chat
|
|
34
|
+
port: 6697
|
|
35
|
+
tls: true
|
|
36
|
+
nickname: meshcore-bridge
|
|
37
|
+
# Defaults to `nickname` if omitted.
|
|
38
|
+
username: meshcore
|
|
39
|
+
realname: MeshCore IRC Bridge
|
|
40
|
+
|
|
41
|
+
auth:
|
|
42
|
+
# One of: sasl | nickserv | none
|
|
43
|
+
#
|
|
44
|
+
# The mode is fixed here -- if it fails (server doesn't offer SASL, a
|
|
45
|
+
# password is rejected, ...) the bridge logs the failure and retries
|
|
46
|
+
# the *same* mode on reconnect, it does not silently fall back to a
|
|
47
|
+
# different method.
|
|
48
|
+
mode: sasl
|
|
49
|
+
|
|
50
|
+
# --- sasl (AUTHENTICATE PLAIN, before registration completes) ---
|
|
51
|
+
sasl:
|
|
52
|
+
username: mynick
|
|
53
|
+
password: "${IRC_SASL_PASSWORD}"
|
|
54
|
+
|
|
55
|
+
# --- nickserv (connect unauthenticated, IDENTIFY, then wait before
|
|
56
|
+
# joining channels -- for networks without SASL) ---
|
|
57
|
+
# auth:
|
|
58
|
+
# mode: nickserv
|
|
59
|
+
# nickserv:
|
|
60
|
+
# password: "${IRC_NICKSERV_PASSWORD}"
|
|
61
|
+
# join_wait_seconds: 5 # default 5
|
|
62
|
+
|
|
63
|
+
# --- none (fully unregistered nickname, joins immediately) ---
|
|
64
|
+
# auth:
|
|
65
|
+
# mode: none
|
|
66
|
+
|
|
67
|
+
reconnect:
|
|
68
|
+
initial_delay_seconds: 1
|
|
69
|
+
max_delay_seconds: 60
|
|
70
|
+
|
|
71
|
+
# A dead peer (netsplit, a middlebox silently dropping the connection,
|
|
72
|
+
# the server being killed) can look identical to an idle one from here
|
|
73
|
+
# -- both send nothing. After this many seconds of silence the bridge
|
|
74
|
+
# sends its own PING; if nothing at all comes back within
|
|
75
|
+
# ping_timeout_seconds after that, the link is declared dead and
|
|
76
|
+
# reconnected. Tune ping_idle_seconds down if your network pings less
|
|
77
|
+
# often than the default assumes.
|
|
78
|
+
ping_idle_seconds: 120
|
|
79
|
+
ping_timeout_seconds: 60
|
|
80
|
+
|
|
81
|
+
# Mesh channel index -> IRC channel mapping. Each mesh channel maps to
|
|
82
|
+
# exactly one IRC channel; several mesh channels may map to the same IRC
|
|
83
|
+
# channel. To create or rename a channel on the radio itself, use
|
|
84
|
+
# meshcorectl (https://github.com/Faradome/meshcorectl):
|
|
85
|
+
# meshcorectl create channel 0 "General"
|
|
86
|
+
# meshcorectl get channels
|
|
87
|
+
channels:
|
|
88
|
+
- mesh_channel: 0
|
|
89
|
+
irc_channel: "#mesh-general"
|
|
90
|
+
- mesh_channel: 1
|
|
91
|
+
irc_channel: "#mesh-emergency"
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "meshcore-irc-bridge"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "A one-way bridge relaying MeshCore companion radio channel messages into IRC channels"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "William Canterbury", email = "william.canterbury@gmail.com" },
|
|
14
|
+
]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Programming Language :: Python :: 3",
|
|
17
|
+
"Operating System :: OS Independent",
|
|
18
|
+
"Environment :: Console",
|
|
19
|
+
"Topic :: Communications :: Ham Radio",
|
|
20
|
+
"Topic :: Communications :: Chat :: Internet Relay Chat",
|
|
21
|
+
]
|
|
22
|
+
dependencies = [
|
|
23
|
+
"meshcore>=2.3.9",
|
|
24
|
+
"bleak>=0.22",
|
|
25
|
+
"pyserial>=3.5",
|
|
26
|
+
"pyyaml>=6.0",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
[project.optional-dependencies]
|
|
30
|
+
dev = [
|
|
31
|
+
"pytest>=8.0",
|
|
32
|
+
"pytest-cov>=5.0",
|
|
33
|
+
"pytest-asyncio>=0.23",
|
|
34
|
+
"ruff>=0.6",
|
|
35
|
+
"mypy>=1.10",
|
|
36
|
+
"types-PyYAML",
|
|
37
|
+
"types-pyserial",
|
|
38
|
+
"build>=1.0",
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
[project.urls]
|
|
42
|
+
Homepage = "https://github.com/Faradome/meshcore-irc-bridge"
|
|
43
|
+
Issues = "https://github.com/Faradome/meshcore-irc-bridge/issues"
|
|
44
|
+
|
|
45
|
+
[project.scripts]
|
|
46
|
+
meshcore-irc-bridge = "meshcore_irc_bridge.cli:main"
|
|
47
|
+
|
|
48
|
+
[tool.hatch.build.targets.wheel]
|
|
49
|
+
packages = ["src/meshcore_irc_bridge"]
|
|
50
|
+
|
|
51
|
+
[tool.pytest.ini_options]
|
|
52
|
+
testpaths = ["tests"]
|
|
53
|
+
asyncio_mode = "auto"
|
|
54
|
+
addopts = "--cov=meshcore_irc_bridge --cov-report=term-missing --cov-fail-under=100"
|
|
55
|
+
|
|
56
|
+
[tool.coverage.run]
|
|
57
|
+
source = ["meshcore_irc_bridge"]
|
|
58
|
+
branch = true
|
|
59
|
+
# A 3-line launcher shim, exercised for real by a subprocess smoke test
|
|
60
|
+
# (tests/cli/test_cli.py) -- subprocess coverage isn't worth the
|
|
61
|
+
# `COVERAGE_PROCESS_START` machinery for something this thin.
|
|
62
|
+
omit = ["*/__main__.py"]
|
|
63
|
+
|
|
64
|
+
[tool.coverage.report]
|
|
65
|
+
exclude_lines = [
|
|
66
|
+
"pragma: no cover",
|
|
67
|
+
"if TYPE_CHECKING:",
|
|
68
|
+
"raise NotImplementedError",
|
|
69
|
+
]
|
|
70
|
+
|
|
71
|
+
[tool.ruff]
|
|
72
|
+
line-length = 100
|
|
73
|
+
target-version = "py310"
|
|
74
|
+
|
|
75
|
+
[tool.ruff.lint]
|
|
76
|
+
select = ["E", "F", "I", "UP", "B"]
|
|
77
|
+
|
|
78
|
+
[tool.mypy]
|
|
79
|
+
python_version = "3.10"
|
|
80
|
+
files = ["src"]
|
|
81
|
+
warn_unused_ignores = true
|
|
82
|
+
warn_redundant_casts = true
|
|
83
|
+
no_implicit_optional = true
|
|
84
|
+
check_untyped_defs = true
|
|
85
|
+
|
|
86
|
+
[[tool.mypy.overrides]]
|
|
87
|
+
module = "meshcore.*"
|
|
88
|
+
ignore_missing_imports = true
|
|
89
|
+
|
|
90
|
+
[[tool.mypy.overrides]]
|
|
91
|
+
module = "serial.*"
|
|
92
|
+
ignore_missing_imports = true
|
|
File without changes
|