mirror of
https://github.com/MarekWo/mc-webui.git
synced 2026-07-04 17:01:34 +02:00
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:
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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();
|
||||
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user