diff --git a/README.md b/README.md index c918cf4..598622d 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ Backend server + browser interface for MeshCore mesh radio networks. Connect you * Run multiple Python bots that can analyze messages and respond to DMs and channels * Monitor unlimited contacts and channels (radio limits don't apply -- packets are decrypted server-side) * Access your radio remotely over your network or VPN -* Search for hashtag room names for channels you don't have keys for yet +* Search for hashtag channel names for channels you don't have keys for yet * Forward packets to MQTT, LetsMesh, MeshRank, SQS, Apprise, etc. * Use the more recent 1.14 firmwares which support multibyte pathing * Visualize the mesh as a map or node set, view repeater stats, and more! diff --git a/README_ADVANCED.md b/README_ADVANCED.md index f061324..64aba19 100644 --- a/README_ADVANCED.md +++ b/README_ADVANCED.md @@ -21,7 +21,7 @@ If the audit finds a mismatch, you'll see an error in the application UI and you ## HTTPS -WebGPU room-finding requires a secure context when you are not on `localhost`. +WebGPU channel-finding requires a secure context when you are not on `localhost`. Generate a local cert and start the backend with TLS: diff --git a/app/AGENTS.md b/app/AGENTS.md index f87c8a8..105fe7f 100644 --- a/app/AGENTS.md +++ b/app/AGENTS.md @@ -101,7 +101,7 @@ app/ - Packet `path_len` values are hop counts, not byte counts. - Hop width comes from the packet or radio `path_hash_mode`: `0` = 1-byte, `1` = 2-byte, `2` = 3-byte. - Channel slot count comes from firmware-reported `DEVICE_INFO.max_channels`; do not hardcode `40` when scanning/offloading channel slots. -- Channel sends use a session-local LRU slot cache after startup channel offload clears the radio. Repeated sends to the same room reuse the loaded slot; new rooms fill free slots up to the discovered channel capacity, then evict the least recently used cached room. +- Channel sends use a session-local LRU slot cache after startup channel offload clears the radio. Repeated sends to the same channel reuse the loaded slot; new channels fill free slots up to the discovered channel capacity, then evict the least recently used cached channel. - TCP radios do not reuse cached slot contents. For TCP, channel sends still force `set_channel(...)` before every send because this backend does not have exclusive device access. - `MESHCORE_FORCE_CHANNEL_SLOT_RECONFIGURE=true` disables slot reuse on all transports and forces the old always-`set_channel(...)` behavior before every channel send. - Contacts persist canonical direct-route fields (`direct_path`, `direct_path_len`, `direct_path_hash_mode`) so contact sync and outbound DM routing reuse the exact stored hop width instead of inferring from path bytes. diff --git a/app/models.py b/app/models.py index 214d884..cf91209 100644 --- a/app/models.py +++ b/app/models.py @@ -266,7 +266,7 @@ class ContactNameHistory(BaseModel): class ContactActiveRoom(BaseModel): - """A channel/room where a contact has been active.""" + """A channel where a contact has been active.""" channel_key: str channel_name: str diff --git a/app/routers/channels.py b/app/routers/channels.py index ca4de07..fb3f94f 100644 --- a/app/routers/channels.py +++ b/app/routers/channels.py @@ -71,7 +71,7 @@ async def create_channel(request: CreateChannelRequest) -> Channel: requested_name = request.name is_hashtag = requested_name.startswith("#") - # Reserve the canonical Public room so it cannot drift to another key, + # Reserve the canonical Public channel so it cannot drift to another key, # and the well-known Public key cannot be renamed to something else. if is_public_channel_name(requested_name): if request.key: diff --git a/frontend/src/components/ChannelFloodScopeOverrideModal.tsx b/frontend/src/components/ChannelFloodScopeOverrideModal.tsx index be8b097..11dbf71 100644 --- a/frontend/src/components/ChannelFloodScopeOverrideModal.tsx +++ b/frontend/src/components/ChannelFloodScopeOverrideModal.tsx @@ -45,8 +45,8 @@ export function ChannelFloodScopeOverrideModal({ Regional Override - Room-level regional routing temporarily changes the radio flood scope before send and - restores it after. This can noticeably slow room sends. + Channel-level regional routing temporarily changes the radio flood scope before send and + restores it after. This can noticeably slow channel sends. diff --git a/frontend/src/components/ChatHeader.tsx b/frontend/src/components/ChatHeader.tsx index e9642b4..d92c4b3 100644 --- a/frontend/src/components/ChatHeader.tsx +++ b/frontend/src/components/ChatHeader.tsx @@ -201,7 +201,9 @@ export function ChatHeader({ e.stopPropagation(); navigator.clipboard.writeText(conversation.id); toast.success( - conversation.type === 'channel' ? 'Room key copied!' : 'Contact key copied!' + conversation.type === 'channel' + ? 'Channel key copied!' + : 'Contact key copied!' ); }} title="Click to copy" diff --git a/frontend/src/components/ContactInfoPane.tsx b/frontend/src/components/ContactInfoPane.tsx index f9abe0b..1daea1d 100644 --- a/frontend/src/components/ContactInfoPane.tsx +++ b/frontend/src/components/ContactInfoPane.tsx @@ -242,8 +242,8 @@ export function ContactInfoPane({ - @@ -515,8 +515,8 @@ export function ContactInfoPane({ - @@ -588,23 +588,23 @@ function MessageStatsSection({ ); } -function MostActiveRoomsSection({ - rooms, +function MostActiveChannelsSection({ + channels, onNavigateToChannel, }: { - rooms: ContactActiveRoom[]; + channels: ContactActiveRoom[]; onNavigateToChannel?: (channelKey: string) => void; }) { - if (rooms.length === 0) { + if (channels.length === 0) { return null; } return (
- Most Active Rooms + Most Active Channels
- {rooms.map((room) => ( -
+ {channels.map((channel) => ( +
onNavigateToChannel?.(room.channel_key)} + onClick={() => onNavigateToChannel?.(channel.channel_key)} > - {room.channel_name.startsWith('#') || isPublicChannelKey(room.channel_key) - ? room.channel_name - : `#${room.channel_name}`} + {channel.channel_name.startsWith('#') || isPublicChannelKey(channel.channel_key) + ? channel.channel_name + : `#${channel.channel_name}`} - {room.message_count.toLocaleString()} msg - {room.message_count !== 1 ? 's' : ''} + {channel.message_count.toLocaleString()} msg + {channel.message_count !== 1 ? 's' : ''}
))} diff --git a/frontend/src/components/CrackerPanel.tsx b/frontend/src/components/CrackerPanel.tsx index b3c1c68..d965906 100644 --- a/frontend/src/components/CrackerPanel.tsx +++ b/frontend/src/components/CrackerPanel.tsx @@ -7,8 +7,8 @@ import { toast } from './ui/sonner'; import { cn } from '@/lib/utils'; import { extractPacketPayloadHex } from '../utils/pathUtils'; -interface CrackedRoom { - roomName: string; +interface CrackedChannel { + channelName: string; key: string; packetId: number; message: string; @@ -45,7 +45,7 @@ export function CrackerPanel({ const [twoWordMode, setTwoWordMode] = useState(false); const [progress, setProgress] = useState(null); const [queue, setQueue] = useState>(new Map()); - const [crackedRooms, setCrackedRooms] = useState([]); + const [crackedChannels, setCrackedChannels] = useState([]); const [wordlistLoaded, setWordlistLoaded] = useState(false); const [gpuAvailable, setGpuAvailable] = useState(null); const [undecryptedPacketCount, setUndecryptedPacketCount] = useState(null); @@ -325,14 +325,14 @@ export function CrackerPanel({ return updated; }); - const newRoom: CrackedRoom = { - roomName: result.roomName, + const newCracked: CrackedChannel = { + channelName: result.roomName, key: result.key, packetId: nextId!, message: result.decryptedMessage || '', crackedAt: Date.now(), }; - setCrackedRooms((prev) => [...prev, newRoom]); + setCrackedChannels((prev) => [...prev, newCracked]); // Auto-add channel if not already exists const keyUpper = result.key.toUpperCase(); @@ -580,20 +580,20 @@ export function CrackerPanel({
)} - {/* Cracked rooms list */} - {crackedRooms.length > 0 && ( + {/* Cracked channels list */} + {crackedChannels.length > 0 && (
-
Cracked Rooms:
+
Cracked Channels:
- {crackedRooms.map((room, i) => ( + {crackedChannels.map((channel, i) => (
- #{room.roomName} + #{channel.channelName} - "{room.message.slice(0, 50)} - {room.message.length > 50 ? '...' : ''}" + "{channel.message.slice(0, 50)} + {channel.message.length > 50 ? '...' : ''}"
))} @@ -604,8 +604,8 @@ export function CrackerPanel({

For unknown-keyed GroupText packets, this will attempt to dictionary attack, then brute - force payloads as they arrive, testing room names up to the specified length to discover - active rooms on the local mesh (GroupText packets may not be hashtag messages; we have no + force payloads as they arrive, testing channel names up to the specified length to discover + active channels on the local mesh (GroupText packets may not be hashtag messages; we have no way of knowing but try as if they are). Retry failed at n+1 will let the cracker return to the failed queue and pick up messages it couldn't crack, attempting them at one longer length. @@ -613,8 +613,8 @@ export function CrackerPanel({ concatenated together (e.g. "hello" + "world" = "#helloworld") after the single-word dictionary pass; this can substantially increase search time and also result in false-positives. - Decrypt historical will run an async job on any room name it finds to see - if any historically captured packets will decrypt with that key. + Decrypt historical will run an async job on any channel name it finds to + see if any historically captured packets will decrypt with that key. Turbo mode will push your GPU to the max (target dispatch time of 10s) and may allow accelerated cracking and/or system instability.

diff --git a/frontend/src/components/NewMessageModal.tsx b/frontend/src/components/NewMessageModal.tsx index d44b282..bb2d91a 100644 --- a/frontend/src/components/NewMessageModal.tsx +++ b/frontend/src/components/NewMessageModal.tsx @@ -15,7 +15,7 @@ import { Checkbox } from './ui/checkbox'; import { Button } from './ui/button'; import { toast } from './ui/sonner'; -type Tab = 'new-contact' | 'new-room' | 'hashtag'; +type Tab = 'new-contact' | 'new-channel' | 'hashtag'; interface NewMessageModalProps { open: boolean; @@ -37,7 +37,7 @@ export function NewMessageModal({ const [tab, setTab] = useState('new-contact'); const [name, setName] = useState(''); const [contactKey, setContactKey] = useState(''); - const [roomKey, setRoomKey] = useState(''); + const [channelKey, setChannelKey] = useState(''); const [tryHistorical, setTryHistorical] = useState(false); const [permitCapitals, setPermitCapitals] = useState(false); const [error, setError] = useState(''); @@ -47,7 +47,7 @@ export function NewMessageModal({ const resetForm = () => { setName(''); setContactKey(''); - setRoomKey(''); + setChannelKey(''); setTryHistorical(false); setPermitCapitals(false); setError(''); @@ -65,12 +65,12 @@ export function NewMessageModal({ } // handleCreateContact sets activeConversation with the backend-normalized key await onCreateContact(name.trim(), contactKey.trim(), tryHistorical); - } else if (tab === 'new-room') { - if (!name.trim() || !roomKey.trim()) { - setError('Room name and key are required'); + } else if (tab === 'new-channel') { + if (!name.trim() || !channelKey.trim()) { + setError('Channel name and key are required'); return; } - await onCreateChannel(name.trim(), roomKey.trim(), tryHistorical); + await onCreateChannel(name.trim(), channelKey.trim(), tryHistorical); } else if (tab === 'hashtag') { const channelName = name.trim(); const validationError = validateHashtagName(channelName); @@ -147,7 +147,7 @@ export function NewMessageModal({ New Conversation {tab === 'new-contact' && 'Add a new contact by entering their name and public key'} - {tab === 'new-room' && 'Create a private room with a shared encryption key'} + {tab === 'new-channel' && 'Create a private channel with a shared encryption key'} {tab === 'hashtag' && 'Join a public hashtag channel'} @@ -162,7 +162,7 @@ export function NewMessageModal({ > Contact - Private Channel + Private Channel Hashtag Channel @@ -187,23 +187,23 @@ export function NewMessageModal({
- +
- + setName(e.target.value)} - placeholder="Room name" + placeholder="Channel name" />
- +
setRoomKey(e.target.value)} + id="channel-key" + value={channelKey} + onChange={(e) => setChannelKey(e.target.value)} placeholder="Pre-shared key (hex)" className="flex-1" /> @@ -217,7 +217,7 @@ export function NewMessageModal({ const hex = Array.from(bytes) .map((b) => b.toString(16).padStart(2, '0')) .join(''); - setRoomKey(hex); + setChannelKey(hex); }} title="Generate random key" aria-label="Generate random key" @@ -251,7 +251,7 @@ export function NewMessageModal({ onChange={(e) => setPermitCapitals(e.target.checked)} className="w-4 h-4 rounded border-input accent-primary" /> - Permit capitals in room key derivation + Permit capitals in channel key derivation

Not recommended; most companions normalize to lowercase diff --git a/frontend/src/components/RawPacketDetailModal.tsx b/frontend/src/components/RawPacketDetailModal.tsx index 73c633f..cc33a0d 100644 --- a/frontend/src/components/RawPacketDetailModal.tsx +++ b/frontend/src/components/RawPacketDetailModal.tsx @@ -161,7 +161,7 @@ function buildGroupTextResolutionCandidates(channels: Channel[]): GroupTextResol })); } -function resolveGroupTextRoomName( +function resolveGroupTextChannelName( payload: { channelHash?: string; cipherMac?: string; @@ -211,15 +211,15 @@ function getPacketContext( groupTextCandidates: GroupTextResolutionCandidate[] ) { const fallbackSender = packet.decrypted_info?.sender ?? null; - const fallbackRoom = packet.decrypted_info?.channel_name ?? null; + const fallbackChannel = packet.decrypted_info?.channel_name ?? null; if (!inspection.decoded?.payload.decoded) { - if (!fallbackSender && !fallbackRoom) { + if (!fallbackSender && !fallbackChannel) { return null; } return { - title: fallbackRoom ? 'Room' : 'Context', - primary: fallbackRoom ?? 'Sender metadata available', + title: fallbackChannel ? 'Channel' : 'Context', + primary: fallbackChannel ?? 'Sender metadata available', secondary: fallbackSender ? `Sender: ${fallbackSender}` : null, }; } @@ -231,11 +231,12 @@ function getPacketContext( ciphertext?: string; decrypted?: { sender?: string; message?: string }; }; - const roomName = fallbackRoom ?? resolveGroupTextRoomName(payload, groupTextCandidates); + const channelName = + fallbackChannel ?? resolveGroupTextChannelName(payload, groupTextCandidates); return { - title: roomName ? 'Room' : 'Channel', + title: 'Channel', primary: - roomName ?? (payload.channelHash ? `Channel hash ${payload.channelHash}` : 'GroupText'), + channelName ?? (payload.channelHash ? `Channel hash ${payload.channelHash}` : 'GroupText'), secondary: payload.decrypted?.sender ? `Sender: ${payload.decrypted.sender}` : fallbackSender diff --git a/frontend/src/components/Sidebar.tsx b/frontend/src/components/Sidebar.tsx index d6b7bae..178937e 100644 --- a/frontend/src/components/Sidebar.tsx +++ b/frontend/src/components/Sidebar.tsx @@ -748,7 +748,7 @@ export function Sidebar({ icon: , label: ( <> - {showCracker ? 'Hide' : 'Show'} Room Finder + {showCracker ? 'Hide' : 'Show'} Channel Finder { }); }); - it('loads name-only channel stats and most active rooms', async () => { + it('loads name-only channel stats and most active channels', async () => { getContactAnalytics.mockResolvedValue( createAnalytics(null, { lookup_type: 'name', @@ -188,7 +188,7 @@ describe('ContactInfoPane', () => { expect(screen.getByText('Name First In Use')).toBeInTheDocument(); expect(screen.getByText('Messages Per Hour')).toBeInTheDocument(); expect(screen.getByText('Messages Per Week')).toBeInTheDocument(); - expect(screen.getByText('Most Active Rooms')).toBeInTheDocument(); + expect(screen.getByText('Most Active Channels')).toBeInTheDocument(); expect(screen.getByText('#ops')).toBeInTheDocument(); expect( screen.getByText(/Name-only analytics include channel messages only/i) diff --git a/frontend/src/test/newMessageModal.test.tsx b/frontend/src/test/newMessageModal.test.tsx index 7dc013a..d8c8d2e 100644 --- a/frontend/src/test/newMessageModal.test.tsx +++ b/frontend/src/test/newMessageModal.test.tsx @@ -105,13 +105,13 @@ describe('NewMessageModal form reset', () => { }); }); - describe('new-room tab', () => { + describe('new-channel tab', () => { it('clears name and key after successful Create', async () => { const user = userEvent.setup(); renderModal(); await switchToTab(user, 'Private Channel'); - await user.type(screen.getByPlaceholderText('Room name'), 'MyRoom'); + await user.type(screen.getByPlaceholderText('Channel name'), 'MyRoom'); await user.type(screen.getByPlaceholderText('Pre-shared key (hex)'), 'cc'.repeat(16)); await user.click(screen.getByRole('button', { name: 'Create' })); @@ -128,7 +128,7 @@ describe('NewMessageModal form reset', () => { renderModal(); await switchToTab(user, 'Private Channel'); - await user.type(screen.getByPlaceholderText('Room name'), 'MyRoom'); + await user.type(screen.getByPlaceholderText('Channel name'), 'MyRoom'); await user.type(screen.getByPlaceholderText('Pre-shared key (hex)'), 'cc'.repeat(16)); await user.click(screen.getByRole('button', { name: 'Create' })); @@ -142,7 +142,7 @@ describe('NewMessageModal form reset', () => { }); describe('tab switching resets form', () => { - it('clears contact fields when switching to room tab', async () => { + it('clears contact fields when switching to channel tab', async () => { const user = userEvent.setup(); renderModal(); await switchToTab(user, 'Contact'); @@ -153,18 +153,18 @@ describe('NewMessageModal form reset', () => { // Switch to Private Channel tab — fields should reset await switchToTab(user, 'Private Channel'); - expect((screen.getByPlaceholderText('Room name') as HTMLInputElement).value).toBe(''); + expect((screen.getByPlaceholderText('Channel name') as HTMLInputElement).value).toBe(''); expect((screen.getByPlaceholderText('Pre-shared key (hex)') as HTMLInputElement).value).toBe( '' ); }); - it('clears room fields when switching to hashtag tab', async () => { + it('clears channel fields when switching to hashtag tab', async () => { const user = userEvent.setup(); renderModal(); await switchToTab(user, 'Private Channel'); - await user.type(screen.getByPlaceholderText('Room name'), 'SecretRoom'); + await user.type(screen.getByPlaceholderText('Channel name'), 'SecretRoom'); await user.type(screen.getByPlaceholderText('Pre-shared key (hex)'), 'ff'.repeat(16)); await switchToTab(user, 'Hashtag Channel'); diff --git a/frontend/src/test/rawPacketFeedView.test.tsx b/frontend/src/test/rawPacketFeedView.test.tsx index b116a04..434ad2e 100644 --- a/frontend/src/test/rawPacketFeedView.test.tsx +++ b/frontend/src/test/rawPacketFeedView.test.tsx @@ -361,7 +361,7 @@ describe('RawPacketFeedView', () => { expect(screen.queryByText('Identity not resolvable')).not.toBeInTheDocument(); }); - it('opens a packet detail modal from the raw feed and decrypts room messages when a key is loaded', () => { + it('opens a packet detail modal from the raw feed and decrypts channel messages when a key is loaded', () => { renderView({ packets: [ { @@ -392,7 +392,7 @@ describe('RawPacketFeedView', () => { ).toBeInTheDocument(); }); - it('does not guess a room name when multiple loaded channels collide on the group hash', () => { + it('does not guess a channel name when multiple loaded channels collide on the group hash', () => { renderView({ packets: [ { diff --git a/frontend/src/utils/rawPacketInspector.ts b/frontend/src/utils/rawPacketInspector.ts index e7ff3fb..73409a4 100644 --- a/frontend/src/utils/rawPacketInspector.ts +++ b/frontend/src/utils/rawPacketInspector.ts @@ -67,8 +67,8 @@ export function describeCiphertextStructure( case PayloadType.GroupText: return `Encrypted message content (${byteLength} bytes). Contains encrypted plaintext with this structure: • Timestamp (4 bytes) - send time as unix timestamp -• Flags (1 byte) - room-message flags byte -• Message (remaining bytes) - UTF-8 room message text`; +• Flags (1 byte) - channel-message flags byte +• Message (remaining bytes) - UTF-8 channel message text`; case PayloadType.TextMessage: return `Encrypted message data (${byteLength} bytes). Contains encrypted plaintext with this structure: • Timestamp (4 bytes) - send time as unix timestamp diff --git a/tests/e2e/specs/incoming-message.spec.ts b/tests/e2e/specs/incoming-message.spec.ts index 4be02bc..9dc9d64 100644 --- a/tests/e2e/specs/incoming-message.spec.ts +++ b/tests/e2e/specs/incoming-message.spec.ts @@ -7,7 +7,7 @@ import { createChannel, getChannels, getMessages } from '../helpers/api'; * Timeout is 3 minutes to allow for intermittent traffic. */ -const ROOMS = [ +const CHANNELS = [ '#flightless', '#bot', '#snoco', '#skagit', '#edmonds', '#bachelorette', '#emergency', '#furry', '#public', '#puppy', '#foobar', '#capitolhill', '#hamradio', '#icewatch', '#saucefamily', '#scvsar', '#startrek', '#metalmusic', @@ -39,14 +39,14 @@ test.describe('Incoming mesh messages', () => { test.setTimeout(180_000); test.beforeAll(async () => { - // Ensure all rooms exist — create any that are missing + // Ensure all channels exist — create any that are missing const existing = await getChannels(); const existingNames = new Set(existing.map((c) => c.name)); - for (const room of ROOMS) { - if (!existingNames.has(room)) { + for (const channel of CHANNELS) { + if (!existingNames.has(channel)) { try { - await createChannel(room); + await createChannel(channel); } catch { // May already exist from a concurrent creation, ignore } @@ -54,7 +54,7 @@ test.describe('Incoming mesh messages', () => { } }); - test('receive an incoming message in any room', { tag: '@mesh-traffic' }, async ({ page }) => { + test('receive an incoming message in any channel', { tag: '@mesh-traffic' }, async ({ page }) => { // Nudge echo bot on #flightless — may generate an incoming packet quickly await nudgeEchoBot();