Files
snowy-yang 755013b9db 初始提交:QQ 群入群验证机器人
基于 qqbotsdk-py 的入群验证:进群发按钮验证码、完成前禁言、
超时踢出(11253 时兜底长期禁言)、乱点他人按钮处罚、状态持久化
与重启恢复。38 个单元/集成测试。
2026-10-04 18:44:08 +08:00

103 lines
3.1 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""数学选择题验证码:题目生成、选项按钮组装与按钮数据编解码。
按钮 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,
)