Refactor: Implement generalized command argument parsing with optional quotes

This commit introduces a significant refactoring to how command arguments are parsed. The primary goal is to allow users to optionally enclose arguments containing spaces in double quotes for all commands, providing a more flexible and intuitive command input experience.

Key changes include:

1.  **Centralized Argument Parsing (`src/queue/server.py`):**
    *   The `process_chat_command` function in `src/queue/server.py` now handles the primary parsing of command arguments.
    *   Incoming raw arguments (previously a list of strings split by spaces) are joined into a single string.
    *   This string is then processed using the regular expression `re.findall(r'"([^"]*)"|(\S+)', ...)` to accurately identify and separate arguments. This regex correctly distinguishes between:
        *   Substrings enclosed in double quotes (e.g., `"my poll question"`), which are treated as single arguments.
        *   Sequences of non-whitespace characters (e.g., `1`, `myoption`), also treated as single arguments.
    *   This ensures that all command handlers receive a list of arguments (`parsed_args`) where multi-word arguments (if originally quoted) are correctly preserved as single elements.
    *   A correction was made to the regex pattern: an initial version mistakenly used `(\\\\S+)` (matching a literal backslash then 'S') instead of the intended `(\\S+)` (matching any non-whitespace sequence) for unquoted arguments. This fix ensures that simple unquoted arguments like numbers (e.g., for the `!vote` command) are parsed correctly.
    *   The `re` module import was ensured.

2.  **Simplification of Poll Command (`src/features/examples.py`):**
    *   The `cmd_start_poll` method in `src/features/examples.py` has been simplified. Its specific regex-based logic for parsing quoted poll questions and options was removed.
    *   This method now relies on the generalized, upstream parsing performed by `process_chat_command` in `src/queue/server.py`. It expects to receive a list of arguments that have already been correctly parsed.
    *   Test calls for `cmd_start_poll` within the `if __name__ == '__main__':` block were updated to align with this change, demonstrating how arguments (like poll questions and options) are now passed as a simple list of strings.

These changes collectively improve the robustness and usability of the command system by standardizing argument parsing and allowing for more complex argument values.
This commit is contained in:
Joey Yakimowich-Payne 2025-06-04 21:56:38 -06:00
commit c17f382c60
5 changed files with 665 additions and 204 deletions

75
main.py
View file

@ -205,11 +205,12 @@ def main() -> None:
client_secret=client_secret,
channel=channel,
use_queue=args.use_queue,
command_prefix=config.get("command_prefix", "!"),
token_cache_file=args.token_cache,
admin_users=admin_users,
force_new_token=args.force_new_token,
access_token=None, # Explicitly set to None to avoid using env vars
refresh_token=None # Explicitly set to None to avoid using env vars
refresh_token=None # Explicitly set to None
)
# Register basic commands
@ -246,17 +247,71 @@ def main() -> None:
if args.advanced:
try:
from src.features.examples import AdvancedExamples
import traceback # For more detailed error logging
print("Enabling advanced features...")
advanced: AdvancedExamples = AdvancedExamples(bot)
print("Advanced features enabled: polls, voting, timers, points system")
if os.name == 'nt':
print("Sound effects enabled (Windows only)")
# Create sounds directory if it doesn't exist
if not os.path.exists("sounds"):
os.makedirs("sounds")
print("Created 'sounds' directory. Add .wav files to use with !sound command")
if args.use_queue:
from src.queue.server import setup_advanced_commands_config
setup_advanced_commands_config(
bot_username_main=bot.username,
channel_main=channel,
admin_users_main=admin_users if admin_users else [],
oauth_token_main=bot.access_token if bot.access_token else ""
)
print("Advanced features configuration (poll, points, etc.) saved for queue processing.")
print("The queue consumer will register these commands: !poll, !vote, !endpoll, !points, !givepoints.")
else: # Not using queue, direct registration with bot instance
# Wrapper for send_message_func for AdvancedExamples
# Expected: (channel, oauth_token, bot_username, message)
def non_queue_send_message_wrapper(ch_ignored: str, oauth_ignored: Optional[str], bot_user_ignored: str, message: str):
bot.send_message(message) # bot.send_message handles its own channel context
# Wrapper for is_admin_func for AdvancedExamples
# Expected: (username, channel, admin_list)
def non_queue_is_admin_wrapper(usr: str, chan_ignored: str, admin_list_ignored: List[str]) -> bool:
return bot.is_admin(usr) # bot.is_admin uses its own channel and admin_list
advanced_instance = AdvancedExamples(
send_message_func=non_queue_send_message_wrapper,
is_admin_func=non_queue_is_admin_wrapper,
bot_username=bot.username,
channel=channel, # Pass the specific channel to AdvancedExamples
admin_users=admin_users if admin_users else [], # Pass specific admin_users
use_queue=False
)
# Register commands with the bot instance
# Lambdas pass username, args, and the bot's current access_token
bot.register_command("poll", lambda u, a, b_inst: advanced_instance.cmd_start_poll(u, a, b_inst.access_token))
bot.register_command("vote", lambda u, a, b_inst: advanced_instance.cmd_vote(u, a, b_inst.access_token))
bot.register_command("endpoll", lambda u, a, b_inst: advanced_instance.cmd_end_poll(u, a, b_inst.access_token))
bot.register_command("points", lambda u, a, b_inst: advanced_instance.cmd_points(u, a, b_inst.access_token))
# Using "givepoints" for consistency with queue registration
bot.register_command("givepoints", lambda u, a, b_inst: advanced_instance.cmd_give_points(u, a, b_inst.access_token))
print("Advanced features enabled directly: polls, voting, points system.")
print("Use !poll, !vote, !endpoll, !points, !givepoints.")
# Timer and Sound commands are handled by AdvancedExamples based on its use_queue flag
# For non-queue mode, AdvancedExamples will allow them.
bot.register_command("timer", lambda u, a, b_inst: advanced_instance.cmd_timer(u,a,b_inst.access_token))
print("Timer command !timer enabled.")
if os.name == 'nt':
bot.register_command("sound", lambda u, a, b_inst: advanced_instance.cmd_play_sound(u, a, b_inst.access_token))
print("Sound effects !sound enabled (Windows only).")
# Create sounds directory if it doesn't exist (moved from previous unconditional spot)
if not os.path.exists("sounds"):
os.makedirs("sounds")
print("Created 'sounds' directory. Add .wav files to use with !sound command")
except ImportError as e:
print(f"Failed to import advanced features: {e}")
print(f"Failed to import or configure advanced features: {e}")
print(traceback.format_exc()) # Print full traceback
except Exception as e:
print(f"Error setting up advanced features: {e}")
print(traceback.format_exc()) # Print full traceback
# Enable game control if requested
if args.game_control: