diff --git a/README.md b/README.md index c918cf447..598622d7b 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 f06132485..64aba19a8 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 f87c8a8b8..105fe7f4c 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 214d884ae..cf91209c5 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 ca4de0768..fb3f94ffe 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 be8b097da..11dbf7193 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 e9642b499..d92c4b30c 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 f9abe0b1c..1daea1d0b 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 b3c1c6891..d96590693 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 d44b2824d..bb2d91af9 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 73c633ffd..cc33a0d45 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 d6b7bae2b..178937edc 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 7dc013aa9..d8c8d2ee1 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 b116a04ef..434ad2e41 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 e7ff3fb85..73409a497 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 4be02bc62..9dc9d64a4 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();