Browse Source

fix: manage channel admins through Bot API

AI-Co-Authored-By: Codex
chendeben 5 days ago
parent
commit
d401f8eec1
2 changed files with 78 additions and 41 deletions
  1. 21 8
      tests/test_channel_management.py
  2. 57 33
      wbb/services/channel_management.py

+ 21 - 8
tests/test_channel_management.py

@@ -247,15 +247,28 @@ async def test_media_post_can_replace_media_and_channel_admin_rights_are_checked
     assert changed["file_id"] == "photo-file-2"
     assert edit_media.await_count == 1
 
-    app_modules.app.members[(-100123, 42)] = SimpleNamespace(
-        status=ChatMemberStatus.MEMBER,
-        user=SimpleNamespace(id=42, username=None, first_name="用户", last_name=None, is_bot=False, is_deleted=False),
-        privileges=None, custom_title=None, until_date=None,
+    member = {"status": "member", "user": {"id": 42, "first_name": "用户"}}
+    admin = {"status": "administrator", "user": member["user"], "can_post_messages": True}
+    bot_api = AsyncMock(side_effect=[member, True, admin])
+    monkeypatch.setattr(service, "channel_bot_api", bot_api)
+    promoted = await service.set_channel_admin(-100123, 42, ["can_post_messages"])
+    assert promoted["status"] == "administrator"
+    assert promoted["privileges"] == ["can_post_messages"]
+    assert [item.args[0] for item in bot_api.await_args_list] == [
+        "getChatMember", "promoteChatMember", "getChatMember",
+    ]
+    assert bot_api.await_args_list[1].kwargs["can_post_messages"] is True
+    assert bot_api.await_args_list[1].kwargs["can_edit_messages"] is False
+    bot_api.reset_mock(side_effect=True)
+    bot_api.side_effect = [admin, True]
+    removed = await service.remove_channel_admin(-100123, 42)
+    assert removed["removed"] is True
+    assert [item.args[0] for item in bot_api.await_args_list] == [
+        "getChatMember", "promoteChatMember",
+    ]
+    assert not any(
+        bot_api.await_args_list[1].kwargs[key] for key in service.CHANNEL_ADMIN_PRIVILEGES
     )
-    promote = AsyncMock()
-    monkeypatch.setattr(service.app, "promote_chat_member", promote, raising=False)
-    await service.set_channel_admin(-100123, 42, ["can_post_messages"])
-    assert promote.await_count == 1
     with pytest.raises(service.ChatManagementError) as caught:
         await service.set_channel_admin(-100123, 42, ["can_restrict_members"])
     assert caught.value.code == "invalid_privileges"

+ 57 - 33
wbb/services/channel_management.py

@@ -11,7 +11,6 @@ from pymongo.errors import DuplicateKeyError
 from pyrogram.enums import ChatMemberStatus, ChatType, ParseMode
 from pyrogram.errors import RPCError
 from pyrogram.types import (
-    ChatPrivileges,
     InputMediaAnimation,
     InputMediaDocument,
     InputMediaPhoto,
@@ -63,6 +62,48 @@ def telegram_error_detail(exc: Exception) -> str:
     return f"{type(exc).__name__}: {str(exc)[:180]}" if str(exc) else type(exc).__name__
 
 
+async def channel_bot_api(method: str, **params: Any) -> Any:
+    token = str(getattr(wbb, "BOT_TOKEN", "") or "")
+    if not token:
+        raise ChatManagementError("bot_token_missing", "机器人令牌不可用。", status=503)
+    data = {
+        key: ("true" if value else "false") if isinstance(value, bool) else str(value)
+        for key, value in params.items()
+    }
+    try:
+        async with ClientSession(timeout=ClientTimeout(total=15)) as session:
+            async with session.post(f"https://api.telegram.org/bot{token}/{method}", data=data) as response:
+                payload = await response.json(content_type=None)
+    except Exception as exc:
+        raise ChatManagementError(
+            "channel_telegram_unavailable", "Telegram 管理员接口暂时不可用。", status=502
+        ) from exc
+    if not isinstance(payload, dict) or not payload.get("ok"):
+        detail = str(payload.get("description") or "请求失败")[:180] if isinstance(payload, dict) else "响应无效"
+        raise ChatManagementError(
+            "channel_telegram_failed", f"Telegram 拒绝管理员操作:{detail}", status=502
+        )
+    return payload.get("result")
+
+
+def serialize_bot_api_member(member: dict[str, Any]) -> dict[str, Any]:
+    user = member.get("user") or {}
+    return {
+        "user": {
+            "id": str(user.get("id") or ""),
+            "username": user.get("username"),
+            "first_name": user.get("first_name"),
+            "last_name": user.get("last_name"),
+            "is_bot": bool(user.get("is_bot")),
+            "is_deleted": bool(user.get("is_deleted")),
+        },
+        "status": "owner" if member.get("status") == "creator" else member.get("status"),
+        "custom_title": member.get("custom_title"),
+        "privileges": [name for name in sorted(CHANNEL_ADMIN_PRIVILEGES) if member.get(name)],
+        "until_date": member.get("until_date"),
+    }
+
+
 def public_post(post: dict[str, Any]) -> dict[str, Any]:
     return {key: value for key, value in post.items() if key not in {"_id", "telegram_key"}}
 
@@ -177,45 +218,28 @@ async def set_channel_admin(chat_id: int, user_id: int, privileges: list[str]) -
     allowed = set(_privilege_list(bot))
     if bot.status != ChatMemberStatus.OWNER and requested - allowed:
         raise ChatManagementError("privilege_exceeds_bot", "不能授予超出机器人自身的权限。", status=403)
-    try:
-        target = await app.get_chat_member(chat_id, user_id)
-        if target.status == ChatMemberStatus.OWNER:
-            raise ChatManagementError("owner_protected", "不能修改频道所有者。", status=403)
-        values = {key: key in requested for key in CHANNEL_ADMIN_PRIVILEGES}
-        await app.promote_chat_member(chat_id, user_id, ChatPrivileges(**values))
-    except ChatManagementError:
-        raise
-    except RPCError as exc:
-        raise ChatManagementError(
-            "admin_update_failed", f"Telegram 拒绝调整管理员:{telegram_error_detail(exc)}", status=502
-        ) from exc
-    except Exception as exc:
-        raise ChatManagementError("admin_update_failed", "Telegram 未能调整管理员;请确认用户已订阅频道及机器人有权管理该用户。", status=502) from exc
-    try:
-        return serialize_member(await app.get_chat_member(chat_id, user_id))
-    except Exception:
-        return {"user_id": str(user_id), "updated": True}
+    target = await channel_bot_api("getChatMember", chat_id=chat_id, user_id=user_id)
+    if target.get("status") in {"creator", "owner"}:
+        raise ChatManagementError("owner_protected", "不能修改频道所有者。", status=403)
+    values = {key: key in requested for key in CHANNEL_ADMIN_PRIVILEGES}
+    await channel_bot_api("promoteChatMember", chat_id=chat_id, user_id=user_id, **values)
+    return serialize_bot_api_member(
+        await channel_bot_api("getChatMember", chat_id=chat_id, user_id=user_id)
+    )
 
 
 async def remove_channel_admin(chat_id: int, user_id: int) -> dict:
     await ensure_channel(chat_id)
     await ensure_permission(chat_id, "can_promote_members")
-    try:
-        target = await app.get_chat_member(chat_id, user_id)
-    except Exception as exc:
-        raise ChatManagementError("admin_lookup_failed", "Telegram 未能读取该用户的频道身份。", status=502) from exc
-    if target.status == ChatMemberStatus.OWNER:
+    target = await channel_bot_api("getChatMember", chat_id=chat_id, user_id=user_id)
+    if target.get("status") in {"creator", "owner"}:
         raise ChatManagementError("owner_protected", "不能移除频道所有者。", status=403)
-    if target.status != ChatMemberStatus.ADMINISTRATOR:
+    if target.get("status") != "administrator":
         raise ChatManagementError("not_admin", "该用户不是频道管理员。", status=409)
-    try:
-        await app.promote_chat_member(chat_id, user_id, ChatPrivileges(can_manage_chat=False))
-    except RPCError as exc:
-        raise ChatManagementError(
-            "admin_remove_failed", f"Telegram 拒绝移除管理员:{telegram_error_detail(exc)}", status=502
-        ) from exc
-    except Exception as exc:
-        raise ChatManagementError("admin_remove_failed", "Telegram 未能移除管理员。", status=502) from exc
+    await channel_bot_api(
+        "promoteChatMember", chat_id=chat_id, user_id=user_id,
+        **{key: False for key in CHANNEL_ADMIN_PRIVILEGES},
+    )
     return {"user_id": str(user_id), "removed": True}