From c68054ba46e94a7ca1369b567be1b34024671a5d Mon Sep 17 00:00:00 2001 From: Jack Kingsman Date: Sun, 24 May 2026 13:35:47 -0700 Subject: [PATCH 1/4] Don't display blocked contacts on the map. Closes #296. --- frontend/src/App.tsx | 2 + frontend/src/components/ConversationPane.tsx | 6 ++ frontend/src/components/MapView.tsx | 14 ++- frontend/src/test/mapView.test.tsx | 100 +++++++++++++++++++ 4 files changed, 121 insertions(+), 1 deletion(-) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 2ae1a42..9d22df3 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -680,6 +680,8 @@ export function App() { onToggleTrackedTelemetry: handleToggleTrackedTelemetry, repeaterAutoLoginKey, onClearRepeaterAutoLogin: () => setRepeaterAutoLoginKey(null), + blockedKeys: appSettings?.blocked_keys, + blockedNames: appSettings?.blocked_names, }; const searchProps = { contacts, diff --git a/frontend/src/components/ConversationPane.tsx b/frontend/src/components/ConversationPane.tsx index 6505cc8..942f399 100644 --- a/frontend/src/components/ConversationPane.tsx +++ b/frontend/src/components/ConversationPane.tsx @@ -92,6 +92,8 @@ interface ConversationPaneProps { onToggleTrackedTelemetry: (publicKey: string) => Promise; repeaterAutoLoginKey: string | null; onClearRepeaterAutoLogin: () => void; + blockedKeys?: string[]; + blockedNames?: string[]; } function LoadingPane({ label }: { label: string }) { @@ -171,6 +173,8 @@ export function ConversationPane({ onToggleTrackedTelemetry, repeaterAutoLoginKey, onClearRepeaterAutoLogin, + blockedKeys, + blockedNames, }: ConversationPaneProps) { const [roomAuthenticated, setRoomAuthenticated] = useState(false); const activeContactIsRepeater = useMemo(() => { @@ -215,6 +219,8 @@ export function ConversationPane({ focusedKey={activeConversation.mapFocusKey} rawPackets={rawPackets} config={config} + blockedKeys={blockedKeys} + blockedNames={blockedNames} onSelectContact={(contact) => onSelectConversation({ type: 'contact', diff --git a/frontend/src/components/MapView.tsx b/frontend/src/components/MapView.tsx index 6a1c6d1..fd08b66 100644 --- a/frontend/src/components/MapView.tsx +++ b/frontend/src/components/MapView.tsx @@ -30,6 +30,8 @@ interface MapViewProps { focusedKey?: string | null; rawPackets?: RawPacket[]; config?: RadioConfig | null; + blockedKeys?: string[]; + blockedNames?: string[]; /** When provided, the contact name in each popup becomes a clickable link * that opens the conversation for that contact (DM, repeater, or room). */ onSelectContact?: (contact: Contact) => void; @@ -496,6 +498,8 @@ export function MapView({ focusedKey, rawPackets, config, + blockedKeys, + blockedNames, onSelectContact, }: MapViewProps) { const [sevenDaysAgo] = useState(() => Date.now() / 1000 - 7 * 24 * 60 * 60); @@ -563,10 +567,14 @@ export function MapView({ // Filter contacts for map display const mappableContacts = useMemo(() => { + const isBlocked = (c: Contact) => + (blockedKeys?.length && blockedKeys.includes(c.public_key.toLowerCase())) || + (blockedNames?.length && c.name != null && blockedNames.includes(c.name)); + if (showPackets && discoveryMode) { // Discovery mode: only show nodes that have appeared in resolved packets return contacts.filter( - (c) => isValidLocation(c.lat, c.lon) && discoveredKeys.has(c.public_key) + (c) => isValidLocation(c.lat, c.lon) && discoveredKeys.has(c.public_key) && !isBlocked(c) ); } if (showPackets) { @@ -574,12 +582,14 @@ export function MapView({ return contacts.filter( (c) => isValidLocation(c.lat, c.lon) && + !isBlocked(c) && (c.public_key === focusedKey || (c.last_seen != null && c.last_seen > threeDaysAgoSec)) ); } return contacts.filter( (c) => isValidLocation(c.lat, c.lon) && + !isBlocked(c) && (c.public_key === focusedKey || (c.last_seen != null && c.last_seen > sevenDaysAgo)) ); }, [ @@ -590,6 +600,8 @@ export function MapView({ showPackets, discoveryMode, discoveredKeys, + blockedKeys, + blockedNames, ]); // Resolve a path of hop tokens to geographic waypoints (only unambiguous + has GPS) diff --git a/frontend/src/test/mapView.test.tsx b/frontend/src/test/mapView.test.tsx index db04638..d299796 100644 --- a/frontend/src/test/mapView.test.tsx +++ b/frontend/src/test/mapView.test.tsx @@ -172,4 +172,104 @@ describe('MapView', () => { vi.useRealTimers(); } }); + + it('excludes contacts whose public key is in blockedKeys', () => { + const visible: Contact = { + public_key: 'aa'.repeat(32), + name: 'Visible', + type: 1, + flags: 0, + direct_path: null, + direct_path_len: -1, + direct_path_hash_mode: -1, + route_override_path: null, + route_override_len: null, + route_override_hash_mode: null, + last_advert: null, + lat: 40, + lon: -74, + last_seen: Math.floor(Date.now() / 1000), + on_radio: false, + favorite: false, + last_contacted: null, + last_read_at: null, + first_seen: null, + }; + const blocked: Contact = { + public_key: 'bb'.repeat(32), + name: 'Blocked', + type: 2, + flags: 0, + direct_path: null, + direct_path_len: -1, + direct_path_hash_mode: -1, + route_override_path: null, + route_override_len: null, + route_override_hash_mode: null, + last_advert: null, + lat: 41, + lon: -73, + last_seen: Math.floor(Date.now() / 1000), + on_radio: false, + favorite: false, + last_contacted: null, + last_read_at: null, + first_seen: null, + }; + + render(); + + expect(screen.getByText('Visible')).toBeInTheDocument(); + expect(screen.queryByText('Blocked')).toBeNull(); + }); + + it('excludes contacts whose name is in blockedNames', () => { + const visible: Contact = { + public_key: 'aa'.repeat(32), + name: 'Visible', + type: 1, + flags: 0, + direct_path: null, + direct_path_len: -1, + direct_path_hash_mode: -1, + route_override_path: null, + route_override_len: null, + route_override_hash_mode: null, + last_advert: null, + lat: 40, + lon: -74, + last_seen: Math.floor(Date.now() / 1000), + on_radio: false, + favorite: false, + last_contacted: null, + last_read_at: null, + first_seen: null, + }; + const blocked: Contact = { + public_key: 'cc'.repeat(32), + name: 'BadActor', + type: 2, + flags: 0, + direct_path: null, + direct_path_len: -1, + direct_path_hash_mode: -1, + route_override_path: null, + route_override_len: null, + route_override_hash_mode: null, + last_advert: null, + lat: 41, + lon: -73, + last_seen: Math.floor(Date.now() / 1000), + on_radio: false, + favorite: false, + last_contacted: null, + last_read_at: null, + first_seen: null, + }; + + render(); + + expect(screen.getByText('Visible')).toBeInTheDocument(); + expect(screen.queryByText('BadActor')).toBeNull(); + }); }); From abfd45f7f1153fa2333283621d5c5297a49d7f11 Mon Sep 17 00:00:00 2001 From: fred777 Date: Wed, 27 May 2026 10:28:34 +0200 Subject: [PATCH 2/4] Introduce _bot_globals for persistent data storage between bot executions --- app/fanout/bot_exec.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/fanout/bot_exec.py b/app/fanout/bot_exec.py index 253cfed..cc6957b 100644 --- a/app/fanout/bot_exec.py +++ b/app/fanout/bot_exec.py @@ -39,6 +39,8 @@ BOT_MESSAGE_SPACING = 2.0 _bot_send_lock = asyncio.Lock() _last_bot_send_time: float = 0.0 +# global container for persistent data storage between bot executions, will be added to execution namespace +_bot_globals: dict[str,Any] = {} @dataclass(frozen=True) class BotCallPlan: @@ -185,6 +187,7 @@ def execute_bot_code( # Build execution namespace with allowed imports namespace: dict[str, Any] = { "__builtins__": __builtins__, + "_bot_globals": _bot_globals, } try: From 20150278b3072a90bb1dcc5928ee5501f53a4fc4 Mon Sep 17 00:00:00 2001 From: fred777 Date: Wed, 27 May 2026 10:33:26 +0200 Subject: [PATCH 3/4] DEFAULT_BOT_CODE: add usage example for bot_globals --- .../src/components/settings/SettingsFanoutSection.tsx | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/frontend/src/components/settings/SettingsFanoutSection.tsx b/frontend/src/components/settings/SettingsFanoutSection.tsx index 9d96532..2dc3323 100644 --- a/frontend/src/components/settings/SettingsFanoutSection.tsx +++ b/frontend/src/components/settings/SettingsFanoutSection.tsx @@ -103,6 +103,15 @@ const DEFAULT_BOT_CODE = `def bot(**kwargs) -> str | list[str] | None: # Don't reply to our own outgoing messages if is_outgoing: return None + + # If you want to make use of persistant data between calls to this function, + # you can put that data into the global _bot_globals dictionary, e.g.: + # + # bot_globals = globals()["_bot_globals"] + # if not "known_sender_names" in bot_globals: + # bot_globals["known_sender_names"] = set() + # + # bot_globals["known_sender_names"].add(sender_name) # Example: Only respond in #bot channel to "!pling" command if channel_name == "#bot" and "!pling" in message_text.lower(): From 08c4ec32836140721a417752a16579a16a55167f Mon Sep 17 00:00:00 2001 From: Jack Kingsman Date: Sat, 20 Jun 2026 17:54:46 -0700 Subject: [PATCH 4/4] Ruff me up --- app/fanout/bot_exec.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/fanout/bot_exec.py b/app/fanout/bot_exec.py index e8b4e19..edf93d0 100644 --- a/app/fanout/bot_exec.py +++ b/app/fanout/bot_exec.py @@ -40,7 +40,8 @@ _bot_send_lock = asyncio.Lock() _last_bot_send_time: float = 0.0 # global container for persistent data storage between bot executions, will be added to execution namespace -_bot_globals: dict[str,Any] = {} +_bot_globals: dict[str, Any] = {} + @dataclass(frozen=True) class BotCallPlan: