Skip to content

Commit 7410bde

Browse files
committed
fix(repeater): 持久化兜底不可以 ban 反查
1 parent ef6c3a6 commit 7410bde

19 files changed

Lines changed: 503 additions & 40 deletions

src/features/corpus/community_source.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,9 @@ async def replace_answers(self, keywords: str, answers: list[Answer], clear_time
161161
async def append_ban(self, keywords: str, ban: Ban) -> None:
162162
return None
163163

164+
async def find_ban_reply_target(self, group_id: int, reply_message: str) -> tuple[str, str] | None:
165+
return None
166+
164167
async def _post_contribute(self, body: dict[str, Any]) -> None:
165168
if not self._api_bases:
166169
return

src/features/corpus/composite_repo.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -319,3 +319,9 @@ async def replace_answers(self, keywords: str, answers: list[Answer], clear_time
319319

320320
async def append_ban(self, keywords: str, ban: Ban) -> None:
321321
await self._local.append_ban(keywords, ban)
322+
323+
async def find_ban_reply_target(self, group_id: int, reply_message: str) -> tuple[str, str] | None:
324+
find_target = getattr(self._local, "find_ban_reply_target", None)
325+
if not callable(find_target):
326+
return None
327+
return await find_target(group_id, reply_message)

src/foundation/db/repository.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,10 @@ async def append_ban(self, keywords: str, ban: Ban) -> None:
7777
"""
7878
...
7979

80+
async def find_ban_reply_target(self, group_id: int, reply_message: str) -> tuple[str, str] | None:
81+
"""按群号与 reply 原文精确反查 (pre_keywords, reply_keywords)。"""
82+
...
83+
8084

8185
class ContextRepositoryExistenceMixin:
8286
"""为已实现 `find_by_keywords` 的仓储提供 `context_exists_by_keywords` 默认实现。"""

src/foundation/db/repository_impl.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,22 @@ async def append_ban(self, keywords: str, ban: Ban) -> None:
128128
{"$push": {"ban": ban.model_dump(by_alias=True)}},
129129
)
130130

131+
async def find_ban_reply_target(self, group_id: int, reply_message: str) -> tuple[str, str] | None:
132+
collection = Context.get_pymongo_collection()
133+
pipeline = [
134+
{"$match": {"answers": {"$elemMatch": {"group_id": int(group_id), "messages": str(reply_message)}}}},
135+
{"$unwind": "$answers"},
136+
{"$match": {"answers.group_id": int(group_id), "answers.messages": str(reply_message)}},
137+
{"$sort": {"answers.time": -1}},
138+
{"$limit": 1},
139+
{"$project": {"_id": 0, "keywords": 1, "reply_keywords": "$answers.keywords"}},
140+
]
141+
docs = await collection.aggregate(pipeline).to_list(length=1)
142+
if not docs:
143+
return None
144+
doc = docs[0]
145+
return str(doc.get("keywords") or ""), str(doc.get("reply_keywords") or "")
146+
131147

132148
class MongoMessageRepository:
133149
"""MongoDB 版 MessageRepository 实现"""

src/foundation/db/repository_pg.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1133,6 +1133,25 @@ async def append_ban(self, keywords: str, ban: Ban) -> None:
11331133
await session.commit()
11341134
await clear_reply_query_snapshot_cache(keywords)
11351135

1136+
async def find_ban_reply_target(self, group_id: int, reply_message: str) -> tuple[str, str] | None:
1137+
async with get_session(read_only=True) as session:
1138+
result = await session.execute(
1139+
select(ContextRow.keywords, ContextAnswerRow.keywords)
1140+
.join(ContextAnswerRow, ContextAnswerRow.context_id == ContextRow.id)
1141+
.join(ContextAnswerMessageRow, ContextAnswerMessageRow.answer_id == ContextAnswerRow.id)
1142+
.where(
1143+
ContextAnswerRow.group_id == int(group_id),
1144+
ContextAnswerMessageRow.message == _s(reply_message),
1145+
)
1146+
.order_by(ContextAnswerRow.time.desc(), ContextAnswerMessageRow.id.desc())
1147+
.limit(1)
1148+
)
1149+
row = result.one_or_none()
1150+
if row is None:
1151+
return None
1152+
pre_keywords, reply_keywords = row
1153+
return str(pre_keywords), str(reply_keywords)
1154+
11361155

11371156
def row_to_message(row: MessageRow) -> Message:
11381157
from src.foundation.db.modules import Message

src/plugins/dream/ban_handlers.py

Lines changed: 49 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,11 @@
66
from nonebot.adapters import Bot # noqa: TC002
77
from nonebot.adapters.onebot.v11 import GroupMessageEvent, GroupRecallNoticeEvent, Message
88
from nonebot.exception import ActionFailed
9-
from nonebot.rule import Rule, keyword, to_me
9+
from nonebot.rule import Rule
1010
from nonebot.typing import T_State # noqa: TC002
1111

1212
from src.features.cmd_perm import group_message_permission_for_command
13+
from src.shared.reply_command_rule import event_has_reply_target, event_targets_self, extract_reply_id_from_raw_message
1314
from src.shared.utils.array2cqcode import try_convert_to_cqcode
1415

1516
from .ban_ack_state import DREAM_BAN_ACK_SENT_STATE_KEY
@@ -19,11 +20,52 @@
1920

2021

2122
async def is_reply_for_ban(event: GroupMessageEvent) -> bool:
22-
return bool(event.reply)
23+
return event_has_reply_target(event)
24+
25+
26+
async def is_dream_ban_trigger(event: GroupMessageEvent) -> bool:
27+
if "不可以" not in event.get_plaintext():
28+
return False
29+
if not await is_reply_for_ban(event):
30+
return False
31+
return event_targets_self(event)
32+
33+
34+
def extract_dream_ban_reply_raw_from_message(message: Message | str) -> str:
35+
if isinstance(message, str):
36+
return message
37+
38+
raw_message = ""
39+
for item in message:
40+
raw_reply = str(item)
41+
raw_message += re.sub(r"(\[CQ\:.+)(?:,url=*)(\])", r"\1\2", raw_reply)
42+
if not raw_message.strip():
43+
raw_message = message.extract_plain_text()
44+
return raw_message
45+
46+
47+
async def resolve_dream_ban_reply_raw(bot: Bot, event: GroupMessageEvent) -> str:
48+
if event.reply and getattr(event.reply, "message", None):
49+
return extract_dream_ban_reply_raw_from_message(event.reply.message)
50+
51+
reply_id = extract_reply_id_from_raw_message(event.raw_message)
52+
if reply_id is None:
53+
return ""
54+
55+
try:
56+
msg = await bot.get_msg(message_id=reply_id)
57+
except ActionFailed:
58+
logger.warning(
59+
f"bot [{event.self_id}] dream ban cleanup get_msg failed in group [{event.group_id}] "
60+
f"for reply_id [{reply_id}]"
61+
)
62+
return ""
63+
64+
return extract_dream_ban_reply_raw_from_message(Message(msg["message"]))
2365

2466

2567
dream_ban_cleanup_msg = on_message(
26-
rule=to_me() & keyword("不可以") & Rule(is_reply_for_ban),
68+
rule=Rule(is_dream_ban_trigger),
2769
priority=4,
2870
block=False,
2971
permission=group_message_permission_for_command("dream.ban_cleanup"),
@@ -32,16 +74,15 @@ async def is_reply_for_ban(event: GroupMessageEvent) -> bool:
3274

3375
@dream_ban_cleanup_msg.handle()
3476
async def _(_bot: Bot, event: GroupMessageEvent, state: T_State):
35-
if "[CQ:reply," not in try_convert_to_cqcode(event.raw_message):
77+
raw_message = await resolve_dream_ban_reply_raw(_bot, event)
78+
if not raw_message.strip():
3679
return
37-
raw_message = ""
38-
for item in event.reply.message: # type: ignore
39-
raw_reply = str(item)
40-
raw_message += re.sub(r"(\[CQ\:.+)(?:,url=*)(\])", r"\1\2", raw_reply)
4180
reply_plain = ""
4281
try:
4382
if event.reply and getattr(event.reply, "message", None):
4483
reply_plain = event.reply.message.extract_plain_text()
84+
elif raw_message:
85+
reply_plain = Message(raw_message).extract_plain_text()
4586
except Exception:
4687
pass
4788
n = await delete_dream_messages_from_ban_reply(

src/plugins/ingress_gate/__init__.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from __future__ import annotations
44

55
import asyncio
6+
import re
67

78
from nonebot import get_driver, logger
89
from nonebot.adapters.onebot.v11 import GroupMessageEvent
@@ -80,6 +81,14 @@ def group_at_qq_ids(event: GroupMessageEvent) -> frozenset[int]:
8081
out.add(int(qq))
8182
except (TypeError, ValueError):
8283
continue
84+
if out:
85+
return frozenset(out)
86+
raw_message = event.raw_message or ""
87+
for match in re.finditer(r"\[(?:CQ:)?at(?:,qq=|:qq=)(\d+)", raw_message):
88+
try:
89+
out.add(int(match.group(1)))
90+
except (TypeError, ValueError):
91+
continue
8392
return frozenset(out)
8493

8594

src/plugins/repeater/__init__.py

Lines changed: 57 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from nonebot.adapters.onebot.v11 import GroupMessageEvent, GroupRecallNoticeEvent, Message, MessageSegment, permission
99
from nonebot.exception import ActionFailed
1010
from nonebot.plugin import PluginMetadata
11-
from nonebot.rule import Rule, keyword, to_me
11+
from nonebot.rule import Rule
1212
from nonebot.typing import T_State
1313
from nonebot_plugin_apscheduler import scheduler
1414

@@ -25,6 +25,7 @@
2525
from src.foundation.config import BotConfig
2626
from src.platform.observability import SlowPathTimer, slow_path_threshold_ms
2727
from src.plugins.dream.ban_ack_state import DREAM_BAN_ACK_SENT_STATE_KEY
28+
from src.shared.reply_command_rule import event_has_reply_target, event_targets_self, extract_reply_id_from_raw_message
2829
from src.shared.utils.array2cqcode import try_convert_to_cqcode
2930
from src.shared.utils.media_cache import get_image, insert_image
3031

@@ -260,11 +261,49 @@ async def _(bot: Bot, event: GroupMessageEvent):
260261

261262

262263
async def is_reply(event: GroupMessageEvent) -> bool:
263-
return bool(event.reply)
264+
return event_has_reply_target(event)
265+
266+
267+
async def is_ban_reply_trigger(event: GroupMessageEvent) -> bool:
268+
if "不可以" not in event.get_plaintext():
269+
return False
270+
if not await is_reply(event):
271+
return False
272+
return event_targets_self(event)
273+
274+
275+
def extract_ban_reply_raw_from_message(message: Message | str) -> str:
276+
if isinstance(message, str):
277+
return message
278+
279+
raw_message = ""
280+
for item in message:
281+
raw_reply = str(item)
282+
raw_message += re.sub(r"(\[CQ\:.+)(?:,url=*)(\])", r"\1\2", raw_reply)
283+
if not raw_message.strip():
284+
raw_message = message.extract_plain_text()
285+
return raw_message
286+
287+
288+
async def resolve_ban_reply_raw(bot: Bot, event: GroupMessageEvent) -> str:
289+
if event.reply and getattr(event.reply, "message", None):
290+
return extract_ban_reply_raw_from_message(event.reply.message)
291+
292+
reply_id = extract_reply_id_from_raw_message(event.raw_message)
293+
if reply_id is None:
294+
return ""
295+
296+
try:
297+
msg = await bot.get_msg(message_id=reply_id)
298+
except ActionFailed:
299+
logger.warning(f"bot [{event.self_id}] failed to get replied msg [{reply_id}] in group [{event.group_id}]")
300+
return ""
301+
302+
return extract_ban_reply_raw_from_message(Message(msg["message"]))
264303

265304

266305
ban_msg = on_message(
267-
rule=to_me() & keyword("不可以") & Rule(is_reply),
306+
rule=Rule(is_ban_reply_trigger),
268307
priority=5,
269308
block=True,
270309
permission=group_message_permission_for_command("repeater.ban"),
@@ -273,21 +312,18 @@ async def is_reply(event: GroupMessageEvent) -> bool:
273312

274313
@ban_msg.handle()
275314
async def _(bot: Bot, event: GroupMessageEvent, state: T_State):
276-
if not event.reply:
315+
raw_message = await resolve_ban_reply_raw(bot, event)
316+
if not raw_message.strip():
317+
logger.info(f"bot [{event.self_id}] ban skipped (empty reply target) in group [{event.group_id}]")
277318
return
278319

279-
raw_message = ""
280-
for item in event.reply.message: # type: ignore
281-
raw_reply = str(item)
282-
# 去掉图片消息中的 url, subType 等字段
283-
raw_message += re.sub(r"(\[CQ\:.+)(?:,url=*)(\])", r"\1\2", raw_reply)
284-
285320
logger.info(f"bot [{event.self_id}] ready to ban [{raw_message}] in group [{event.group_id}]")
286321

287-
try:
288-
await bot.delete_msg(message_id=event.reply.message_id) # type: ignore
289-
except ActionFailed:
290-
logger.warning(f"bot [{event.self_id}] failed to delete [{raw_message}] in group [{event.group_id}]")
322+
if event.reply:
323+
try:
324+
await bot.delete_msg(message_id=event.reply.message_id) # type: ignore
325+
except ActionFailed:
326+
logger.warning(f"bot [{event.self_id}] failed to delete [{raw_message}] in group [{event.group_id}]")
291327

292328
banned = await Chat.ban(event.group_id, event.self_id, raw_message, str(event.user_id))
293329
if banned:
@@ -355,8 +391,14 @@ async def message_is_ban(bot: Bot, event: GroupMessageEvent, state: T_State) ->
355391
return event.get_plaintext().strip() == "不可以发这个"
356392

357393

394+
async def is_ban_latest_trigger(bot: Bot, event: GroupMessageEvent, state: T_State) -> bool:
395+
if not await message_is_ban(bot, event, state):
396+
return False
397+
return event_targets_self(event)
398+
399+
358400
ban_msg_latest = on_message(
359-
rule=to_me() & Rule(message_is_ban),
401+
rule=Rule(is_ban_latest_trigger),
360402
priority=5,
361403
block=True,
362404
permission=group_message_permission_for_command("repeater.ban_latest"),

src/plugins/repeater/ban_manager.py

Lines changed: 42 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
import time
33
from collections import defaultdict
44

5+
from nonebot import logger
6+
57
from src.foundation.db import Ban, Context, make_blacklist_repository
68
from src.foundation.db.context_repo_access import context_repo
79

@@ -62,34 +64,57 @@ def iter_ban_bot_ids(group_id: int, bot_id: int, reply_dict: dict) -> list[int]:
6264
bot_ids.append(bid)
6365
return bot_ids
6466

67+
@staticmethod
68+
async def find_ban_reply_fallback(group_id: int, ban_raw_message: str) -> dict | None:
69+
if not ban_raw_message.strip():
70+
return None
71+
find_target = getattr(context_repo, "find_ban_reply_target", None)
72+
if not callable(find_target):
73+
return None
74+
try:
75+
found = await find_target(group_id, ban_raw_message)
76+
except RuntimeError as exc:
77+
logger.debug("repeater ban fallback skipped for group {}: {}", group_id, exc)
78+
return None
79+
if not found:
80+
return None
81+
pre_keywords, reply_keywords = found
82+
return {
83+
"pre_keywords": pre_keywords,
84+
"reply_keywords": reply_keywords,
85+
}
86+
6587
@staticmethod
6688
async def ban(group_id: int, bot_id: int, ban_raw_message: str, reason: str, reply_dict: dict) -> bool:
6789
"""
6890
禁止以后回复这句话,仅对该群有效果
6991
"""
92+
ban_reply = None
7093
for candidate_bot_id in BanManager.iter_ban_bot_ids(group_id, bot_id, reply_dict):
7194
ban_reply = BanManager.find_ban_reply(group_id, candidate_bot_id, ban_raw_message, reply_dict)
72-
if not ban_reply:
73-
continue
74-
75-
pre_keywords = ban_reply["pre_keywords"]
76-
keywords = ban_reply["reply_keywords"]
95+
if ban_reply:
96+
break
97+
if not ban_reply:
98+
ban_reply = await BanManager.find_ban_reply_fallback(group_id, ban_raw_message)
99+
if not ban_reply:
100+
return False
77101

78-
# 通过 append_ban 细粒度 API 原子追加,避免整文档读-改-写。
79-
# 当 Context(keywords=pre_keywords) 不存在时为 no-op(Mongo update_one matched=0)。
80-
ban_reason = Ban(keywords=keywords, group_id=group_id, reason=reason, time=int(time.time()))
81-
await context_repo.append_ban(pre_keywords, ban_reason)
102+
pre_keywords = ban_reply["pre_keywords"]
103+
keywords = ban_reply["reply_keywords"]
82104

83-
if keywords in BanManager._blacklist_answer_reserve[group_id]:
84-
BanManager._blacklist_answer[group_id].add(keywords)
85-
if keywords in BanManager._blacklist_answer_reserve[BanManager.BLACKLIST_FLAG]:
86-
BanManager._blacklist_answer[BanManager.BLACKLIST_FLAG].add(keywords)
87-
else:
88-
BanManager._blacklist_answer_reserve[group_id].add(keywords)
105+
# 通过 append_ban 细粒度 API 原子追加,避免整文档读-改-写。
106+
# 当 Context(keywords=pre_keywords) 不存在时为 no-op(Mongo update_one matched=0)。
107+
ban_reason = Ban(keywords=keywords, group_id=group_id, reason=reason, time=int(time.time()))
108+
await context_repo.append_ban(pre_keywords, ban_reason)
89109

90-
return True
110+
if keywords in BanManager._blacklist_answer_reserve[group_id]:
111+
BanManager._blacklist_answer[group_id].add(keywords)
112+
if keywords in BanManager._blacklist_answer_reserve[BanManager.BLACKLIST_FLAG]:
113+
BanManager._blacklist_answer[BanManager.BLACKLIST_FLAG].add(keywords)
114+
else:
115+
BanManager._blacklist_answer_reserve[group_id].add(keywords)
91116

92-
return False
117+
return True
93118

94119
@staticmethod
95120
async def find_ban_keywords(context: Context | None, group_id) -> set:

0 commit comments

Comments
 (0)