初始提交:QQ 群入群验证机器人
基于 qqbotsdk-py 的入群验证:进群发按钮验证码、完成前禁言、 超时踢出(11253 时兜底长期禁言)、乱点他人按钮处罚、状态持久化 与重启恢复。38 个单元/集成测试。
This commit is contained in:
commit
755013b9db
17 files changed
+1864
No files matched your search
@@ -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,
|
||||
)
|
||||
Reference in new issue
Block a user