mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-09-27 02:49:21 +00:00
feat: ping-bot also triggers on "/ping", not just bare "ping"
Trigger check moved from a single string comparison to a small pingTriggerWords set (mirrored by hand in db.go and channels.js), so adding more trigger words later is a one-line change in each. Still an exact match after the existing @mention-stripping -- "/pingx" etc. don't match. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
5842d85abd
commit
653949479a
+10
-2
@@ -1919,13 +1919,21 @@ func (db *DB) GetEncryptedChannels(region ...string) ([]map[string]interface{},
|
||||
// a bare "ping".
|
||||
var channelMentionPrefixRe = regexp.MustCompile(`^@[A-Za-z0-9_-]{1,32}\s+`)
|
||||
|
||||
// pingTriggerWords are the exact (case-insensitive) message bodies that
|
||||
// trigger a pong reply. Mirrored by pingTriggerWords in
|
||||
// public/channels.js -- keep both lists in sync by hand.
|
||||
var pingTriggerWords = map[string]bool{
|
||||
"ping": true,
|
||||
"/ping": true,
|
||||
}
|
||||
|
||||
// isPingTrigger reports whether displayText, after stripping a leading
|
||||
// "@target " mention the same way the frontend does (public/channels.js
|
||||
// replyMatch), is exactly "ping".
|
||||
// replyMatch), exactly matches one of pingTriggerWords.
|
||||
func isPingTrigger(displayText string) bool {
|
||||
trigger := strings.TrimSpace(displayText)
|
||||
trigger = channelMentionPrefixRe.ReplaceAllString(trigger, "")
|
||||
return strings.EqualFold(strings.TrimSpace(trigger), "ping")
|
||||
return pingTriggerWords[strings.ToLower(strings.TrimSpace(trigger))]
|
||||
}
|
||||
|
||||
// pingBotReply synthesizes a "pong" reply for a channel message whose
|
||||
|
||||
+17
-2
@@ -1527,12 +1527,19 @@ func TestGetChannelMessages_PingBotReply(t *testing.T) {
|
||||
db.conn.Exec(`INSERT INTO observations (transmission_id, observer_idx, snr, rssi, path_json, timestamp)
|
||||
VALUES (4, 1, 3.0, -99, '[]', 1736935380)`)
|
||||
|
||||
// tx5: "/ping" -- the slash-command form must trigger too.
|
||||
db.conn.Exec(`INSERT INTO transmissions (raw_hex, hash, first_seen, route_type, payload_type, decoded_json, channel_hash)
|
||||
VALUES ('EE', 'chanmsg00000005', '2026-01-15T10:04:00Z', 1, 5,
|
||||
'{"type":"CHAN","channel":"#ping","text":"/ping","sender":"Frank"}', '#ping')`)
|
||||
db.conn.Exec(`INSERT INTO observations (transmission_id, observer_idx, snr, rssi, path_json, timestamp)
|
||||
VALUES (5, 1, 6.0, -91, '["aa"]', 1736935440)`)
|
||||
|
||||
messages, total, err := db.GetChannelMessages("#ping", 100, 0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if total != 4 {
|
||||
t.Fatalf("expected 4 messages, got %d", total)
|
||||
if total != 5 {
|
||||
t.Fatalf("expected 5 messages, got %d", total)
|
||||
}
|
||||
|
||||
byText := map[string]map[string]interface{}{}
|
||||
@@ -1572,6 +1579,14 @@ func TestGetChannelMessages_PingBotReply(t *testing.T) {
|
||||
if mentionReply["hops"] != 0 {
|
||||
t.Errorf("mention-prefixed ping botReply hops = %v, want 0 (empty path)", mentionReply["hops"])
|
||||
}
|
||||
|
||||
slashReply, _ := byText["/ping"]["botReply"].(map[string]interface{})
|
||||
if slashReply == nil {
|
||||
t.Fatal("\"/ping\" should get a botReply -- it's in pingTriggerWords alongside bare \"ping\"")
|
||||
}
|
||||
if slashReply["sender"] != "CoreScopeBot" {
|
||||
t.Errorf("\"/ping\" botReply sender = %v, want CoreScopeBot", slashReply["sender"])
|
||||
}
|
||||
}
|
||||
|
||||
// TestGetChannelMessages_PingBotReply_MultiObservation covers a single
|
||||
|
||||
+6
-2
@@ -335,9 +335,12 @@
|
||||
// path (repeater names) -- the live WS broadcast doesn't carry a
|
||||
// per-packet resolved_path, only REST-loaded history does (via
|
||||
// GetChannelMessages). scope/area ARE available live and are included.
|
||||
// pingTriggerWords mirrors pingTriggerWords in cmd/server/db.go -- keep
|
||||
// both lists in sync by hand.
|
||||
var pingTriggerWords = { 'ping': true, '/ping': true };
|
||||
function pingBotReply(text, hops, snr, observer, scope, area) {
|
||||
var trigger = String(text || '').trim().replace(/^@[A-Za-z0-9_-]{1,32}\s+/, '').trim();
|
||||
if (trigger.toLowerCase() !== 'ping') return null;
|
||||
if (!pingTriggerWords[trigger.toLowerCase()]) return null;
|
||||
var parts = [hops > 0 ? (hops + ' hop' + (hops === 1 ? '' : 's')) : '0 hops (direct)'];
|
||||
if (snr !== null && snr !== undefined) parts.push('SNR ' + Number(snr).toFixed(1) + 'dB');
|
||||
if (observer) parts.push('heard by ' + observer);
|
||||
@@ -2324,7 +2327,8 @@
|
||||
const safeId = btoa(encodeURIComponent(sender));
|
||||
|
||||
// Ping-bot reply (server-synthesized in GetChannelMessages when this
|
||||
// message's text is exactly "ping" -- see pingBotReply in db.go).
|
||||
// message's text matches a trigger word (pingTriggerWords) -- see
|
||||
// pingBotReply in db.go).
|
||||
// CoreScope-only: never transmitted back onto the mesh, since
|
||||
// CoreScope has no publish path to a MeshCore broker/radio. The
|
||||
// "Not sent to the mesh" caveat is load-bearing, not decoration --
|
||||
|
||||
@@ -86,6 +86,14 @@ test('exact "ping" (any case) triggers a reply', () => {
|
||||
assert.ok(fn(' ping ', 1, 5, 'Obs') !== null, 'surrounding whitespace should be trimmed');
|
||||
});
|
||||
|
||||
test('"/ping" (the slash-command form) also triggers, alongside bare "ping"', () => {
|
||||
const { ctx } = makeSandbox();
|
||||
const fn = ctx.window._channelsPingBotReplyForTest;
|
||||
assert.ok(fn('/ping', 1, 5, 'Obs') !== null);
|
||||
assert.ok(fn('/PING', 1, 5, 'Obs') !== null, 'case-insensitive like the bare form');
|
||||
assert.strictEqual(fn('/pingx', 1, 5, 'Obs'), null, 'still an exact match, not a prefix match');
|
||||
});
|
||||
|
||||
test('a mention prefix like "@CoreScopeBot ping" is stripped before matching', () => {
|
||||
const { ctx } = makeSandbox();
|
||||
const fn = ctx.window._channelsPingBotReplyForTest;
|
||||
|
||||
Reference in New Issue
Block a user