diff --git a/src/mudlib/commands/helpadmin.py b/src/mudlib/commands/helpadmin.py new file mode 100644 index 0000000..8174727 --- /dev/null +++ b/src/mudlib/commands/helpadmin.py @@ -0,0 +1,170 @@ +"""Admin help topic management commands (@help).""" + +from pathlib import Path + +from mudlib.commands import CommandDefinition, register +from mudlib.commands.help import _help_topics +from mudlib.content import HelpTopic +from mudlib.editor import Editor +from mudlib.player import Player + + +async def cmd_at_help(player: Player, args: str) -> None: + """List all help topics or dispatch to subcommands.""" + args = args.strip() + + parts = args.split(None, 1) + subcmd = parts[0] if parts else "" + rest = parts[1] if len(parts) > 1 else "" + + if subcmd == "create": + await _cmd_create(player, rest) + return + if subcmd == "edit": + await _cmd_edit(player, rest) + return + if subcmd == "remove": + await _cmd_remove(player, rest) + return + + # Default: list all topics + if not _help_topics: + await player.send("No help topics defined.\r\n") + return + + lines = ["help topics"] + for name in sorted(_help_topics): + topic = _help_topics[name] + admin_tag = " [admin]" if topic.admin else "" + lines.append(f" {name:<20} {topic.title}{admin_tag}") + lines.append("") + lines.append(" @help create create a new topic") + lines.append(" @help edit edit an existing topic") + lines.append(" @help remove remove a topic") + await player.send("\r\n".join(lines) + "\r\n") + + +async def _cmd_create(player: Player, args: str) -> None: + """Guided topic creation.""" + if args: + # Name provided, skip to title prompt + await _prompt_title(player, name=args) + else: + await player.send("topic name: ") + player.pending_input = _on_name_input + + +async def _on_name_input(player: Player, line: str) -> None: + """Handle name input.""" + name = line.strip() + if not name: + await player.send("Cancelled.\r\n") + return + if name in _help_topics: + await player.send(f"Topic '{name}' already exists. Use @help edit.\r\n") + return + await _prompt_title(player, name=name) + + +async def _prompt_title(player: Player, name: str) -> None: + await player.send(f"title [{name}]: ") + player.pending_input = lambda p, line: _on_title_input(p, line, name) + + +async def _on_title_input(player: Player, line: str, name: str) -> None: + title = line.strip() or name + await player.send("admin only? [y/N]: ") + player.pending_input = lambda p, line: _on_admin_input(p, line, name, title) + + +async def _on_admin_input(player: Player, line: str, name: str, title: str) -> None: + admin = line.strip().lower() in ("y", "yes") + await _open_body_editor(player, name=name, title=title, admin=admin, body="") + + +async def _open_body_editor( + player: Player, name: str, title: str, admin: bool, body: str +) -> None: + save_cb = _make_help_save_callback(player, name, title, admin) + player.editor = Editor( + save_callback=save_cb, + content_type="text", + color_depth=player.color_depth or "16", + initial_content=body, + ) + player.mode_stack.append("editor") + await player.send("entering editor for body text. type :h for help.\r\n") + + +def _make_help_save_callback(player: Player, name: str, title: str, admin: bool): + async def save_callback(content: str) -> None: + _write_help_toml(name, title, admin, content) + _help_topics[name] = HelpTopic( + name=name, body=content.strip(), title=title, admin=admin + ) + await player.send(f"help topic '{name}' saved.\r\n") + + return save_callback + + +def _write_help_toml(name: str, title: str, admin: bool, body: str) -> None: + """Write a help topic TOML file.""" + help_dir = Path(__file__).resolve().parents[2] / "content" / "help" + help_dir.mkdir(parents=True, exist_ok=True) + path = help_dir / f"{name}.toml" + + def _escape(s: str) -> str: + return s.replace("\\", "\\\\").replace('"', '\\"') + + lines = [f'name = "{_escape(name)}"'] + lines.append(f'title = "{_escape(title)}"') + if admin: + lines.append("admin = true") + lines.append(f'body = """\n{body.strip()}\n"""') + path.write_text("\n".join(lines) + "\n") + + +async def _cmd_edit(player: Player, args: str) -> None: + """Edit an existing help topic.""" + name = args.strip() + if not name: + await player.send("Usage: @help edit \r\n") + return + + topic = _help_topics.get(name) + if topic is None: + await player.send(f"Topic '{name}' not found.\r\n") + return + + await player.send(f"title [{topic.title}]: ") + player.pending_input = lambda p, line: _on_edit_title(p, line, name, topic) + + +async def _on_edit_title( + player: Player, line: str, name: str, topic: HelpTopic +) -> None: + title = line.strip() or topic.title + default = "Y/n" if topic.admin else "y/N" + await player.send(f"admin only? [{default}]: ") + player.pending_input = lambda p, line: _on_edit_admin(p, line, name, title, topic) + + +async def _on_edit_admin( + player: Player, line: str, name: str, title: str, topic: HelpTopic +) -> None: + admin = line.strip().lower() in ("y", "yes") if line.strip() else topic.admin + await _open_body_editor( + player, name=name, title=title, admin=admin, body=topic.body + ) + + +async def _cmd_remove(player: Player, args: str) -> None: + """Placeholder for topic removal.""" + await player.send("Not yet implemented.\r\n") + + +register( + CommandDefinition( + "@help", cmd_at_help, admin=True, mode="*", help="manage help topics" + ) +) diff --git a/src/mudlib/server.py b/src/mudlib/server.py index 2647e00..b8daa08 100644 --- a/src/mudlib/server.py +++ b/src/mudlib/server.py @@ -24,6 +24,7 @@ import mudlib.commands.examine import mudlib.commands.fly import mudlib.commands.furnish import mudlib.commands.help +import mudlib.commands.helpadmin import mudlib.commands.home import mudlib.commands.look import mudlib.commands.movement diff --git a/tests/test_help_topics.py b/tests/test_help_topics.py index 2861c5f..08e6e10 100644 --- a/tests/test_help_topics.py +++ b/tests/test_help_topics.py @@ -7,6 +7,7 @@ import pytest from mudlib import commands from mudlib.commands import help as help_mod # noqa: F401 +from mudlib.commands import helpadmin # noqa: F401 from mudlib.commands.help import _help_topics from mudlib.content import HelpTopic, load_help_topics @@ -161,3 +162,27 @@ def test_player_has_pending_input(): p = Player(name="Test", x=0, y=0, reader=MagicMock(), writer=MagicMock()) assert p.pending_input is None + + +@pytest.mark.asyncio +async def test_at_help_lists_topics(admin_player): + _help_topics["combat"] = HelpTopic( + name="combat", body="fight", title="combat primer" + ) + _help_topics["zones"] = HelpTopic( + name="zones", body="build", title="zone guide", admin=True + ) + await commands.dispatch(admin_player, "@help") + output = "".join(c[0][0] for c in admin_player.writer.write.call_args_list) + assert "combat" in output + assert "combat primer" in output + assert "zones" in output + assert "[admin]" in output + + +@pytest.mark.asyncio +async def test_at_help_requires_admin(player): + await commands.dispatch(player, "@help") + output = "".join(c[0][0] for c in player.writer.write.call_args_list) + # Should be rejected by dispatch (admin=True check) + assert "permission" in output.lower()