test_blacklist_enforcement.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316
  1. from __future__ import annotations
  2. from datetime import UTC, datetime
  3. from types import SimpleNamespace
  4. from unittest.mock import AsyncMock
  5. import pytest
  6. from pyrogram.enums import ChatMemberStatus, MessageEntityType
  7. def make_message(
  8. *,
  9. text: str | None = None,
  10. caption: str | None = None,
  11. photo: object | None = None,
  12. entities: list[object] | None = None,
  13. user_id: int = 10,
  14. ):
  15. return SimpleNamespace(
  16. text=text,
  17. caption=caption,
  18. photo=photo,
  19. entities=entities or [],
  20. caption_entities=[],
  21. chat=SimpleNamespace(id=-100),
  22. from_user=SimpleNamespace(
  23. id=user_id,
  24. is_bot=False,
  25. mention=f"user-{user_id}",
  26. ),
  27. delete=AsyncMock(),
  28. )
  29. def grant_bot_permissions(app_modules, *, delete: bool = True, restrict: bool = True):
  30. app_modules.app.members[(-100, 999)] = SimpleNamespace(
  31. status=ChatMemberStatus.ADMINISTRATOR,
  32. privileges=SimpleNamespace(
  33. can_delete_messages=delete,
  34. can_restrict_members=restrict,
  35. ),
  36. )
  37. def test_risk_policy_supports_legacy_keywords_and_rejects_conflicting_actions(
  38. app_modules,
  39. ):
  40. service = app_modules.load("wbb.services.blacklist_enforcement")
  41. policy = service.normalize_risk_control(None, fallback_keywords=["做爱"])
  42. assert policy == {
  43. "enabled": True,
  44. "keywords": ["做爱"],
  45. "match_images": False,
  46. "match_links": False,
  47. "actions": ["delete"],
  48. "duration_seconds": 3600,
  49. }
  50. with pytest.raises(service.RiskControlValidationError):
  51. service.normalize_risk_control(
  52. {"enabled": True, "actions": ["warn", "ban"]},
  53. strict=True,
  54. )
  55. def test_risk_match_supports_chinese_images_links_and_normalization(app_modules):
  56. service = app_modules.load("wbb.services.blacklist_enforcement")
  57. assert service.find_blacklist_match("这句话包含做爱关键词", ["做爱"]) == "做爱"
  58. assert service.find_blacklist_match("SPAM message", ["spam"]) == "spam"
  59. assert service.find_blacklist_match("普通消息", ["做爱"]) is None
  60. policy = service.normalize_risk_control(
  61. {
  62. "enabled": True,
  63. "keywords": [],
  64. "match_images": True,
  65. "match_links": True,
  66. "actions": ["delete"],
  67. }
  68. )
  69. match = service.detect_risk_match(
  70. make_message(
  71. text="查看链接",
  72. photo=object(),
  73. entities=[SimpleNamespace(type=MessageEntityType.URL)],
  74. ),
  75. policy,
  76. )
  77. assert match.trigger_types == ("image", "link")
  78. async def test_admin_message_is_deleted_without_member_punishment(
  79. app_modules, monkeypatch
  80. ):
  81. service = app_modules.load("wbb.services.blacklist_enforcement")
  82. grant_bot_permissions(app_modules)
  83. message = make_message(text="管理员也包含做爱关键词")
  84. app_modules.app.members[(-100, 10)] = SimpleNamespace(
  85. status=ChatMemberStatus.ADMINISTRATOR
  86. )
  87. ban_member = AsyncMock()
  88. monkeypatch.setattr(service.app, "ban_chat_member", ban_member, raising=False)
  89. result = await service.enforce_risk_message(
  90. message,
  91. {
  92. "enabled": True,
  93. "keywords": ["做爱"],
  94. "actions": ["delete", "ban"],
  95. },
  96. )
  97. message.delete.assert_awaited_once()
  98. ban_member.assert_not_awaited()
  99. assert result.deleted is True
  100. assert result.applied_member_action == "none"
  101. async def test_regular_member_photo_can_be_deleted_and_kicked(
  102. app_modules, monkeypatch
  103. ):
  104. service = app_modules.load("wbb.services.blacklist_enforcement")
  105. grant_bot_permissions(app_modules)
  106. message = make_message(photo=object())
  107. ban_member = AsyncMock()
  108. unban_member = AsyncMock()
  109. monkeypatch.setattr(service.app, "ban_chat_member", ban_member, raising=False)
  110. monkeypatch.setattr(service.app, "unban_chat_member", unban_member, raising=False)
  111. result = await service.enforce_risk_message(
  112. message,
  113. {
  114. "enabled": True,
  115. "match_images": True,
  116. "actions": ["delete", "kick"],
  117. },
  118. )
  119. message.delete.assert_awaited_once()
  120. ban_member.assert_awaited_once_with(-100, 10)
  121. unban_member.assert_awaited_once_with(-100, 10)
  122. assert result.applied_member_action == "kick"
  123. async def test_delete_failure_does_not_block_selected_ban(app_modules, monkeypatch):
  124. service = app_modules.load("wbb.services.blacklist_enforcement")
  125. grant_bot_permissions(app_modules)
  126. message = make_message(text="做爱")
  127. message.delete.side_effect = RuntimeError("missing delete permission")
  128. ban_member = AsyncMock()
  129. monkeypatch.setattr(service.app, "ban_chat_member", ban_member, raising=False)
  130. result = await service.enforce_risk_message(
  131. message,
  132. {
  133. "enabled": True,
  134. "keywords": ["做爱"],
  135. "actions": ["delete", "ban"],
  136. },
  137. )
  138. ban_member.assert_awaited_once()
  139. args, kwargs = ban_member.await_args
  140. assert args == (-100, 10)
  141. assert 3500 <= (kwargs["until_date"] - datetime.now(UTC)).total_seconds() <= 3600
  142. assert result.deleted is False
  143. assert result.applied_member_action == "ban"
  144. assert result.duration_seconds == 3600
  145. async def test_warning_policy_runs_again_after_threshold_ban_and_unban(
  146. app_modules, monkeypatch
  147. ):
  148. service = app_modules.load("wbb.services.blacklist_enforcement")
  149. grant_bot_permissions(app_modules)
  150. ban_member = AsyncMock()
  151. monkeypatch.setattr(service.app, "ban_chat_member", ban_member, raising=False)
  152. policy = {
  153. "enabled": True,
  154. "keywords": ["做爱"],
  155. "actions": ["warn"],
  156. "duration_seconds": 1800,
  157. }
  158. first = await service.enforce_risk_message(make_message(text="做爱"), policy)
  159. second = await service.enforce_risk_message(make_message(text="再次做爱"), policy)
  160. third = await service.enforce_risk_message(make_message(text="第三次做爱"), policy)
  161. after_unban = await service.enforce_risk_message(
  162. make_message(text="解封后再次做爱"), policy
  163. )
  164. ban_member.assert_awaited_once()
  165. args, kwargs = ban_member.await_args
  166. assert args == (-100, 10)
  167. assert 1700 <= (kwargs["until_date"] - datetime.now(UTC)).total_seconds() <= 1800
  168. assert (first.applied_member_action, first.warning_count) == ("warn", 1)
  169. assert (second.applied_member_action, second.warning_count) == ("warn", 2)
  170. assert (third.applied_member_action, third.warning_count) == ("ban", 3)
  171. assert (after_unban.applied_member_action, after_unban.warning_count) == (
  172. "warn",
  173. 1,
  174. )
  175. async def test_multiple_matching_rules_merge_delete_and_highest_member_action(
  176. app_modules, monkeypatch
  177. ):
  178. service = app_modules.load("wbb.services.blacklist_enforcement")
  179. grant_bot_permissions(app_modules)
  180. message = make_message(
  181. text="广告请访问 https://example.com",
  182. photo=object(),
  183. )
  184. ban_member = AsyncMock()
  185. unban_member = AsyncMock()
  186. monkeypatch.setattr(service.app, "ban_chat_member", ban_member, raising=False)
  187. monkeypatch.setattr(service.app, "unban_chat_member", unban_member, raising=False)
  188. rules = [
  189. {
  190. "rule_id": "keyword-rule",
  191. "name": "广告关键词",
  192. "enabled": True,
  193. "keywords": ["广告"],
  194. "actions": ["delete", "warn"],
  195. "duration_seconds": 1800,
  196. },
  197. {
  198. "rule_id": "link-rule",
  199. "name": "链接拦截",
  200. "enabled": True,
  201. "match_links": True,
  202. "actions": ["kick"],
  203. },
  204. {
  205. "rule_id": "image-rule",
  206. "name": "图片封禁",
  207. "enabled": True,
  208. "match_images": True,
  209. "actions": ["delete", "ban"],
  210. "duration_seconds": 7200,
  211. },
  212. ]
  213. result = await service.enforce_risk_message(message, rules)
  214. message.delete.assert_awaited_once()
  215. ban_member.assert_awaited_once()
  216. unban_member.assert_not_awaited()
  217. assert result.requested_member_action == "ban"
  218. assert result.applied_member_action == "ban"
  219. assert result.duration_seconds == 7200
  220. assert [match.rule_name for match in result.matches] == [
  221. "广告关键词",
  222. "链接拦截",
  223. "图片封禁",
  224. ]
  225. assert all(name in app_modules.app.sent_messages[-1][1] for name in ["广告关键词", "链接拦截", "图片封禁"])
  226. def test_risk_rules_require_unique_ids_valid_triggers_and_ban_duration(app_modules):
  227. service = app_modules.load("wbb.services.blacklist_enforcement")
  228. base = {
  229. "rule_id": "same-id",
  230. "name": "测试规则",
  231. "enabled": True,
  232. "keywords": ["广告"],
  233. "actions": ["ban"],
  234. "duration_seconds": 3600,
  235. }
  236. with pytest.raises(service.RiskControlValidationError, match="规则 ID 不能重复"):
  237. service.normalize_risk_rules([base, base], strict=True)
  238. with pytest.raises(service.RiskControlValidationError, match="触发条件"):
  239. service.normalize_risk_rules(
  240. [{**base, "rule_id": "empty", "keywords": []}], strict=True
  241. )
  242. with pytest.raises(service.RiskControlValidationError, match="处罚时长"):
  243. service.normalize_risk_rules(
  244. [{**base, "duration_seconds": 30}], strict=True
  245. )
  246. async def test_mute_keeps_member_in_chat_and_uses_configured_duration(
  247. app_modules, monkeypatch
  248. ):
  249. service = app_modules.load("wbb.services.blacklist_enforcement")
  250. grant_bot_permissions(app_modules)
  251. restrict_member = AsyncMock()
  252. ban_member = AsyncMock()
  253. monkeypatch.setattr(service.app, "restrict_chat_member", restrict_member, raising=False)
  254. monkeypatch.setattr(service.app, "ban_chat_member", ban_member, raising=False)
  255. result = await service.enforce_risk_message(
  256. make_message(text="广告"),
  257. [
  258. {
  259. "rule_id": "mute-ads",
  260. "name": "广告禁言",
  261. "enabled": True,
  262. "keywords": ["广告"],
  263. "actions": ["delete", "mute"],
  264. "duration_seconds": 900,
  265. }
  266. ],
  267. )
  268. restrict_member.assert_awaited_once()
  269. args, kwargs = restrict_member.await_args
  270. assert args[:2] == (-100, 10)
  271. assert 800 <= (kwargs["until_date"] - datetime.now(UTC)).total_seconds() <= 900
  272. ban_member.assert_not_awaited()
  273. assert result.applied_member_action == "mute"
  274. assert result.duration_seconds == 900