Update admin commands to include channelpause functionality

- Added `channelpause` and `channelresume` commands to the admin commands list in configuration files, allowing admins to pause or resume bot responses on public channels via DM.
- Updated documentation to reflect the new command functionality and its implications for channel responses.
- Modified validation and message handling to incorporate the new channel response control feature.
This commit is contained in:
agessaman
2026-03-21 20:43:58 -07:00
parent 6c1040db60
commit 7a3bb56d70
10 changed files with 104 additions and 6 deletions
+2 -1
View File
@@ -211,7 +211,8 @@ admin_pubkeys =
# Commands that require admin access (comma-separated)
# These commands will only work for users in the admin_pubkeys list
# reload: Reload configuration without restarting (radio settings cannot be changed)
admin_commands = repeater,webviewer,reload
# channelpause: DM-only; channelpause / channelresume — pause or resume bot responses on channels (not persisted)
admin_commands = repeater,webviewer,reload,channelpause
[Plugin_Overrides]
# Plugin Overrides - Use alternative plugin implementations
+1 -1
View File
@@ -180,7 +180,7 @@ admin_pubkeys =
# Commands that require admin access (comma-separated)
# These commands will only work for users in the admin_pubkeys list
# reload: Reload configuration without restarting (radio settings cannot be changed)
admin_commands = repeater,webviewer,reload
admin_commands = repeater,webviewer,reload,channelpause
[Keywords]
# Available placeholders (message-based):
+1 -1
View File
@@ -25,7 +25,7 @@ banned_users =
[Admin_ACL]
# 64-char hex public keys (comma-separated). Leave blank to disable admin commands.
admin_pubkeys =
admin_commands = repeater,webviewer,reload
admin_commands = repeater,webviewer,reload,channelpause
[Keywords]
test = "ack @[{sender}]{phrase_part} | {connection_info} | Received at: {timestamp}"
+1 -1
View File
@@ -46,7 +46,7 @@ The bot will not start without these sections. The validator reports them as **e
If these are absent, the validator reports **info** (no error):
- **`[Admin_ACL]`** Absent means admin commands (repeater, webviewer, reload) are disabled.
- **`[Admin_ACL]`** Absent means admin commands (repeater, webviewer, reload, channelpause) are disabled.
- **`[Banned_Users]`** Absent means no users are banned.
- **`[Localization]`** Absent means defaults (e.g. `language=en`, `translation_path=translations/`) are used.
+4
View File
@@ -93,3 +93,7 @@ Before starting the bot, you can validate section names and path writability. Se
## Reloading configuration
Some configuration can be reloaded without restarting the bot using the **`reload`** command (admin only). Radio/connection settings are not changed by reload; restart the bot for those.
## Pausing channel responses (remote)
Admins can DM **`channelpause`** or **`channelresume`** (see `[Admin_ACL]` in `config.ini`) to stop or resume bot reactions on **public channels** only—greeter, keywords, and commands on channels are skipped; DMs still work. The setting is **in memory only** (back to responding on channels after restart). Scheduled channel posts from the scheduler are **not** blocked by this toggle.
+70
View File
@@ -0,0 +1,70 @@
#!/usr/bin/env python3
"""
Channel pause command
DM-only admin: pause or resume bot responses on public channels (in-memory only).
"""
from .base_command import BaseCommand
from ..models import MeshMessage
class ChannelPauseCommand(BaseCommand):
"""Pause or resume channel-triggered bot responses (greeter, keywords, commands)."""
name = "channelpause"
keywords = ["channelpause", "channelresume"]
description = "Pause or resume bot responses on channels (DM only, admin only)"
requires_dm = True
cooldown_seconds = 2
category = "admin"
def __init__(self, bot):
super().__init__(bot)
def can_execute(self, message: MeshMessage) -> bool:
if not self.requires_admin_access():
return False
return super().can_execute(message)
def requires_admin_access(self) -> bool:
return True
def get_help_text(self) -> str:
return (
"Controls whether the bot responds to public channel messages.\n"
"DMs always work (including this command).\n"
"Not saved across restarts.\n"
"Usage: channelpause — stop channel responses\n"
" channelresume — resume channel responses"
)
def _stripped_content_lower(self, message: MeshMessage) -> str:
content = message.content.strip()
if self._command_prefix:
if not content.startswith(self._command_prefix):
return ""
content = content[len(self._command_prefix) :].strip()
elif content.startswith("!"):
content = content[1:].strip()
content = self._strip_mentions(content)
return content.lower()
async def execute(self, message: MeshMessage) -> bool:
text = self._stripped_content_lower(message)
resume_kw = self.keywords[1].lower()
pause_kw = self.keywords[0].lower()
if text == resume_kw or text.startswith(resume_kw + " "):
self.bot.channel_responses_enabled = True
reply = "Channel responses: ON. The bot will respond on public channels again."
elif text == pause_kw or text.startswith(pause_kw + " "):
self.bot.channel_responses_enabled = False
reply = (
"Channel responses: OFF. No greeter, keywords, or commands on channels; "
"DMs still work. Not persisted after restart."
)
else:
reply = "Use channelpause or channelresume."
await self.send_response(message, reply)
return True
+9
View File
@@ -1201,6 +1201,15 @@ class GreeterCommand(BaseCommand):
self.logger.debug(f"Waiting {self.dead_air_delay_seconds} seconds before greeting {message.sender_id} on {message.channel}")
await asyncio.sleep(self.dead_air_delay_seconds)
if not getattr(self.bot, "channel_responses_enabled", True):
self.logger.info(
f"Skipping delayed greeting for {message.sender_id} on {message.channel} "
"(channel responses paused)"
)
if key in self.pending_greetings:
del self.pending_greetings[key]
return
# Check if greeting was cancelled (user was already greeted or human responded)
if key not in self.pending_greetings:
self.logger.debug(f"Greeting for {message.sender_id} on {message.channel} was cancelled")
+1 -1
View File
@@ -177,7 +177,7 @@ def validate_config(config_path: str) -> list[tuple[str, str]]:
if ADMIN_ACL_SECTION not in sections_present:
results.append((
SEVERITY_INFO,
f"Section [{ADMIN_ACL_SECTION}] absent; admin commands (repeater, webviewer, reload) disabled.",
f"Section [{ADMIN_ACL_SECTION}] absent; admin commands (repeater, webviewer, reload, channelpause) disabled.",
))
if BANNED_USERS_SECTION not in sections_present:
results.append((
+3
View File
@@ -81,6 +81,9 @@ class MeshCoreBot:
self.connected = False
self.connection_time = None # Track when connection was established to skip old cached messages
# Volatile: DM-only admin command (channelpause) toggles this; not persisted across restarts.
self.channel_responses_enabled = True
# Bot start time for uptime tracking
self.start_time = time.time()
+12 -1
View File
@@ -2628,7 +2628,7 @@ class MessageHandler:
# Check greeter command for public channel messages (BEFORE general message filtering)
# This allows greeter to work on its own configured channels even if not in monitor_channels
if 'greeter' in self.bot.command_manager.commands:
if self._channel_responses_allowed(message) and 'greeter' in self.bot.command_manager.commands:
greeter_command = self.bot.command_manager.commands['greeter']
# First, check if this message should cancel a pending greeting (human greeting detection)
if greeter_command:
@@ -2779,6 +2779,11 @@ class MessageHandler:
self.logger.debug(f"Ignoring message from banned user: {message.sender_id}")
return False
# Channel-only pause (DM-only admin command); DMs still processed
if not message.is_dm and not getattr(self.bot, "channel_responses_enabled", True):
self.logger.debug("Ignoring non-DM message: channel responses paused")
return False
# Check if channel is monitored (with command override support)
if not message.is_dm and message.channel:
# Check if channel is in global monitor_channels
@@ -2804,6 +2809,12 @@ class MessageHandler:
return True
def _channel_responses_allowed(self, message: MeshMessage) -> bool:
"""True if channel-driven bot responses are allowed for this message (DMs always True here)."""
if message.is_dm:
return True
return getattr(self.bot, "channel_responses_enabled", True)
async def handle_new_contact(self, event, metadata=None):
"""Handle NEW_CONTACT events for automatic contact management"""
try: