Compare commits
5 Commits
932171c34d
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 2a7b71fc78 | |||
| facf1742ef | |||
| d7aa18682b | |||
| b8e9b65995 | |||
| 29b048e066 |
@@ -83,17 +83,19 @@ jobs:
|
|||||||
tar_file=$(ls plugin/dist/*.tar.gz | head -n 1)
|
tar_file=$(ls plugin/dist/*.tar.gz | head -n 1)
|
||||||
api_url="${{ github.server_url }}/api/v1/repos/${{ github.repository }}"
|
api_url="${{ github.server_url }}/api/v1/repos/${{ github.repository }}"
|
||||||
|
|
||||||
# server_url may be http while Gitea redirects (308) to https:
|
# server_url is http and Gitea answers with a 308 redirect to https.
|
||||||
# curl needs -L or the POST returns an empty body. Release may also
|
# curl needs -L to follow, AND --location-trusted: without it curl
|
||||||
# already exist (retried run) — reuse it instead of failing.
|
# drops the Authorization header on the scheme-changing redirect and
|
||||||
|
# Gitea replies "token is required". Release may already exist
|
||||||
|
# (retried run) — reuse it instead of failing.
|
||||||
fetch_id() { python3 -c "import json,sys;print(json.load(sys.stdin).get('id') or '')" 2>/dev/null; }
|
fetch_id() { python3 -c "import json,sys;print(json.load(sys.stdin).get('id') or '')" 2>/dev/null; }
|
||||||
|
|
||||||
response=$(curl -s -k -L -H "Authorization: token $GITEA_TOKEN" \
|
response=$(curl -s -k -L --location-trusted -H "Authorization: token $GITEA_TOKEN" \
|
||||||
"$api_url/releases/tags/$tag_name")
|
"$api_url/releases/tags/$tag_name")
|
||||||
release_id=$(printf '%s' "$response" | fetch_id)
|
release_id=$(printf '%s' "$response" | fetch_id)
|
||||||
|
|
||||||
if [ -z "$release_id" ]; then
|
if [ -z "$release_id" ]; then
|
||||||
response=$(curl -s -k -L -X POST "$api_url/releases" \
|
response=$(curl -s -k -L --location-trusted -X POST "$api_url/releases" \
|
||||||
-H "Authorization: token $GITEA_TOKEN" \
|
-H "Authorization: token $GITEA_TOKEN" \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
-d "{\"tag_name\": \"$tag_name\", \"name\": \"$tag_name\", \"body\": \"Release $tag_name for hermes-meshtastic plugin\"}")
|
-d "{\"tag_name\": \"$tag_name\", \"name\": \"$tag_name\", \"body\": \"Release $tag_name for hermes-meshtastic plugin\"}")
|
||||||
@@ -101,11 +103,11 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
if [ -n "$release_id" ]; then
|
if [ -n "$release_id" ]; then
|
||||||
curl -s -k -L -X POST "$api_url/releases/$release_id/assets?name=$(basename "$wheel_file")" \
|
curl -s -k -L --location-trusted -X POST "$api_url/releases/$release_id/assets?name=$(basename "$wheel_file")" \
|
||||||
-H "Authorization: token $GITEA_TOKEN" \
|
-H "Authorization: token $GITEA_TOKEN" \
|
||||||
-H "Content-Type: application/octet-stream" \
|
-H "Content-Type: application/octet-stream" \
|
||||||
--data-binary "@$wheel_file"
|
--data-binary "@$wheel_file"
|
||||||
curl -s -k -L -X POST "$api_url/releases/$release_id/assets?name=$(basename "$tar_file")" \
|
curl -s -k -L --location-trusted -X POST "$api_url/releases/$release_id/assets?name=$(basename "$tar_file")" \
|
||||||
-H "Authorization: token $GITEA_TOKEN" \
|
-H "Authorization: token $GITEA_TOKEN" \
|
||||||
-H "Content-Type: application/octet-stream" \
|
-H "Content-Type: application/octet-stream" \
|
||||||
--data-binary "@$tar_file"
|
--data-binary "@$tar_file"
|
||||||
|
|||||||
@@ -214,13 +214,21 @@ def get_adapter_class():
|
|||||||
"""Handle incoming text packet from Meshtastic pubsub (worker thread)."""
|
"""Handle incoming text packet from Meshtastic pubsub (worker thread)."""
|
||||||
try:
|
try:
|
||||||
ch = packet.get("channel", 0)
|
ch = packet.get("channel", 0)
|
||||||
if self.channel_index is not None and ch != self.channel_index:
|
my_info = getattr(interface, "myInfo", None)
|
||||||
|
my_node_num = getattr(my_info, "my_node_num", 0)
|
||||||
|
is_direct_to_me = packet.get("to") == my_node_num
|
||||||
|
is_pki_dm = is_direct_to_me and packet.get("pkiEncrypted", False)
|
||||||
|
if (
|
||||||
|
self.channel_index is not None
|
||||||
|
and ch != self.channel_index
|
||||||
|
and not is_pki_dm
|
||||||
|
):
|
||||||
# Ignore packets from different channels (e.g. public channel 0).
|
# Ignore packets from different channels (e.g. public channel 0).
|
||||||
|
# PKI direct messages omit the channel field and decode as
|
||||||
|
# channel 0 even when sent from a secondary channel.
|
||||||
return
|
return
|
||||||
|
|
||||||
from_id = packet.get("fromId") or f"!{packet.get('from', 0):08x}"
|
from_id = packet.get("fromId") or f"!{packet.get('from', 0):08x}"
|
||||||
my_info = getattr(interface, "myInfo", None)
|
|
||||||
my_node_num = getattr(my_info, "my_node_num", 0)
|
|
||||||
if packet.get("from") == my_node_num:
|
if packet.get("from") == my_node_num:
|
||||||
# Prevent self-message loops.
|
# Prevent self-message loops.
|
||||||
return
|
return
|
||||||
@@ -237,7 +245,7 @@ def get_adapter_class():
|
|||||||
return
|
return
|
||||||
|
|
||||||
to_id = packet.get("toId")
|
to_id = packet.get("toId")
|
||||||
is_dm = to_id != "^all" and packet.get("to") == my_node_num
|
is_dm = to_id != "^all" and is_direct_to_me
|
||||||
chat_id = from_id if is_dm else f"channel_{ch}"
|
chat_id = from_id if is_dm else f"channel_{ch}"
|
||||||
chat_type = "dm" if is_dm else "channel"
|
chat_type = "dm" if is_dm else "channel"
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "hermes-meshtastic"
|
name = "hermes-meshtastic"
|
||||||
version = "0.1.2"
|
version = "0.1.4"
|
||||||
description = "Meshtastic LoRa mesh gateway adapter plugin for Hermes Agent"
|
description = "Meshtastic LoRa mesh gateway adapter plugin for Hermes Agent"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.10"
|
requires-python = ">=3.10"
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ so the gate can never silently skip.
|
|||||||
|
|
||||||
import inspect
|
import inspect
|
||||||
import os
|
import os
|
||||||
|
from types import SimpleNamespace
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
@@ -228,3 +229,40 @@ def test_inbound_event_construction_is_valid(registered_entry):
|
|||||||
)
|
)
|
||||||
assert event.source.platform.value == "meshtastic"
|
assert event.source.platform.value == "meshtastic"
|
||||||
assert event.text == "hello over LoRa"
|
assert event.text == "hello over LoRa"
|
||||||
|
|
||||||
|
|
||||||
|
def test_pki_direct_message_without_channel_reaches_gateway(
|
||||||
|
registered_entry, monkeypatch
|
||||||
|
):
|
||||||
|
"""PKI DMs omit channel and must bypass the secondary-channel filter."""
|
||||||
|
adapter = _make_adapter(registered_entry)
|
||||||
|
adapter._loop = SimpleNamespace(is_running=lambda: True)
|
||||||
|
captured = {}
|
||||||
|
|
||||||
|
async def handle_message(event):
|
||||||
|
captured["event"] = event
|
||||||
|
|
||||||
|
def submit(coro, loop):
|
||||||
|
captured["loop"] = loop
|
||||||
|
with pytest.raises(StopIteration):
|
||||||
|
coro.send(None)
|
||||||
|
|
||||||
|
adapter.handle_message = handle_message
|
||||||
|
monkeypatch.setattr(plugin_adapter.asyncio, "run_coroutine_threadsafe", submit)
|
||||||
|
|
||||||
|
interface = SimpleNamespace(myInfo=SimpleNamespace(my_node_num=0x1BA1BC60))
|
||||||
|
adapter._on_meshtastic_receive(
|
||||||
|
{
|
||||||
|
"from": 0x1BA1496C,
|
||||||
|
"to": 0x1BA1BC60,
|
||||||
|
"toId": "!1ba1bc60",
|
||||||
|
"pkiEncrypted": True,
|
||||||
|
"decoded": {"text": "hello over PKI"},
|
||||||
|
"id": 42,
|
||||||
|
},
|
||||||
|
interface,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert captured["loop"] is adapter._loop
|
||||||
|
assert captured["event"].text == "hello over PKI"
|
||||||
|
assert captured["event"].source.chat_id == "!1ba1496c"
|
||||||
|
|||||||
Reference in New Issue
Block a user