mirror of
https://github.com/MarekZegare4/MeshCore-Solo.git
synced 2026-07-26 14:58:12 +00:00
Bot: add DM contact target alongside channel
The bot can now reply to a specific DM contact instead of a channel. UI (BotScreen): item 1 cycles through all channels followed by all DM contacts (ADV_TYPE_CHAT). The label switches between "Channel" / "Contact" and the value shows the channel or contact name. Left/right/enter navigate the combined list. Logic (MyMesh::onMessageRecv): when bot_target_type==1, match the trigger against incoming DM text and reply via sendMessage(). A 4-byte pubkey prefix identifies the target; all-zero prefix matches any DM sender. Persistence: bot_target_type and bot_dm_pubkey[4] appended to prefs with backward-compat read guard (old firmware defaults to channel mode). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -258,6 +258,10 @@ void DataStore::loadPrefsInt(const char *filename, NodePrefs& _prefs, double& no
|
||||
file.read((uint8_t *)&_prefs.bot_channel_idx, sizeof(_prefs.bot_channel_idx));
|
||||
file.read((uint8_t *)_prefs.bot_trigger, sizeof(_prefs.bot_trigger));
|
||||
file.read((uint8_t *)_prefs.bot_reply, sizeof(_prefs.bot_reply));
|
||||
if (file.available()) {
|
||||
file.read((uint8_t *)&_prefs.bot_target_type, sizeof(_prefs.bot_target_type));
|
||||
file.read((uint8_t *)_prefs.bot_dm_pubkey, sizeof(_prefs.bot_dm_pubkey));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -322,6 +326,8 @@ void DataStore::savePrefs(const NodePrefs& _prefs, double node_lat, double node_
|
||||
file.write((uint8_t *)&_prefs.bot_channel_idx, sizeof(_prefs.bot_channel_idx));
|
||||
file.write((uint8_t *)_prefs.bot_trigger, sizeof(_prefs.bot_trigger));
|
||||
file.write((uint8_t *)_prefs.bot_reply, sizeof(_prefs.bot_reply));
|
||||
file.write((uint8_t *)&_prefs.bot_target_type, sizeof(_prefs.bot_target_type));
|
||||
file.write((uint8_t *)_prefs.bot_dm_pubkey, sizeof(_prefs.bot_dm_pubkey));
|
||||
|
||||
file.close();
|
||||
}
|
||||
|
||||
@@ -512,6 +512,33 @@ void MyMesh::onMessageRecv(const ContactInfo &from, mesh::Packet *pkt, uint32_t
|
||||
const char *text) {
|
||||
markConnectionActive(from); // in case this is from a server, and we have a connection
|
||||
queueMessage(from, TXT_TYPE_PLAIN, pkt, sender_timestamp, NULL, 0, text);
|
||||
|
||||
// DM auto-reply bot
|
||||
if (_prefs.bot_enabled && _prefs.bot_target_type == 1 &&
|
||||
_prefs.bot_trigger[0] && _prefs.bot_reply[0] &&
|
||||
millis() - _bot_last_reply_ms > 10000UL) {
|
||||
// all-zero pubkey = any sender; otherwise match first 4 bytes
|
||||
bool key_ok = (_prefs.bot_dm_pubkey[0] == 0 && _prefs.bot_dm_pubkey[1] == 0 &&
|
||||
_prefs.bot_dm_pubkey[2] == 0 && _prefs.bot_dm_pubkey[3] == 0) ||
|
||||
memcmp(from.id.pub_key, _prefs.bot_dm_pubkey, 4) == 0;
|
||||
if (key_ok) {
|
||||
const char* t = text;
|
||||
const char* tr = _prefs.bot_trigger;
|
||||
int tlen = strlen(tr);
|
||||
bool matched = false;
|
||||
for (; *t && !matched; t++) {
|
||||
int i = 0;
|
||||
while (i < tlen && tolower((uint8_t)t[i]) == tolower((uint8_t)tr[i])) i++;
|
||||
if (i == tlen) matched = true;
|
||||
}
|
||||
if (matched) {
|
||||
uint32_t ts = getRTCClock()->getCurrentTime();
|
||||
uint32_t expected_ack, est_timeout;
|
||||
if (sendMessage(from, ts, 0, _prefs.bot_reply, expected_ack, est_timeout) != MSG_SEND_FAILED)
|
||||
_bot_last_reply_ms = millis();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void MyMesh::onCommandDataRecv(const ContactInfo &from, mesh::Packet *pkt, uint32_t sender_timestamp,
|
||||
@@ -899,6 +926,8 @@ MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMe
|
||||
_prefs.bot_channel_idx = 0;
|
||||
_prefs.bot_trigger[0] = '\0';
|
||||
_prefs.bot_reply[0] = '\0';
|
||||
_prefs.bot_target_type = 0;
|
||||
memset(_prefs.bot_dm_pubkey, 0, sizeof(_prefs.bot_dm_pubkey));
|
||||
_prefs.auto_off_secs = 15; // 15 seconds auto-off by default
|
||||
_prefs.tz_offset_hours = 0; // UTC by default
|
||||
_prefs.low_batt_mv = 3400; // auto-shutdown at 3.4V by default
|
||||
|
||||
@@ -50,7 +50,9 @@ struct NodePrefs { // persisted to file
|
||||
uint8_t ringtone_notes[32]; // packed: bits0-2=pitch, bits3-4=octave-4, bits5-6=dur_idx
|
||||
uint16_t home_pages_mask; // bitmask of visible home pages (bit0=Clock..bit8=Shutdown); 0=all visible
|
||||
uint8_t bot_enabled; // 0=disabled, 1=enabled
|
||||
uint8_t bot_channel_idx; // channel index to monitor
|
||||
uint8_t bot_channel_idx; // channel index to monitor (when bot_target_type==0)
|
||||
char bot_trigger[64]; // trigger phrase (case-insensitive contains match)
|
||||
char bot_reply[140]; // auto-reply text
|
||||
uint8_t bot_target_type; // 0=channel (default), 1=DM contact
|
||||
uint8_t bot_dm_pubkey[4]; // pubkey prefix of DM target; all-zero = any DM sender
|
||||
};
|
||||
@@ -2108,10 +2108,16 @@ class BotScreen : public UIScreen {
|
||||
static const char KB_CHARS[4][10];
|
||||
static const int KB_ROWS = 4, KB_COLS = 10;
|
||||
|
||||
// channel count cache (refreshed on enter)
|
||||
int _num_channels;
|
||||
// channel + DM contact caches (refreshed on enter)
|
||||
static const int MAX_BOT_DM = 16;
|
||||
|
||||
int _num_channels;
|
||||
uint8_t _channel_indices[MAX_GROUP_CHANNELS];
|
||||
|
||||
int _num_dm;
|
||||
uint8_t _dm_pubkeys[MAX_BOT_DM][4];
|
||||
char _dm_names[MAX_BOT_DM][32];
|
||||
|
||||
void refreshChannels() {
|
||||
_num_channels = 0;
|
||||
ChannelDetails ch;
|
||||
@@ -2121,11 +2127,32 @@ class BotScreen : public UIScreen {
|
||||
}
|
||||
}
|
||||
|
||||
// returns display index of current bot_channel_idx in _channel_indices
|
||||
int currentChanDisplayIdx() const {
|
||||
for (int i = 0; i < _num_channels; i++)
|
||||
if (_channel_indices[i] == _prefs->bot_channel_idx) return i;
|
||||
return 0;
|
||||
void refreshContacts() {
|
||||
_num_dm = 0;
|
||||
ContactInfo ci;
|
||||
int n = the_mesh.getNumContacts();
|
||||
for (int i = 0; i < n && _num_dm < MAX_BOT_DM; i++) {
|
||||
if (!the_mesh.getContactByIdx(i, ci)) continue;
|
||||
if (ci.type != ADV_TYPE_CHAT) continue;
|
||||
memcpy(_dm_pubkeys[_num_dm], ci.id.pub_key, 4);
|
||||
strncpy(_dm_names[_num_dm], ci.name, sizeof(_dm_names[0]) - 1);
|
||||
_dm_names[_num_dm][sizeof(_dm_names[0]) - 1] = '\0';
|
||||
_num_dm++;
|
||||
}
|
||||
}
|
||||
|
||||
// returns combined index in [channels..., DM contacts...]
|
||||
int currentTargetIdx() const {
|
||||
if (_prefs->bot_target_type == 0) {
|
||||
for (int i = 0; i < _num_channels; i++)
|
||||
if (_channel_indices[i] == _prefs->bot_channel_idx) return i;
|
||||
return 0;
|
||||
} else {
|
||||
for (int i = 0; i < _num_dm; i++)
|
||||
if (memcmp(_dm_pubkeys[i], _prefs->bot_dm_pubkey, 4) == 0)
|
||||
return _num_channels + i;
|
||||
return _num_channels; // fallback: first DM
|
||||
}
|
||||
}
|
||||
|
||||
public:
|
||||
@@ -2135,6 +2162,7 @@ public:
|
||||
_sel = 0;
|
||||
_kb_field = -1;
|
||||
refreshChannels();
|
||||
refreshContacts();
|
||||
}
|
||||
|
||||
int render(DisplayDriver& display) override {
|
||||
@@ -2194,7 +2222,7 @@ public:
|
||||
display.drawTextCentered(display.width() / 2, 0, "AUTO-REPLY BOT");
|
||||
display.fillRect(0, 10, display.width(), 1);
|
||||
|
||||
static const char* labels[] = { "Enable", "Channel", "Trigger", "Reply" };
|
||||
static const char* labels[] = { "Enable", NULL, "Trigger", "Reply" };
|
||||
for (int i = 0; i < ITEM_COUNT; i++) {
|
||||
int y = START_Y + i * ITEM_H;
|
||||
bool sel = (i == _sel);
|
||||
@@ -2203,20 +2231,33 @@ public:
|
||||
display.setColor(DisplayDriver::DARK);
|
||||
}
|
||||
display.setCursor(2, y);
|
||||
display.print(labels[i]);
|
||||
if (i == 1)
|
||||
display.print(_prefs->bot_target_type == 0 ? "Channel" : "Contact");
|
||||
else
|
||||
display.print(labels[i]);
|
||||
display.setCursor(VAL_X, y);
|
||||
|
||||
if (i == 0) {
|
||||
display.print(_prefs->bot_enabled ? "ON" : "OFF");
|
||||
} else if (i == 1) {
|
||||
if (_num_channels == 0) {
|
||||
int total = _num_channels + _num_dm;
|
||||
if (total == 0) {
|
||||
display.print("none");
|
||||
} else {
|
||||
} else if (_prefs->bot_target_type == 0) {
|
||||
ChannelDetails ch;
|
||||
if (the_mesh.getChannel(_prefs->bot_channel_idx, ch) && ch.name[0])
|
||||
if (_num_channels > 0 && the_mesh.getChannel(_prefs->bot_channel_idx, ch) && ch.name[0])
|
||||
display.drawTextEllipsized(VAL_X, y, display.width() - VAL_X - 1, ch.name);
|
||||
else
|
||||
display.print("Ch?");
|
||||
display.print("?");
|
||||
} else {
|
||||
bool found = false;
|
||||
for (int j = 0; j < _num_dm && !found; j++) {
|
||||
if (memcmp(_dm_pubkeys[j], _prefs->bot_dm_pubkey, 4) == 0) {
|
||||
display.drawTextEllipsized(VAL_X, y, display.width() - VAL_X - 1, _dm_names[j]);
|
||||
found = true;
|
||||
}
|
||||
}
|
||||
if (!found) display.print("?");
|
||||
}
|
||||
} else if (i == 2) {
|
||||
const char* tr = _prefs->bot_trigger;
|
||||
@@ -2297,11 +2338,23 @@ public:
|
||||
_prefs->bot_enabled ^= 1;
|
||||
return true;
|
||||
}
|
||||
if (_sel == 1 && _num_channels > 0) {
|
||||
int idx = currentChanDisplayIdx();
|
||||
if (right || enter) idx = (idx + 1) % _num_channels;
|
||||
if (left) idx = (idx + _num_channels - 1) % _num_channels;
|
||||
if (left || right || enter) { _prefs->bot_channel_idx = _channel_indices[idx]; return true; }
|
||||
if (_sel == 1) {
|
||||
int total = _num_channels + _num_dm;
|
||||
if (total == 0) return false;
|
||||
int idx = currentTargetIdx();
|
||||
if (right || enter) idx = (idx + 1) % total;
|
||||
if (left) idx = (idx + total - 1) % total;
|
||||
if (left || right || enter) {
|
||||
if (idx < _num_channels) {
|
||||
_prefs->bot_target_type = 0;
|
||||
_prefs->bot_channel_idx = _channel_indices[idx];
|
||||
} else {
|
||||
int di = idx - _num_channels;
|
||||
_prefs->bot_target_type = 1;
|
||||
memcpy(_prefs->bot_dm_pubkey, _dm_pubkeys[di], 4);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if ((_sel == 2 || _sel == 3) && enter) {
|
||||
_kb_field = _sel;
|
||||
|
||||
Reference in New Issue
Block a user