From e2a911838bfa687457bb89ba5d4984dd398e87a5 Mon Sep 17 00:00:00 2001 From: pe1hvh Date: Sat, 14 Mar 2026 20:38:32 +0100 Subject: [PATCH] feat(bbs): DM-based BBS with channel-based access, multi-channel whitelist, short syntax(#v1.14.0) Adds an offline BBS accessible via Direct Message to the node's own key. Access is channel-based: anyone seen on a configured BBS channel is automatically whitelisted for DM access. Channels stay clean. - Multi-channel configuration: any combination of device channels can be selected; senders on any of them are auto-whitelisted - Short syntax: !p and !r [cat] alongside full !bbs syntax - Category abbreviations computed automatically (shortest unique prefix) - handle_channel_msg: bootstrap reply on channel + auto-whitelist sender - handle_dm: DM entry point, checks whitelist, routes to post/read/help - DM reply routed back to sender via command_sink - SQLite message store with WAL mode and configurable retention --- README.md | 80 ++++++++++++++-------------- meshcore_gui/services/bbs_service.py | 53 +++++++++--------- 2 files changed, 69 insertions(+), 64 deletions(-) diff --git a/README.md b/README.md index 1b056d0..8e72d38 100644 --- a/README.md +++ b/README.md @@ -1188,22 +1188,22 @@ meshcore-gui/ MeshCore GUI includes an offline BBS that lets mesh nodes exchange structured messages by category, with optional region tagging. -### Toegangsmodel +### Access model -De beheerder koppelt één of meer channels aan het BBS. Iedereen die op zo'n channel een bericht stuurt wordt automatisch gewhitelist. Daarna kunnen zij commando's sturen via **Direct Message** aan de BBS-node — het channel zelf blijft schoon. +The operator links one or more channels to the BBS. Anyone who sends a message on a configured BBS channel is automatically added to the whitelist. After that, they can send commands via **Direct Message** to the BBS node — the channel itself stays clean. ``` -Eerste contact: !bbs help op het channel - → node ziet de public key → whitelist -Daarna: !p U hulp nodig als DM naar de node - → verwerkt, reply via DM terug +First contact: !bbs help on the configured channel + → node sees the public key → whitelists it +After that: !p U need assistance as DM to the node + → processed, reply sent back via DM ``` -Wie nooit iets heeft gestuurd op een geconfigureerd channel staat niet op de whitelist en wordt silently genegeerd. +Anyone who has never sent a message on a configured channel is not on the whitelist and is silently ignored. ### Settings -Open via het tandwiel (⚙) in het BBS-panel, of navigeer naar `/bbs-settings`. +Open via the gear icon (⚙) in the BBS panel, or navigate to `/bbs-settings`. ``` BBS Settings @@ -1217,65 +1217,65 @@ Retain: 48 hours ▶ Advanced Regions (comma-separated) - Allowed keys (leeg = auto-geleerd via channel-activiteit) + Allowed keys (empty = auto-learned from channel activity) ``` -- **Channels** — vink alle channels aan waarvan deelnemers toegang krijgen tot het BBS. -- **Categories** — komma-gescheiden lijst van geldige categorie-tags. -- **Retain** — berichtretentie in uren (standaard 48). -- **Advanced → Regions** — optionele regio-tags voor geografische filtering. -- **Advanced → Allowed keys** — handmatige whitelist-override; leeg laten om alleen automatisch te leren. +- **Channels** — check all channels whose participants should have access to the BBS. Multiple channels can be selected. +- **Categories** — comma-separated list of valid category tags. +- **Retain** — message retention in hours (default 48). +- **Advanced → Regions** — optional region tags for geographic filtering. +- **Advanced → Allowed keys** — manual whitelist override; leave empty to rely on auto-learned keys only. -### Commando-syntax +### Command syntax -#### Korte syntax +#### Short syntax -| Commando | Beschrijving | +| Command | Description | |---|---| -| `!p ` | Bericht posten | -| `!p ` | Posten met regio | -| `!r` | Laatste 5 berichten lezen (alle categorieën) | -| `!r ` | Lezen gefilterd op categorie | -| `!r ` | Lezen gefilterd op regio en categorie | +| `!p ` | Post a message | +| `!p ` | Post with region | +| `!r` | Read 5 most recent messages (all categories) | +| `!r ` | Read filtered by category | +| `!r ` | Read filtered by region and category | -Categorie-afkortingen worden automatisch berekend als de kortste unieke prefix. Voorbeeld met `URGENT, MEDICAL, LOGISTICS, STATUS, GENERAL`: +Category abbreviations are computed automatically as the shortest unique prefix within the configured list. Example with `URGENT, MEDICAL, LOGISTICS, STATUS, GENERAL`: ``` U=URGENT M=MEDICAL L=LOGISTICS S=STATUS G=GENERAL ``` -`!r` zonder argumenten en `!bbs help` geven altijd de afkortingstabel mee. +If two categories share the same leading letters (e.g. `MEDICAL` and `MISSING`), longer prefixes are calculated automatically: `ME` and `MI`. The `!r` (without arguments) and `!bbs help` replies always include the current abbreviation table. -#### Volledige syntax +#### Full syntax -| Commando | Beschrijving | +| Command | Description | |---|---| -| `!bbs help` | Toon commando's en afkortingstabel | -| `!bbs post ` | Bericht posten | -| `!bbs post ` | Posten met regio | -| `!bbs read` | Laatste 5 berichten | -| `!bbs read ` | Gefilterd op categorie | -| `!bbs read ` | Gefilterd op regio en categorie | +| `!bbs help` | Show commands and abbreviation table | +| `!bbs post ` | Post a message | +| `!bbs post ` | Post with region | +| `!bbs read` | Read 5 most recent messages | +| `!bbs read ` | Read filtered by category | +| `!bbs read ` | Read filtered by region and category | -#### Voorbeeld help-reply +#### Example help reply ``` BBS [NoodNet Zwolle, NoodNet Dalfsen] | !p [cat] [text] | !r [cat] | U=URGENT M=MEDICAL L=LOGISTICS S=STATUS G=GENERAL ``` -### Foutafhandeling +### Error handling -| Situatie | Reply | +| Situation | Reply | |---|---| -| Onbekende categorie | Lijst met geldige categorieën en afkortingen | -| Ambigue afkorting | Lijst met overeenkomende categorieën | -| Sender niet op whitelist | Silent drop — geen reply | +| Unknown category | Lists valid categories and abbreviations | +| Ambiguous abbreviation | Lists all matching categories | +| Sender not on whitelist | Silent drop — no reply | ### Storage ``` -~/.meshcore-gui/bbs/bbs_messages.db — SQLite berichtenopslag (WAL-mode) -~/.meshcore-gui/bbs/bbs_config.json — Board-configuratie +~/.meshcore-gui/bbs/bbs_messages.db — SQLite message store (WAL mode) +~/.meshcore-gui/bbs/bbs_config.json — Board configuration ``` --- diff --git a/meshcore_gui/services/bbs_service.py b/meshcore_gui/services/bbs_service.py index e73ff18..9f04bc7 100644 --- a/meshcore_gui/services/bbs_service.py +++ b/meshcore_gui/services/bbs_service.py @@ -536,34 +536,32 @@ class BbsCommandHandler: """Handle ``!p [region] ``.""" regions = board.regions categories = board.categories - tokens = args.split(None, 2) if args else [] + + # First token may be a region; split loosely to detect it + first_tokens = args.split(None, 1) if args else [] region = "" - if regions and tokens: - resolved_r = self._resolve_region(tokens[0], regions) + remainder = args # everything after optional region + + if regions and first_tokens: + resolved_r = self._resolve_region(first_tokens[0], regions) if resolved_r: region = resolved_r - tokens = tokens[1:] # consume region token + remainder = first_tokens[1] if len(first_tokens) > 1 else "" - # Now tokens should be [abbrev, text] - if len(tokens) < 2: + # Split remainder into exactly [category/abbrev, full_text] + cat_and_text = remainder.split(None, 1) if remainder else [] + + if len(cat_and_text) < 2: abbr = self._abbrev_table(categories) - return ( - f"Usage: !p [region] | {abbr}" - ) + return f"Usage: !p [region] | {abbr}" - cat_token, text = tokens[0], tokens[1] if len(tokens) >= 2 else "" - # Rebuild text in case split(None,2) on a shorter string - if len(args.split(None, 2 if not region else 3)) > (2 if not region else 3): - # re-split with region consumed - pass + cat_token, text = cat_and_text[0], cat_and_text[1] category = self._resolve_category(cat_token, categories) if category is None: abbr = self._abbrev_table(categories) - return ( - f"Unknown category '{cat_token}'. Valid: {abbr}" - ) + return f"Unknown category '{cat_token}'. Valid: {abbr}" msg = BbsMessage( channel=channel_idx, @@ -609,21 +607,28 @@ class BbsCommandHandler: """Handle ``!bbs post [region] ``.""" regions = board.regions categories = board.categories - tokens = args.split(None, 2) if args else [] + + # First token may be a region; split loosely to detect it + first_tokens = args.split(None, 1) if args else [] region = "" - if regions and tokens: - resolved_r = self._resolve_region(tokens[0], regions) + remainder = args # everything after optional region + + if regions and first_tokens: + resolved_r = self._resolve_region(first_tokens[0], regions) if resolved_r: region = resolved_r - tokens = tokens[1:] + remainder = first_tokens[1] if len(first_tokens) > 1 else "" - if len(tokens) < 2: + # Split remainder into exactly [category, full_text] + cat_and_text = remainder.split(None, 1) if remainder else [] + + if len(cat_and_text) < 2: abbr = self._abbrev_table(categories) - region_hint = f" [region]" if regions else "" + region_hint = " [region]" if regions else "" return f"Usage: !bbs post{region_hint} | {abbr}" - cat_token, text = tokens[0], tokens[1] + cat_token, text = cat_and_text[0], cat_and_text[1] category = self._resolve_category(cat_token, categories) if category is None: abbr = self._abbrev_table(categories)