Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 45 additions & 1 deletion src/api_ptt.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,20 @@
_pool_lock = threading.Lock()


def _bad_post_type_from_name(name: Optional[str]) -> Optional["PyPtt.BadPostType"]:
"""劣退/惡退分類的字串名 → PyPtt.BadPostType。

BadPostType 是 IntEnum(不像 SearchType 是 str 子類),MCP 收到的字串不能
直接傳給 PyPtt,得先用成員名查回 enum。未知名稱丟 ValueError 由呼叫端轉錯誤。
"""
if name is None:
return None
try:
return PyPtt.BadPostType[name]
except KeyError:
raise ValueError(name)


def _login_new_service(
ptt_id: str, ptt_pw: str
) -> Tuple[Optional[Any], Dict[str, Any]]:
Expand Down Expand Up @@ -400,7 +414,11 @@ def reply_post(

@mcp.tool()
def del_post(
board: str, aid: Optional[str] = None, index: int = 0
board: str,
aid: Optional[str] = None,
index: int = 0,
bad_post_type: Optional[str] = None,
bad_post_reason: Optional[str] = None,
) -> Dict[str, Any]:
"""刪除文章。

Expand All @@ -409,22 +427,48 @@ def del_post(

註記:此函式必須先登入 PTT。

劣退/惡退(劣文退文,PTT 選單上顯示為「惡退」,兩種說法都是同一件事):
板主刪除「他人」文章時,可對作者記一支退文(badpost)。
只有板主刪他人文章時才適用;刪自己的文章請勿傳 bad_post_type。

Args:
board (str): 文章所在的看板名稱。
aid (str, optional): 文章的 ID (AID)。與 `index` 擇一使用。
index (int, optional): 文章的索引,從 1 開始。與 `aid` 擇一使用。
bad_post_type (str, optional): 劣退/惡退分類,四選一:
"AD"(廣告)、"BAD_LANGUAGE"(不當用辭)、
"PERSONAL_ATTACK"(人身攻擊)、"OTHER"(其他)。
不傳=不記劣退/惡退,只單純刪文。
bad_post_reason (str, optional): 劣退/惡退理由。僅當 bad_post_type 為 "OTHER" 時
必須傳入,且不可超過 50 bytes(Big5 編碼);
其餘分類請勿傳入。

Returns:
Dict[str, Any]: 一個包含操作結果的字典。
成功: {'success': True, 'message': '刪除成功'}
失敗: {'success': False, 'message': '...', 'code': '...'}
bad_post_type 無效: {'success': False, 'message': '...',
'code': 'BAD_PARAM'}
"""
try:
bpt = _bad_post_type_from_name(bad_post_type)
except ValueError:
return {
"success": False,
"message": (
f"未知的 bad_post_type: {bad_post_type!r},"
"可用: AD, BAD_LANGUAGE, PERSONAL_ATTACK, OTHER"
),
"code": "BAD_PARAM",
}
return _call_ptt_service(
memory_storage,
"del_post",
board=board,
aid=aid,
index=index,
bad_post_type=bpt,
bad_post_reason=bad_post_reason,
success_message="刪除成功",
)

Expand Down
58 changes: 58 additions & 0 deletions src/test_del_post.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import os
import sys

import pytest

# api_ptt / utils / PyPtt are shared via sys.modules. Sibling tests stub PyPtt
# (an empty ModuleType) so they can run without it installed, and monkeypatch
# api_ptt.PyPtt. This test needs the REAL PyPtt.BadPostType, so it briefly
# swaps in the real modules and restores the exact prior sys.modules state in a
# finally block — otherwise reloading api_ptt would leak into those tests.
_SHARED = ("PyPtt", "utils", "api_ptt")


def _load_real_api_ptt():
src_dir = os.path.dirname(os.path.abspath(__file__))
if src_dir not in sys.path:
sys.path.insert(0, src_dir)
# Evict any stub PyPtt (no __file__) and the modules that bound it.
if not getattr(sys.modules.get("PyPtt"), "__file__", None):
for m in _SHARED:
sys.modules.pop(m, None)
import PyPtt

if not hasattr(PyPtt, "BadPostType"):
pytest.skip("real PyPtt not installed; helper needs PyPtt.BadPostType")
import api_ptt

return api_ptt, PyPtt


def test_bad_post_type_from_name():
snapshot = {m: sys.modules.get(m) for m in _SHARED}
try:
api_ptt, PyPtt = _load_real_api_ptt()

# Every valid name maps to the same IntEnum member (MCP sends a string,
# PyPtt wants the enum).
for name in ("AD", "BAD_LANGUAGE", "PERSONAL_ATTACK", "OTHER"):
assert api_ptt._bad_post_type_from_name(name) is PyPtt.BadPostType[name]

# None means "no bad-post", passes straight through.
assert api_ptt._bad_post_type_from_name(None) is None

# Unknown name raises ValueError (del_post turns it into a BAD_PARAM error).
with pytest.raises(ValueError):
api_ptt._bad_post_type_from_name("NOPE")
finally:
for m in _SHARED:
mod = snapshot[m]
if mod is None:
sys.modules.pop(m, None)
else:
sys.modules[m] = mod


if __name__ == "__main__":
test_bad_post_type_from_name()
print("OK")
Loading