feat(bot): hardening + auto-responder, query commands, quiet hours

Reply-bot overhaul on top of the trigger/reply auto-reply:

Robustness
- single botTriggerMatches() shared by DM/channel paths; named constants
  (BOT_REPLY_COOLDOWN_MS / BOT_SCRATCH) replace magic numbers.
- per-contact DM throttle (8-entry ring) so a second sender isn't starved
  while one contact is on cooldown.
- channel anti-loop: skip only when an incoming message equals our own reply
  (was: any reply containing the trigger word — which silently killed channel
  replies for the common "trigger word in reply" setup).

Features
- away / reply-to-all: a lone "*" trigger matches every message.
- independent DM vs channel triggers (bot_trigger / bot_trigger_ch); "*" works
  on the channel too, bounded by cooldown + echo guard.
- query commands: a DM or monitored-channel message is scanned for "!word"
  tokens — !ping/!batt/!loc/!time/!temp/!hops/!status/!help — merged into one
  combined reply. DM = per-contact throttle, ignores quiet hours; channel =
  broadcast, per-channel cooldown, respects quiet hours. !hops uses
  getPathHashCount() (0 = direct).
- quiet hours (bot_quiet_start/end, local, wraps midnight) silence push replies.
- reply counter shown in the BotScreen header.

UI / storage
- BotScreen: 9 rows, scrolling list (no more cramming), field I/O via
  fieldBuf()/fieldCap().
- prefs bot_commands_enabled / bot_quiet_start / bot_quiet_end / bot_trigger_ch
  persisted; schema 0xC0DE000A → 0xC0DE000C with clamping + migration
  (bot_trigger_ch seeded from bot_trigger on upgrade).
- docs: tools_screen bot section rewritten; FEATURES.md refreshed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
MarekZegare4
2026-06-15 11:57:27 +02:00
co-authored by Claude Opus 4.8
parent 8c0ccaed73
commit 8bd6fbf1cb
8 changed files with 410 additions and 100 deletions
+18 -1
View File
@@ -245,9 +245,20 @@ public:
// To check if there is pending work
bool hasPendingWork() const;
// Number of auto-replies sent since boot (DM + channel). Shown on BotScreen.
uint16_t botReplyCount() const { return _bot_reply_count; }
private:
void tryBotReplyDM(const ContactInfo& from, const char* text);
void tryBotReplyChannel(uint8_t channel_idx, const char* text);
bool tryBotCommand(const ContactInfo& from, const char* text, uint8_t hops); // DM commands
bool tryBotChannelCommand(uint8_t channel_idx, const char* text, uint8_t hops); // channel commands
bool botCommandReply(const char* cmd, uint8_t hops, uint32_t ts, char* out, int out_len); // one command → reply text
int botScanCommands(const char* body, uint8_t hops, uint32_t ts, char* out, int out_len); // scan "!word"s → combined reply, returns count
bool botTriggerMatches(const char* trigger, const char* body, bool allow_wildcard) const;
bool botInQuietHours() const; // true when auto-replies should stay silent
bool botDmAllowed(const uint8_t* pubkey); // per-contact DM throttle: ok to reply?
void botDmRecord(const uint8_t* pubkey); // remember we just replied to this contact
void writeOKFrame();
void writeErrFrame(uint8_t err_code);
@@ -320,10 +331,16 @@ private:
uint8_t *sign_data;
uint32_t sign_data_len;
unsigned long dirty_contacts_expiry;
unsigned long _bot_last_dm_reply_ms;
unsigned long _bot_last_ch_reply_ms;
unsigned long _next_auto_advert_ms;
// Per-contact DM reply throttle: a small ring of the most recent recipients so
// one chatty contact can't be spammed while a different sender is still served.
struct BotReplyLog { uint8_t key[4]; unsigned long t_ms; bool used; };
static const int BOT_DM_LOG_SIZE = 8;
BotReplyLog _bot_dm_log[BOT_DM_LOG_SIZE];
uint16_t _bot_reply_count; // total auto-replies sent since boot
TransportKey send_scope;
uint8_t cmd_frame[MAX_FRAME_SIZE + 1];