初始提交:QQ 群入群验证机器人

基于 qqbotsdk-py 的入群验证:进群发按钮验证码、完成前禁言、
超时踢出(11253 时兜底长期禁言)、乱点他人按钮处罚、状态持久化
与重启恢复。38 个单元/集成测试。
This commit is contained in:
snowy-yang committed 2026-10-04 18:44:08 +08:00
commit 755013b9db
17 files changed
+1864

No files matched your search

+102
View File
@@ -0,0 +1,102 @@
"""数学选择题验证码:题目生成、选项按钮组装与按钮数据编解码。
按钮 data 格式为 ``v|<token>|<选项值>``:token 标识"当前这道题",
答错换题后旧消息上的按钮因 token 不匹配而失效,防止回放旧题。
"""
from __future__ import annotations
import random
from dataclasses import dataclass
from uuid import uuid4
from qqbotsdk.api import button, keyboard
_BUTTON_PREFIX = "v"
@dataclass(slots=True)
class Captcha:
"""一道验证码;a/op/b 保留运算数便于测试与排查。"""
question: str
answer: int
options: list[int]
token: str
a: int
op: str
b: int
def body(self) -> str:
"""题面正文(不含欢迎/提示头),换题重发时复用。"""
return f"### {self.a} {self.op} {self.b} = ?"
def keyboard(self) -> dict:
"""选项按钮键盘,每行 2 个。"""
rows = [
[button(str(v), encode_button_data(self.token, v)) for v in self.options[i : i + 2]]
for i in range(0, len(self.options), 2)
]
return keyboard(*rows)
def encode_button_data(token: str, value: int) -> str:
return f"{_BUTTON_PREFIX}|{token}|{value}"
def parse_button_data(data: str) -> tuple[str, int] | None:
"""解析按钮 data 为 (token, 选项值);非本机器人按钮返回 None。"""
parts = data.split("|")
if len(parts) != 3 or parts[0] != _BUTTON_PREFIX:
return None
try:
return parts[1], int(parts[2])
except ValueError:
return None
def welcome_markdown(cap: Captcha, timeout_minutes: int, subject: str = "新同学") -> str:
"""进群首题/测试指令消息(markdown + 按钮一起发);subject 为称呼前缀。"""
return (
"**入群验证**\n"
f"{subject}请在 **{timeout_minutes} 分钟内** 点击下方正确答案完成验证,"
"超时未通过将被移出本群。\n\n" + cap.body()
)
def retry_markdown(cap: Captcha) -> str:
"""答错后换题重发的消息。"""
return "答错了,换一题再试试:\n\n" + cap.body()
def generate(option_count: int = 4) -> Captcha:
"""随机生成一道加减/乘法选择题;干扰项取答案邻域且互不重复。"""
op = random.choice(("+", "-", "×"))
match op:
case "+":
a, b = random.randint(1, 20), random.randint(1, 20)
case "-":
a = random.randint(5, 30)
b = random.randint(1, a) # 保证差非负
case _:
a, b = random.randint(2, 9), random.randint(2, 9)
answer = a + b if op == "+" else a - b if op == "-" else a * b
lo = max(0, answer - 12)
distractors: set[int] = set()
while len(distractors) < option_count - 1:
cand = random.randint(lo, answer + 12)
if cand != answer:
distractors.add(cand)
options = [answer, *distractors]
random.shuffle(options)
return Captcha(
question=f"{a} {op} {b}",
answer=answer,
options=options,
token=uuid4().hex[:12],
a=a,
op=op,
b=b,
)