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