Add @help list command for admin topic management
This commit is contained in:
parent
6524116e97
commit
d1671c60c1
3 changed files with 196 additions and 0 deletions
170
src/mudlib/commands/helpadmin.py
Normal file
170
src/mudlib/commands/helpadmin.py
Normal 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"
|
||||
)
|
||||
)
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
Loading…
Reference in a new issue