失效按钮乱点同样计数处罚;不再静默

This commit is contained in:
snowy-yang committed 2026-10-05 19:46:30 +08:00
1 parent 37e44a512d
commit 1db8f1cc0e
3 files changed
+48 -12

No files matched your search

+3 -2
View File
@@ -12,8 +12,9 @@
- 验证前禁言:按 `member_openid` 单独禁言(不碰全员禁言),通过后立即解禁
- 超时踢出:默认 10 分钟;踢人失败(业务码 11253)自动兜底为禁言 72 小时 + 群内提示管理员
- 旧题防重放:按钮 data 携带题目 token,换题后旧按钮点击无效
- 反捣乱:非被试者点击他人的验证按钮,30 分钟窗口内累计 3 次即禁言 5 分钟
(次数与时长可配:`FOREIGN_CLICK_LIMIT` / `FOREIGN_CLICK_MUTE_MINUTES`)
- 反捣乱:点击**非本人的验证按钮**(他人的当前题或已失效的残留按钮),30 分钟窗口内
累计 3 次即禁言 5 分钟,警告会 @ 点击者(次数与时长可配:`FOREIGN_CLICK_LIMIT` /
`FOREIGN_CLICK_MUTE_MINUTES`)
- 状态持久化:`data/` 下 JSON 存储,进程重启自动恢复禁言与倒计时(已过期立即处理)
- 轻量管理指令:群里 `@机器人 验证状态` 查看待验证名单与剩余时间;
`@机器人 验证我`(或 `测试验证`)对发起人本人走一遍完整验证流程,方便上线前自测
+7 -5
View File
@@ -5,7 +5,8 @@
发送失败(通常是 markdown/按钮权限未开通)则不禁言,避免把成员
永久锁死,同时记日志提示排查方向;
- INTERACTION_CREATE → 回应互动 → 校验点击者/题目 → 答对解禁+记录,
答错换题(倒计时不重置),非本人或旧题点击礼貌忽略;
答错换题(倒计时不重置);非本人点击(他人的当前题或已失效的残留
按钮)一律计入乱点处罚,达到次数禁言;
- 超时 → 尝试移出群(官方"内邀"能力,未开通时返回 11253)→ 失败则
延长禁言兜底并通知管理员;
- GROUP_MEMBER_REMOVE → 清理状态;@机器人 可查"验证状态"。
@@ -379,11 +380,12 @@ def setup(bot: Bot, cfg: AuthConfig) -> VerifyState:
await _reply_text(api, inter.group_openid, "你已经通过验证啦,不用再答题~",
event_id=event.event_id)
else:
# 旧题/已失效按钮,礼貌忽略不计处罚
logger.info(f"点击已失效按钮,礼貌回复 (…{clicker[-6:]})")
await _reply_text(api, inter.group_openid, "这道题不是给你的~",
# 失效/他人残留按钮的乱点同样计数处罚(不静默、不放过)
logger.info(f"点击已失效按钮,计入乱点 (…{clicker[-6:]})")
await _handle_foreign_click(
api, cfg, foreign_clicks, inter.group_openid, clicker,
event_id=event.event_id,
mention=clicker)
)
return
if not is_subject:
logger.info(f"被试者点击自己的旧题,提示换题 (…{clicker[-6:]})")
+38 -5
View File
@@ -160,7 +160,9 @@ def test_foreign_clicker_muted_on_third_click(tmp_path, monkeypatch):
asyncio.run(scenario())
def test_stale_button_click_not_punished(tmp_path, monkeypatch):
def test_stale_button_clicks_counted_as_foreign(tmp_path, monkeypatch):
"""点已失效的残留按钮同样计入乱点处罚:3 次后禁言。"""
async def scenario():
monkeypatch.setenv("DATA_DIR", str(tmp_path))
bot = make_bot()
@@ -169,13 +171,44 @@ def test_stale_button_click_not_punished(tmp_path, monkeypatch):
fake = FakeApi()
bot.services[BotApi] = fake
# token 不属于任何在途题目(旧题残留按钮)
# token 不属于任何在途题目(验证通过后残留的按钮)
for seq in range(1, 4):
await bot._emitter.emit(interaction_payload("ghost", 1, "MEMBER_C", seq=seq))
await asyncio.sleep(0.03)
assert fake.raw_posts == [] # 三次都不触发禁言
assert all("不是给你的" in (m["markdown"] or "") for m in fake.group_messages)
assert all("<@MEMBER_C>" in (m["markdown"] or "") for m in fake.group_messages)
# 前两次带计数的警告(@点击者),第三次禁言
warns = [(m["markdown"] or "") for m in fake.group_messages[:2]]
assert all("不是给你的" in md and "<@MEMBER_C>" in md for md in warns)
assert "再乱点" in warns[0] and "再乱点 1 次" in warns[1]
mutes = [j for p, j in fake.raw_posts if p.endswith("/restrict_chat_setting")]
assert len(mutes) == 1
entry = mutes[0]["members"][0]
assert entry["op"] == "add"
assert entry["member_openid"] == "MEMBER_C"
asyncio.run(scenario())
def test_verified_member_clicking_stale_button_not_punished(tmp_path, monkeypatch):
"""刚通过验证的成员点自己残留的旧按钮:礼貌回复,不计处罚。"""
async def scenario():
import time as _time
monkeypatch.setenv("DATA_DIR", str(tmp_path))
monkeypatch.setenv("VERIFIED_TRUST_DAYS", "3") # 开启信任期,已通过成员才免计
bot = make_bot()
cfg = AuthConfig.load()
state = setup(bot, cfg)
fake = FakeApi()
bot.services[BotApi] = fake
key = pending_key("GROUP1", "MEMBER_C")
state.mark_verified(key) # trust 窗口内
await bot._emitter.emit(interaction_payload("ghost", 1, "MEMBER_C", seq=1))
await asyncio.sleep(0.05)
assert fake.raw_posts == [] # 不触发禁言
assert "已经通过验证" in (fake.group_messages[-1]["content"] or "")
asyncio.run(scenario())