feat: Add support for public channels and auto-cleanup on channel deletion

- Allow joining public channels (starting with #) without encryption key
  - Frontend: Make key field optional with validation for # channels
  - Backend: Update API to accept optional key parameter
  - CLI wrapper: Build meshcli command dynamically based on key presence

- Implement automatic message cleanup when deleting channels
  - Add delete_channel_messages() function to remove channel history
  - Integrate cleanup into DELETE /api/channels endpoint
  - Prevents message leakage when reusing channel slots

- Update documentation with new features and usage instructions

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
MarekWo
2025-12-24 12:34:14 +01:00
parent 4401aaf980
commit 6c3551cd2d
6 changed files with 123 additions and 24 deletions
+17
View File
@@ -17,6 +17,7 @@ A lightweight web interface for meshcore-cli, providing browser-based access to
- ✉️ **Send messages** - Publish to any channel (140 byte limit for LoRa)
- 📡 **Channel management** - Create, join, and switch between encrypted channels
- 🔐 **Channel sharing** - Share channels via QR code or encrypted keys
- 🔓 **Public channels** - Join public channels (starting with #) without encryption keys
- 🎯 **Reply to users** - Quick reply with `@[UserName]` format
- 🧹 **Clean contacts** - Remove inactive contacts with configurable threshold
- 📦 **Message archiving** - Automatic daily archiving with browse-by-date selector
@@ -197,11 +198,27 @@ Access channel management:
3. Others can join using the "Join Existing" option
#### Joining a Channel
**For private channels:**
1. Click "Join Existing"
2. Enter the channel name and encryption key (received from channel creator)
3. Click "Join Channel"
4. The channel will be added to your available channels
**For public channels (starting with #):**
1. Click "Join Existing"
2. Enter the channel name (e.g., `#test`, `#krakow`)
3. Leave the encryption key field empty (key is auto-generated based on channel name)
4. Click "Join Channel"
5. You can now chat with other users on the same public channel
#### Deleting a Channel
1. In the Channels modal, click the delete icon (trash) next to any channel
2. Confirm the deletion
3. The channel configuration and **all its messages** will be permanently removed
**Note:** Deleting a channel removes all message history for that channel from your device to prevent data leakage when reusing channel slots.
#### Switching Channels
Use the channel selector dropdown in the navbar to switch between channels. Your selection is remembered between sessions.
+13 -12
View File
@@ -222,28 +222,29 @@ def add_channel(name: str) -> Tuple[bool, str, Optional[str]]:
return True, stdout or stderr, None
def set_channel(index: int, name: str, key: str) -> Tuple[bool, str]:
def set_channel(index: int, name: str, key: Optional[str] = None) -> Tuple[bool, str]:
"""
Set/join a channel at specific index with name and key.
Set/join a channel at specific index with name and optional key.
Args:
index: Channel slot number
name: Channel name
key: 32-char hex key
key: 32-char hex key (optional for channels starting with #)
Returns:
Tuple of (success, message)
"""
# Validate key format
if not re.match(r'^[a-f0-9]{32}$', key.lower()):
return False, "Invalid key format (must be 32 hex characters)"
# Build command arguments
cmd_args = ['set_channel', str(index), name]
success, stdout, stderr = _run_command([
'set_channel',
str(index),
name,
key.lower()
])
# Add key if provided
if key:
# Validate key format
if not re.match(r'^[a-f0-9]{32}$', key.lower()):
return False, "Invalid key format (must be 32 hex characters)"
cmd_args.append(key.lower())
success, stdout, stderr = _run_command(cmd_args)
return success, stdout or stderr
+52
View File
@@ -254,3 +254,55 @@ def filter_messages_by_days(messages: List[Dict], days: int) -> List[Dict]:
logger.info(f"Filtered {len(filtered)} messages from last {days} days (out of {len(messages)} total)")
return filtered
def delete_channel_messages(channel_idx: int) -> bool:
"""
Delete all messages for a specific channel from the .msgs file.
Args:
channel_idx: Channel index to delete messages from
Returns:
True if successful, False otherwise
"""
msgs_file = config.msgs_file_path
if not msgs_file.exists():
logger.warning(f"Messages file not found: {msgs_file}")
return True # No messages to delete
try:
# Read all lines
lines_to_keep = []
deleted_count = 0
with open(msgs_file, 'r', encoding='utf-8') as f:
for line_num, line in enumerate(f, 1):
line = line.strip()
if not line:
continue
try:
data = json.loads(line)
# Keep messages from other channels
if data.get('channel_idx', 0) != channel_idx:
lines_to_keep.append(line)
else:
deleted_count += 1
except json.JSONDecodeError as e:
# Keep malformed lines (don't delete them)
logger.warning(f"Invalid JSON at line {line_num}, keeping: {e}")
lines_to_keep.append(line)
# Write back the filtered lines
with open(msgs_file, 'w', encoding='utf-8') as f:
for line in lines_to_keep:
f.write(line + '\n')
logger.info(f"Deleted {deleted_count} messages from channel {channel_idx}")
return True
except Exception as e:
logger.error(f"Error deleting channel messages: {e}")
return False
+20 -7
View File
@@ -495,7 +495,7 @@ def join_channel():
JSON body:
name (str): Channel name (required)
key (str): 32-char hex key (required)
key (str): 32-char hex key (optional for channels starting with #)
index (int): Channel slot (optional, auto-detect if not provided)
Returns:
@@ -504,14 +504,21 @@ def join_channel():
try:
data = request.get_json()
if not data or 'name' not in data or 'key' not in data:
if not data or 'name' not in data:
return jsonify({
'success': False,
'error': 'Missing required fields: name, key'
'error': 'Missing required field: name'
}), 400
name = data['name'].strip()
key = data['key'].strip().lower()
key = data.get('key', '').strip().lower() if 'key' in data else None
# Validate: key is optional for channels starting with #
if not name.startswith('#') and not key:
return jsonify({
'success': False,
'error': 'Key is required for channels not starting with #'
}), 400
# Auto-detect free slot if not provided
if 'index' in data:
@@ -548,7 +555,7 @@ def join_channel():
'channel': {
'index': index,
'name': name,
'key': key
'key': key if key else 'auto-generated'
}
}), 200
else:
@@ -568,7 +575,7 @@ def join_channel():
@api_bp.route('/channels/<int:index>', methods=['DELETE'])
def delete_channel(index):
"""
Remove a channel.
Remove a channel and delete all its messages.
Args:
index: Channel index to remove
@@ -577,13 +584,19 @@ def delete_channel(index):
JSON with result
"""
try:
# First, delete all messages for this channel
messages_deleted = parser.delete_channel_messages(index)
if not messages_deleted:
logger.warning(f"Failed to delete messages for channel {index}, continuing with channel removal")
# Then remove the channel itself
success, message = cli.remove_channel(index)
if success:
invalidate_channels_cache() # Clear cache to force refresh
return jsonify({
'success': True,
'message': f'Channel {index} removed'
'message': f'Channel {index} removed and messages deleted'
}), 200
else:
return jsonify({
+18 -1
View File
@@ -172,13 +172,30 @@ function setupEventListeners() {
const name = document.getElementById('joinChannelName').value.trim();
const key = document.getElementById('joinChannelKey').value.trim().toLowerCase();
// Validate: key is optional for channels starting with #, but required for others
if (!name.startsWith('#') && !key) {
showNotification('Channel key is required for channels not starting with #', 'warning');
return;
}
// Validate key format if provided
if (key && !/^[a-f0-9]{32}$/.test(key)) {
showNotification('Invalid key format. Must be 32 hex characters.', 'warning');
return;
}
try {
const payload = { name: name };
if (key) {
payload.key = key;
}
const response = await fetch('/api/channels/join', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ name: name, key: key })
body: JSON.stringify(payload)
});
const data = await response.json();
+3 -4
View File
@@ -143,12 +143,11 @@
<input type="text" class="form-control" id="joinChannelName" required>
</div>
<div class="mb-2">
<label for="joinChannelKey" class="form-label">Channel Key (32 hex chars)</label>
<label for="joinChannelKey" class="form-label">Channel Key (32 hex chars) <small class="text-muted">- optional for channels starting with #</small></label>
<input type="text" class="form-control" id="joinChannelKey"
placeholder="485af7e164459d280d8818d9c99fb30d"
placeholder="485af7e164459d280d8818d9c99fb30d (leave empty for # channels)"
pattern="[a-fA-F0-9]{32}"
maxlength="32"
required>
maxlength="32">
</div>
<div class="d-flex gap-2">
<button type="submit" class="btn btn-success">