chat_management.py 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758
  1. from __future__ import annotations
  2. from datetime import UTC, datetime, timedelta
  3. from typing import Any
  4. from pyrogram.enums import ChatMembersFilter, ChatMemberStatus
  5. from pyrogram.errors import ChatNotModified
  6. from pyrogram.types import ChatPermissions, ChatPrivileges
  7. from wbb import BOT_ID, BOT_PROFILE_ID, SUDOERS, TELEGRAM_CONNECTED, app
  8. from wbb.services.blacklist_enforcement import (
  9. RiskControlValidationError,
  10. normalize_risk_rules,
  11. replace_legacy_rule_keywords,
  12. risk_rule_keywords,
  13. sync_legacy_blacklist,
  14. )
  15. from wbb.services.member_identity import observe_member_identity
  16. from wbb.utils.dbadmin import (
  17. delete_unavailable_managed_chat,
  18. get_managed_chat_settings,
  19. list_managed_chats,
  20. list_stored_invite_links,
  21. managed_chat_exists,
  22. mark_managed_chat_unavailable,
  23. revoke_stored_invite_link,
  24. store_invite_link,
  25. update_managed_chat_settings,
  26. upsert_managed_chat,
  27. )
  28. from wbb.utils.dbadmin import (
  29. list_recent_chat_members as list_cached_recent_chat_members,
  30. )
  31. from wbb.utils.dbfunctions import (
  32. add_chatbot,
  33. add_warn,
  34. captcha_off,
  35. captcha_on,
  36. check_chatbot,
  37. del_welcome,
  38. delete_filter,
  39. flood_off,
  40. flood_on,
  41. get_blacklisted_words,
  42. get_served_chats,
  43. get_warn,
  44. int_to_alpha,
  45. remove_warns,
  46. rm_chatbot,
  47. save_filter,
  48. set_welcome,
  49. )
  50. from wbb.utils.i18n import telegram_permission_label
  51. PERMISSION_NAMES = (
  52. "can_manage_chat",
  53. "can_delete_messages",
  54. "can_manage_video_chats",
  55. "can_restrict_members",
  56. "can_promote_members",
  57. "can_change_info",
  58. "can_post_messages",
  59. "can_edit_messages",
  60. "can_invite_users",
  61. "can_pin_messages",
  62. )
  63. DEFAULT_MEMBER_PERMISSIONS = {
  64. "can_send_messages": True,
  65. "can_send_media_messages": True,
  66. "can_send_other_messages": True,
  67. "can_send_polls": True,
  68. "can_add_web_page_previews": True,
  69. "can_change_info": False,
  70. "can_invite_users": True,
  71. "can_pin_messages": False,
  72. }
  73. class ChatManagementError(RuntimeError):
  74. def __init__(self, code: str, message: str, *, status: int = 400):
  75. super().__init__(message)
  76. self.code = code
  77. self.status = status
  78. def _ensure_telegram_connected() -> None:
  79. if not TELEGRAM_CONNECTED:
  80. raise ChatManagementError(
  81. "telegram_not_connected",
  82. "所选机器人尚未连接 Telegram。",
  83. status=409,
  84. )
  85. def _status_value(status: Any) -> str:
  86. return str(getattr(status, "value", status))
  87. def _privilege_list(member: Any) -> list[str]:
  88. if member.status == ChatMemberStatus.OWNER:
  89. return list(PERMISSION_NAMES)
  90. privileges = member.privileges
  91. if not privileges:
  92. return []
  93. return [name for name in PERMISSION_NAMES if bool(getattr(privileges, name, False))]
  94. def serialize_user(user: Any) -> dict[str, Any]:
  95. return {
  96. "id": str(user.id),
  97. "username": user.username,
  98. "first_name": user.first_name,
  99. "last_name": user.last_name,
  100. "is_bot": bool(user.is_bot),
  101. "is_deleted": bool(user.is_deleted),
  102. }
  103. def serialize_member(member: Any) -> dict[str, Any]:
  104. return {
  105. "user": serialize_user(member.user),
  106. "status": _status_value(member.status),
  107. "custom_title": member.custom_title,
  108. "privileges": _privilege_list(member),
  109. "until_date": member.until_date,
  110. }
  111. async def sync_managed_chat(chat_id: int) -> dict[str, Any]:
  112. _ensure_telegram_connected()
  113. try:
  114. chat = await app.get_chat(int(chat_id))
  115. bot_member = await app.get_chat_member(int(chat_id), BOT_ID)
  116. try:
  117. member_count = await app.get_chat_members_count(int(chat_id))
  118. except Exception:
  119. member_count = None
  120. await upsert_managed_chat(
  121. chat_id=int(chat.id),
  122. title=chat.title,
  123. username=chat.username,
  124. chat_type=_status_value(chat.type),
  125. member_count=member_count,
  126. accessible=bot_member.status
  127. not in {ChatMemberStatus.LEFT, ChatMemberStatus.BANNED},
  128. bot_status=_status_value(bot_member.status),
  129. bot_privileges=_privilege_list(bot_member),
  130. )
  131. return {
  132. "chat_id": int(chat.id),
  133. "title": chat.title,
  134. "username": chat.username,
  135. "type": _status_value(chat.type),
  136. "description": chat.description,
  137. "member_count": member_count,
  138. "accessible": True,
  139. "bot_status": _status_value(bot_member.status),
  140. "bot_privileges": _privilege_list(bot_member),
  141. "permissions": {
  142. name: bool(getattr(chat.permissions, name, False))
  143. for name in DEFAULT_MEMBER_PERMISSIONS
  144. }
  145. if chat.permissions
  146. else dict(DEFAULT_MEMBER_PERMISSIONS),
  147. }
  148. except Exception as exc:
  149. await mark_managed_chat_unavailable(int(chat_id), str(exc))
  150. raise ChatManagementError(
  151. "chat_unavailable", "机器人当前无法访问该群。", status=404
  152. ) from exc
  153. async def hydrate_managed_chats() -> None:
  154. if not TELEGRAM_CONNECTED:
  155. return
  156. _, total = await list_managed_chats(page=1, page_size=1, chat_type="group")
  157. if total:
  158. return
  159. if BOT_PROFILE_ID != "primary":
  160. return
  161. for item in await get_served_chats():
  162. chat_id = int(item["chat_id"])
  163. if chat_id >= 0:
  164. continue
  165. try:
  166. await sync_managed_chat(chat_id)
  167. except ChatManagementError:
  168. continue
  169. async def list_accessible_chats(
  170. *,
  171. query: str = "",
  172. page: int = 1,
  173. page_size: int = 20,
  174. actor_id: int | None = None,
  175. ) -> tuple[list[dict[str, Any]], int]:
  176. await hydrate_managed_chats()
  177. if actor_id is None or actor_id in SUDOERS:
  178. return await list_managed_chats(query=query, page=page, page_size=page_size, chat_type="group")
  179. all_items: list[dict[str, Any]] = []
  180. source_page = 1
  181. while True:
  182. batch, source_total = await list_managed_chats(
  183. query=query, page=source_page, page_size=100, chat_type="group"
  184. )
  185. all_items.extend(batch)
  186. if not batch or len(all_items) >= source_total:
  187. break
  188. source_page += 1
  189. visible: list[dict[str, Any]] = []
  190. for item in all_items:
  191. try:
  192. member = await app.get_chat_member(int(item["chat_id"]), actor_id)
  193. except Exception:
  194. continue
  195. if member.status in {ChatMemberStatus.OWNER, ChatMemberStatus.ADMINISTRATOR}:
  196. visible.append(item)
  197. start = (max(1, page) - 1) * max(1, page_size)
  198. return visible[start : start + page_size], len(visible)
  199. async def remove_unavailable_chat(chat_id: int) -> dict[str, Any]:
  200. if await delete_unavailable_managed_chat(chat_id):
  201. return {"chat_id": str(chat_id), "removed": True}
  202. if not await managed_chat_exists(chat_id):
  203. raise ChatManagementError(
  204. "chat_not_found",
  205. "群记录不存在或已被移除。",
  206. status=404,
  207. )
  208. raise ChatManagementError(
  209. "chat_still_accessible",
  210. "该群当前仍可用,不能从管理列表移除。",
  211. status=409,
  212. )
  213. async def ensure_permission(
  214. chat_id: int,
  215. permission: str,
  216. *,
  217. actor_id: int | None = None,
  218. ) -> None:
  219. _ensure_telegram_connected()
  220. try:
  221. bot_member = await app.get_chat_member(int(chat_id), BOT_ID)
  222. except Exception as exc:
  223. raise ChatManagementError(
  224. "bot_not_member", "机器人不在该群或无法读取群状态。", status=403
  225. ) from exc
  226. if bot_member.status != ChatMemberStatus.OWNER and permission not in _privilege_list(
  227. bot_member
  228. ):
  229. raise ChatManagementError(
  230. "bot_permission_missing",
  231. f"机器人缺少 Telegram 权限:{telegram_permission_label(permission)}",
  232. status=403,
  233. )
  234. if actor_id is None or actor_id in SUDOERS:
  235. return
  236. try:
  237. actor = await app.get_chat_member(int(chat_id), int(actor_id))
  238. except Exception as exc:
  239. raise ChatManagementError(
  240. "actor_not_admin", "你已不是该群管理员。", status=403
  241. ) from exc
  242. if actor.status != ChatMemberStatus.OWNER and permission not in _privilege_list(actor):
  243. raise ChatManagementError(
  244. "actor_permission_missing",
  245. f"你缺少 Telegram 权限:{telegram_permission_label(permission)}",
  246. status=403,
  247. )
  248. async def get_chat_overview(chat_id: int, actor_id: int | None = None) -> dict[str, Any]:
  249. _ensure_telegram_connected()
  250. if actor_id is not None and actor_id not in SUDOERS:
  251. try:
  252. member = await app.get_chat_member(chat_id, actor_id)
  253. except Exception as exc:
  254. raise ChatManagementError(
  255. "actor_not_admin", "你无权管理该群。", status=403
  256. ) from exc
  257. if member.status not in {
  258. ChatMemberStatus.OWNER,
  259. ChatMemberStatus.ADMINISTRATOR,
  260. }:
  261. raise ChatManagementError(
  262. "actor_not_admin", "你无权管理该群。", status=403
  263. )
  264. overview = await sync_managed_chat(chat_id)
  265. overview["automation"] = await get_automation_settings(chat_id)
  266. return overview
  267. async def get_automation_settings(chat_id: int) -> dict[str, Any]:
  268. settings = dict(await get_managed_chat_settings(chat_id))
  269. legacy_words = await get_blacklisted_words(chat_id)
  270. rules = normalize_risk_rules(
  271. settings.get("risk_rules") if "risk_rules" in settings else None,
  272. legacy_policy=(
  273. None if "risk_rules" in settings else settings.get("risk_control")
  274. ),
  275. fallback_keywords=legacy_words or settings.get("blacklist_words", []),
  276. )
  277. settings.pop("risk_control", None)
  278. settings["risk_rules"] = rules
  279. settings["blacklist_words"] = risk_rule_keywords(rules)
  280. identity_monitor = settings.get("identity_monitor") or {}
  281. settings["identity_monitor"] = {
  282. "enabled": bool(identity_monitor.get("enabled", False)),
  283. "notify_in_chat": bool(identity_monitor.get("notify_in_chat", False)),
  284. }
  285. return settings
  286. async def update_chat_profile(
  287. chat_id: int,
  288. *,
  289. title: str | None = None,
  290. description: str | None = None,
  291. actor_id: int | None = None,
  292. ) -> dict[str, Any]:
  293. if title is not None:
  294. if not isinstance(title, str):
  295. raise ChatManagementError("invalid_title", "群标题必须是文本。")
  296. title = title.strip()
  297. if not title or len(title) > 128:
  298. raise ChatManagementError("invalid_title", "群标题长度需要在 1 到 128 之间。")
  299. if description is not None:
  300. if not isinstance(description, str):
  301. raise ChatManagementError("invalid_description", "群描述必须是文本。")
  302. if len(description) > 255:
  303. raise ChatManagementError("invalid_description", "群描述不能超过 255 个字符。")
  304. await ensure_permission(chat_id, "can_change_info", actor_id=actor_id)
  305. overview = await sync_managed_chat(chat_id)
  306. title_changed = title is not None and title != overview.get("title")
  307. description_changed = (
  308. description is not None and description != (overview.get("description") or "")
  309. )
  310. if title_changed:
  311. try:
  312. await app.set_chat_title(chat_id, title)
  313. except ChatNotModified:
  314. pass
  315. except Exception as exc:
  316. raise ChatManagementError(
  317. "chat_profile_update_failed",
  318. "Telegram 未能更新群标题,请稍后重试。",
  319. status=502,
  320. ) from exc
  321. overview["title"] = title
  322. if description_changed:
  323. try:
  324. await app.set_chat_description(chat_id, description)
  325. except ChatNotModified:
  326. pass
  327. except Exception as exc:
  328. message = (
  329. "群标题已更新,但 Telegram 未能更新群描述,请重试。"
  330. if title_changed
  331. else "Telegram 未能更新群描述,请稍后重试。"
  332. )
  333. raise ChatManagementError(
  334. "chat_profile_update_failed", message, status=502
  335. ) from exc
  336. overview["description"] = description
  337. if title is not None:
  338. overview["title"] = title
  339. if description is not None:
  340. overview["description"] = description
  341. return overview
  342. async def update_chat_permissions(
  343. chat_id: int, values: dict[str, Any], *, actor_id: int | None = None
  344. ) -> dict[str, Any]:
  345. await ensure_permission(chat_id, "can_restrict_members", actor_id=actor_id)
  346. permissions = {
  347. key: bool(values.get(key, default))
  348. for key, default in DEFAULT_MEMBER_PERMISSIONS.items()
  349. }
  350. await app.set_chat_permissions(chat_id, ChatPermissions(**permissions))
  351. return await sync_managed_chat(chat_id)
  352. async def send_announcement(
  353. chat_id: int,
  354. *,
  355. text: str,
  356. media_type: str | None = None,
  357. file_id: str | None = None,
  358. pin: bool = False,
  359. actor_id: int | None = None,
  360. ) -> dict[str, Any]:
  361. await ensure_permission(chat_id, "can_change_info", actor_id=actor_id)
  362. if not text.strip() and not file_id:
  363. raise ChatManagementError("empty_announcement", "公告文字或媒体至少填写一项。")
  364. method_map = {
  365. "photo": app.send_photo,
  366. "animation": app.send_animation,
  367. "video": app.send_video,
  368. "document": app.send_document,
  369. }
  370. if file_id:
  371. method = method_map.get(media_type or "")
  372. if not method:
  373. raise ChatManagementError("invalid_media", "不支持的公告媒体类型。")
  374. keyword = "animation" if media_type == "animation" else media_type
  375. sent = await method(chat_id, **{keyword: file_id}, caption=text or None)
  376. else:
  377. sent = await app.send_message(chat_id, text)
  378. if pin:
  379. await ensure_permission(chat_id, "can_pin_messages", actor_id=actor_id)
  380. await app.pin_chat_message(chat_id, sent.id)
  381. return {"message_id": str(sent.id)}
  382. async def list_chat_admins(chat_id: int) -> list[dict[str, Any]]:
  383. await sync_managed_chat(chat_id)
  384. return [
  385. serialize_member(member)
  386. async for member in app.get_chat_members(
  387. chat_id, filter=ChatMembersFilter.ADMINISTRATORS
  388. )
  389. ]
  390. async def find_chat_member(chat_id: int, query: str) -> dict[str, Any]:
  391. _ensure_telegram_connected()
  392. query = query.strip()
  393. if not query:
  394. raise ChatManagementError("member_query_required", "请输入用户 ID 或 @username。")
  395. value: int | str = int(query) if query.lstrip("-").isdigit() else query
  396. try:
  397. member = await app.get_chat_member(chat_id, value)
  398. except Exception as exc:
  399. raise ChatManagementError("member_not_found", "未找到该群成员。", status=404) from exc
  400. await observe_member_identity(chat_id=chat_id, user=member.user)
  401. return serialize_member(member)
  402. async def search_chat_members(
  403. chat_id: int,
  404. query: str,
  405. *,
  406. limit: int = 20,
  407. ) -> list[dict[str, Any]]:
  408. _ensure_telegram_connected()
  409. normalized_query = query.strip()
  410. if not normalized_query:
  411. raise ChatManagementError(
  412. "member_query_required", "请输入成员昵称、用户名或用户 ID。"
  413. )
  414. limit = max(1, min(int(limit), 30))
  415. results: dict[int, dict[str, Any]] = {}
  416. if normalized_query.lstrip("-").isdigit() or normalized_query.startswith("@"):
  417. try:
  418. exact = await find_chat_member(chat_id, normalized_query)
  419. except ChatManagementError:
  420. exact = None
  421. if exact:
  422. results[int(exact["user"]["id"])] = exact
  423. else:
  424. try:
  425. members = app.get_chat_members(
  426. chat_id,
  427. query=normalized_query,
  428. limit=limit,
  429. filter=ChatMembersFilter.SEARCH,
  430. )
  431. if members is not None:
  432. async for member in members:
  433. await observe_member_identity(chat_id=chat_id, user=member.user)
  434. results[int(member.user.id)] = serialize_member(member)
  435. except Exception:
  436. pass
  437. cached = await list_cached_recent_chat_members(
  438. chat_id,
  439. query=normalized_query,
  440. limit=limit,
  441. )
  442. for item in cached:
  443. user_id = int(item["user_id"])
  444. if user_id in results or len(results) >= limit:
  445. continue
  446. try:
  447. member = await app.get_chat_member(chat_id, user_id)
  448. except Exception:
  449. continue
  450. if member.status in {ChatMemberStatus.LEFT, ChatMemberStatus.BANNED}:
  451. continue
  452. await observe_member_identity(chat_id=chat_id, user=member.user)
  453. results[user_id] = serialize_member(member)
  454. if not results:
  455. raise ChatManagementError(
  456. "member_not_found", "未找到匹配的群成员。", status=404
  457. )
  458. return list(results.values())[:limit]
  459. def _serialize_cached_member(item: dict[str, Any]) -> dict[str, Any]:
  460. return {
  461. "user": {
  462. "id": str(item["user_id"]),
  463. "username": item.get("username"),
  464. "first_name": item.get("first_name"),
  465. "last_name": item.get("last_name"),
  466. "is_bot": bool(item.get("is_bot", False)),
  467. "is_deleted": False,
  468. },
  469. "status": "recent",
  470. "custom_title": None,
  471. "privileges": [],
  472. "until_date": None,
  473. "last_seen_at": item.get("last_seen_at"),
  474. }
  475. async def list_recent_members(
  476. chat_id: int,
  477. *,
  478. limit: int = 30,
  479. ) -> list[dict[str, Any]]:
  480. _ensure_telegram_connected()
  481. limit = max(1, min(int(limit), 50))
  482. results: dict[int, dict[str, Any]] = {}
  483. try:
  484. members = app.get_chat_members(
  485. chat_id,
  486. limit=limit,
  487. filter=ChatMembersFilter.RECENT,
  488. )
  489. if members is not None:
  490. async for member in members:
  491. if member.user.is_bot:
  492. continue
  493. await observe_member_identity(chat_id=chat_id, user=member.user)
  494. results[int(member.user.id)] = serialize_member(member)
  495. except Exception:
  496. pass
  497. cached = await list_cached_recent_chat_members(chat_id, limit=limit)
  498. for item in cached:
  499. user_id = int(item["user_id"])
  500. if user_id not in results and len(results) < limit:
  501. results[user_id] = _serialize_cached_member(item)
  502. return list(results.values())[:limit]
  503. async def execute_member_action(
  504. chat_id: int,
  505. *,
  506. user_id: int,
  507. action: str,
  508. reason: str = "",
  509. duration_seconds: int | None = None,
  510. privileges: dict[str, Any] | None = None,
  511. actor_id: int | None = None,
  512. ) -> dict[str, Any]:
  513. permission = (
  514. "can_promote_members"
  515. if action in {"promote", "demote"}
  516. else "can_restrict_members"
  517. )
  518. await ensure_permission(chat_id, permission, actor_id=actor_id)
  519. until_date = datetime.now(UTC) + timedelta(seconds=duration_seconds) if duration_seconds else None
  520. if action == "warn":
  521. key = await int_to_alpha(user_id)
  522. current = await get_warn(chat_id, key)
  523. count = int(current.get("warns", 0)) + 1 if current else 1
  524. if count >= 3:
  525. await app.ban_chat_member(chat_id, user_id)
  526. await remove_warns(chat_id, key)
  527. return {"action": "ban", "warns": 3, "reason": reason}
  528. await add_warn(chat_id, key, {"warns": count})
  529. return {"action": "warn", "warns": count, "reason": reason}
  530. if action == "ban":
  531. kwargs = {"until_date": until_date} if until_date else {}
  532. await app.ban_chat_member(chat_id, user_id, **kwargs)
  533. elif action == "unban":
  534. await app.unban_chat_member(chat_id, user_id)
  535. elif action == "kick":
  536. await app.ban_chat_member(chat_id, user_id)
  537. await app.unban_chat_member(chat_id, user_id)
  538. elif action == "mute":
  539. kwargs = {"until_date": until_date} if until_date else {}
  540. await app.restrict_chat_member(
  541. chat_id, user_id, ChatPermissions(), **kwargs
  542. )
  543. elif action == "unmute":
  544. await app.restrict_chat_member(
  545. chat_id, user_id, ChatPermissions(**DEFAULT_MEMBER_PERMISSIONS)
  546. )
  547. elif action == "promote":
  548. values = {
  549. key: bool((privileges or {}).get(key, False))
  550. for key in PERMISSION_NAMES
  551. if key != "can_manage_chat"
  552. }
  553. await app.promote_chat_member(chat_id, user_id, ChatPrivileges(**values))
  554. elif action == "demote":
  555. values = {key: False for key in PERMISSION_NAMES if key != "can_manage_chat"}
  556. await app.promote_chat_member(chat_id, user_id, ChatPrivileges(**values))
  557. else:
  558. raise ChatManagementError("invalid_member_action", "不支持的成员操作。")
  559. return {"action": action, "user_id": str(user_id), "reason": reason}
  560. async def create_invite_link(
  561. chat_id: int,
  562. *,
  563. name: str,
  564. expires_at: datetime | None,
  565. member_limit: int | None,
  566. actor_id: int | None = None,
  567. ) -> dict[str, Any]:
  568. await ensure_permission(chat_id, "can_invite_users", actor_id=actor_id)
  569. link = await app.create_chat_invite_link(
  570. chat_id,
  571. name=name[:32] or None,
  572. expire_date=expires_at,
  573. member_limit=member_limit,
  574. )
  575. await store_invite_link(
  576. chat_id=chat_id,
  577. invite_link=link.invite_link,
  578. name=link.name or name,
  579. expires_at=link.expire_date,
  580. )
  581. return {
  582. "invite_link": link.invite_link,
  583. "name": link.name,
  584. "expires_at": link.expire_date,
  585. "member_limit": link.member_limit,
  586. "is_revoked": link.is_revoked,
  587. }
  588. async def revoke_invite_link(
  589. chat_id: int, invite_link: str, *, actor_id: int | None = None
  590. ) -> None:
  591. await ensure_permission(chat_id, "can_invite_users", actor_id=actor_id)
  592. await app.revoke_chat_invite_link(chat_id, invite_link)
  593. await revoke_stored_invite_link(chat_id, invite_link)
  594. async def get_invite_links(chat_id: int) -> list[dict[str, Any]]:
  595. return await list_stored_invite_links(chat_id)
  596. async def apply_automation_settings(
  597. chat_id: int, values: dict[str, Any], *, actor_id: int | None = None
  598. ) -> dict[str, Any]:
  599. await ensure_permission(chat_id, "can_change_info", actor_id=actor_id)
  600. previous = await get_automation_settings(chat_id)
  601. if "auto_replies" in values:
  602. for old in previous.get("auto_replies", []):
  603. await delete_filter(chat_id, str(old.get("keyword", "")))
  604. normalized_replies = []
  605. for item in values["auto_replies"][:100]:
  606. keyword = str(item.get("keyword") or "").strip().lower()
  607. if not keyword:
  608. continue
  609. reply = {
  610. "keyword": keyword[:100],
  611. "type": str(item.get("type") or "text"),
  612. "text": str(item.get("text") or "")[:4000],
  613. "file_id": item.get("file_id"),
  614. }
  615. normalized_replies.append(reply)
  616. await save_filter(
  617. chat_id,
  618. reply["keyword"],
  619. {
  620. "type": reply["type"],
  621. "data": reply["text"],
  622. "file_id": reply["file_id"],
  623. },
  624. )
  625. values["auto_replies"] = normalized_replies
  626. if "risk_rules" in values:
  627. try:
  628. rules = normalize_risk_rules(values["risk_rules"], strict=True)
  629. except RiskControlValidationError as exc:
  630. raise ChatManagementError("invalid_risk_rules", str(exc)) from exc
  631. values["risk_rules"] = rules
  632. values["blacklist_words"] = await sync_legacy_blacklist(
  633. chat_id, risk_rule_keywords(rules, enabled_only=True)
  634. )
  635. values.pop("risk_control", None)
  636. elif "risk_control" in values:
  637. try:
  638. rules = normalize_risk_rules(
  639. None,
  640. legacy_policy=values.pop("risk_control"),
  641. fallback_keywords=values.get("blacklist_words", []),
  642. strict=True,
  643. )
  644. except RiskControlValidationError as exc:
  645. raise ChatManagementError("invalid_risk_control", str(exc)) from exc
  646. values["risk_rules"] = rules
  647. values["blacklist_words"] = await sync_legacy_blacklist(
  648. chat_id, risk_rule_keywords(rules, enabled_only=True)
  649. )
  650. elif "blacklist_words" in values:
  651. rules = replace_legacy_rule_keywords(
  652. previous.get("risk_rules", []), values["blacklist_words"]
  653. )
  654. values["risk_rules"] = rules
  655. values["blacklist_words"] = await sync_legacy_blacklist(
  656. chat_id, risk_rule_keywords(rules, enabled_only=True)
  657. )
  658. if "identity_monitor" in values:
  659. identity_monitor = values["identity_monitor"]
  660. if not isinstance(identity_monitor, dict):
  661. raise ChatManagementError(
  662. "invalid_identity_monitor", "成员资料监控配置格式不正确。"
  663. )
  664. values["identity_monitor"] = {
  665. "enabled": bool(identity_monitor.get("enabled", False)),
  666. "notify_in_chat": bool(identity_monitor.get("notify_in_chat", False)),
  667. }
  668. if "welcome" in values:
  669. welcome = values["welcome"] or {}
  670. if welcome.get("enabled"):
  671. media = welcome.get("media") or {}
  672. media_type = str(media.get("type") or "text").capitalize()
  673. await set_welcome(
  674. chat_id,
  675. media_type,
  676. str(welcome.get("text") or "")[:4000],
  677. media.get("file_id"),
  678. )
  679. else:
  680. await del_welcome(chat_id)
  681. if "captcha_enabled" in values:
  682. await (captcha_on(chat_id) if values["captcha_enabled"] else captcha_off(chat_id))
  683. if "antiflood_enabled" in values:
  684. await (flood_on(chat_id) if values["antiflood_enabled"] else flood_off(chat_id))
  685. if "chatbot_enabled" in values:
  686. state = await check_chatbot()
  687. enabled = chat_id in state.get("bot", [])
  688. if values["chatbot_enabled"] and not enabled:
  689. await add_chatbot(chat_id)
  690. elif not values["chatbot_enabled"] and enabled:
  691. await rm_chatbot(chat_id)
  692. await update_managed_chat_settings(chat_id, values)
  693. return await get_automation_settings(chat_id)