Add @help list command for admin topic management

This commit is contained in:
Jared Miller 2026-02-15 09:56:21 -05:00
parent 6524116e97
commit d1671c60c1
Signed by: shmup
GPG key ID: 22B5C6D66A38B06C
3 changed files with 196 additions and 0 deletions

View file

@ -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 <topic> edit an existing topic")
lines.append(" @help remove <topic> 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 <topic>\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"
)
)

View file

@ -24,6 +24,7 @@ import mudlib.commands.examine
import mudlib.commands.fly import mudlib.commands.fly
import mudlib.commands.furnish import mudlib.commands.furnish
import mudlib.commands.help import mudlib.commands.help
import mudlib.commands.helpadmin
import mudlib.commands.home import mudlib.commands.home
import mudlib.commands.look import mudlib.commands.look
import mudlib.commands.movement import mudlib.commands.movement

View file

@ -7,6 +7,7 @@ import pytest
from mudlib import commands from mudlib import commands
from mudlib.commands import help as help_mod # noqa: F401 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.commands.help import _help_topics
from mudlib.content import HelpTopic, load_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()) p = Player(name="Test", x=0, y=0, reader=MagicMock(), writer=MagicMock())
assert p.pending_input is None 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()