diff --git a/AGENTS.md b/AGENTS.md index 8d6e553..0c09b6f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -75,7 +75,7 @@ Ancillary AGENTS.md files which should generally not be reviewed unless specific - Raw packet feed — a debug/observation tool ("radio aquarium"); interesting to watch or copy packets from, but not critical infrastructure - Map view — visual display of node locations from advertisements - Network visualizer — force-directed graph of mesh topology -- Bot system — automated message responses +- Fanout integrations (MQTT, bots, webhooks, Apprise) — see `app/fanout/AGENTS_fanout.md` - Read state tracking / mark-all-read — convenience feature for unread badges; no need for transactional atomicity or race-condition hardening ## Error Handling Philosophy @@ -259,7 +259,7 @@ All endpoints are prefixed with `/api` (e.g., `/api/health`). | Method | Endpoint | Description | |--------|----------|-------------| -| GET | `/api/health` | Connection status | +| GET | `/api/health` | Connection status, fanout statuses, bots_disabled flag | | GET | `/api/radio/config` | Radio configuration | | PATCH | `/api/radio/config` | Update name, location, radio params | | PUT | `/api/radio/private-key` | Import private key to radio | @@ -312,6 +312,10 @@ All endpoints are prefixed with `/api` (e.g., `/api/health`). | POST | `/api/settings/blocked-keys/toggle` | Toggle blocked key | | POST | `/api/settings/blocked-names/toggle` | Toggle blocked name | | POST | `/api/settings/migrate` | One-time migration from frontend localStorage | +| GET | `/api/fanout` | List all fanout configs | +| POST | `/api/fanout` | Create new fanout config | +| PATCH | `/api/fanout/{id}` | Update fanout config (triggers module reload) | +| DELETE | `/api/fanout/{id}` | Delete fanout config (stops module) | | GET | `/api/statistics` | Aggregated mesh network statistics | | WS | `/api/ws` | Real-time updates | diff --git a/LICENSES.md b/LICENSES.md index 12419d5..965eee3 100644 --- a/LICENSES.md +++ b/LICENSES.md @@ -56,6 +56,41 @@ SOFTWARE. +### apprise (1.9.7) — BSD-2-Clause + +
+Full license text + +``` +BSD 2-Clause License + +Copyright (c) 2025, Chris Caron +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +``` + +
+ ### fastapi (0.128.0) — MIT
diff --git a/app/AGENTS.md b/app/AGENTS.md index dd94b7d..50d083c 100644 --- a/app/AGENTS.md +++ b/app/AGENTS.md @@ -179,6 +179,12 @@ app/ - `POST /settings/blocked-names/toggle` - `POST /settings/migrate` +### Fanout +- `GET /fanout` — list all fanout configs +- `POST /fanout` — create new fanout config +- `PATCH /fanout/{id}` — update fanout config (triggers module reload) +- `DELETE /fanout/{id}` — delete fanout config (stops module) + ### Statistics - `GET /statistics` — aggregated mesh network stats (entity counts, message/packet splits, activity windows, busiest channels) diff --git a/app/fanout/bot.py b/app/fanout/bot.py index 696448c..ac6c991 100644 --- a/app/fanout/bot.py +++ b/app/fanout/bot.py @@ -58,7 +58,7 @@ class BotModule(FanoutModule): else: conversation_key = data.get("conversation_key", "") sender_key = None - is_outgoing = False + is_outgoing = bool(data.get("outgoing", False)) sender_name = data.get("sender_name") channel_key = conversation_key diff --git a/app/migrations.py b/app/migrations.py index a13c125..bc5d869 100644 --- a/app/migrations.py +++ b/app/migrations.py @@ -2129,14 +2129,22 @@ async def _migrate_036_create_fanout_configs(conn: aiosqlite.Connection) -> None sort_order += 1 logger.info("Migrated private MQTT settings to fanout_configs (enabled=%s)", enabled) - # 4. Migrate community MQTT if enabled + # 4. Migrate community MQTT if enabled OR configured (preserve disabled-but-configured) community_enabled = bool(row["community_mqtt_enabled"]) - if community_enabled: + community_iata = row["community_mqtt_iata"] or "" + community_host = row["community_mqtt_broker_host"] or "" + community_email = row["community_mqtt_email"] or "" + community_has_config = bool( + community_iata + or community_email + or (community_host and community_host != "mqtt-us-v1.letsmesh.net") + ) + if community_enabled or community_has_config: config = { - "broker_host": row["community_mqtt_broker_host"] or "mqtt-us-v1.letsmesh.net", + "broker_host": community_host or "mqtt-us-v1.letsmesh.net", "broker_port": row["community_mqtt_broker_port"] or 443, - "iata": row["community_mqtt_iata"] or "", - "email": row["community_mqtt_email"] or "", + "iata": community_iata, + "email": community_email, } scope = { @@ -2153,14 +2161,16 @@ async def _migrate_036_create_fanout_configs(conn: aiosqlite.Connection) -> None str(uuid.uuid4()), "mqtt_community", "Community MQTT", - 1, + 1 if community_enabled else 0, json.dumps(config), json.dumps(scope), sort_order, now, ), ) - logger.info("Migrated community MQTT settings to fanout_configs") + logger.info( + "Migrated community MQTT settings to fanout_configs (enabled=%s)", community_enabled + ) await conn.commit() diff --git a/app/routers/fanout.py b/app/routers/fanout.py index 3a0928a..a4dc357 100644 --- a/app/routers/fanout.py +++ b/app/routers/fanout.py @@ -167,6 +167,9 @@ async def update_fanout_config(config_id: str, body: FanoutConfigUpdate) -> dict if existing is None: raise HTTPException(status_code=404, detail="Fanout config not found") + if existing["type"] == "bot" and server_settings.disable_bots: + raise HTTPException(status_code=403, detail="Bot system disabled by server configuration") + kwargs = {} if body.name is not None: kwargs["name"] = body.name diff --git a/frontend/AGENTS.md b/frontend/AGENTS.md index 407e803..095bd53 100644 --- a/frontend/AGENTS.md +++ b/frontend/AGENTS.md @@ -241,15 +241,14 @@ LocalStorage migration helpers for favorites; canonical favorites are server-sid - `preferences_migrated` - `advert_interval` - `last_advert_time` -- `bots` -- `mqtt_broker_host`, `mqtt_broker_port`, `mqtt_username`, `mqtt_password` -- `mqtt_use_tls`, `mqtt_tls_insecure`, `mqtt_topic_prefix`, `mqtt_publish_messages`, `mqtt_publish_raw_packets` -- `community_mqtt_enabled`, `community_mqtt_iata`, `community_mqtt_broker_host`, `community_mqtt_broker_port`, `community_mqtt_email` - `flood_scope` - `blocked_keys`, `blocked_names` -`HealthStatus` includes `mqtt_status` (`"connected"`, `"disconnected"`, `"disabled"`, or `null`). -`HealthStatus` also includes `community_mqtt_status` with the same status values. +Note: MQTT, bot, and community MQTT settings were migrated to the `fanout_configs` table (managed via `/api/fanout`). They are no longer part of `AppSettings`. + +`HealthStatus` includes `fanout_statuses: Record` mapping config IDs to `{name, type, status}`. Also includes `bots_disabled: boolean`. + +`FanoutConfig` represents a single fanout integration: `{id, type, name, enabled, config, scope, sort_order, created_at}`. `RawPacket.decrypted_info` includes `channel_key` and `contact_key` for MQTT topic routing. diff --git a/frontend/src/components/settings/SettingsFanoutSection.tsx b/frontend/src/components/settings/SettingsFanoutSection.tsx index 5d2d57a..bac4560 100644 --- a/frontend/src/components/settings/SettingsFanoutSection.tsx +++ b/frontend/src/components/settings/SettingsFanoutSection.tsx @@ -477,8 +477,8 @@ function ScopeSelector({ onChange({ ...scope, messages: buildMessages(selectedChannels, current) }); }; - // Non-repeater contacts only (type 0) - const filteredContacts = contacts.filter((c) => c.type === 0); + // Exclude repeaters (2), rooms (3), and sensors (4) + const filteredContacts = contacts.filter((c) => c.type === 0 || c.type === 1); const modeDescriptions: Record = { all: 'All messages', @@ -1045,6 +1045,13 @@ export function SettingsFanoutSection({ Integrations are an experimental feature in open beta. + {health?.bots_disabled && ( +
+ Bot system is disabled by server configuration (MESHCORE_DISABLE_BOTS). Bot integrations + cannot be created or modified. +
+ )} +
Add: {TYPE_OPTIONS.filter((opt) => opt.value !== 'bot' || !health?.bots_disabled).map((opt) => (