Store and resolve manual captcha tickets.
| 23 | |
| 24 | |
| 25 | class CaptchaService: |
| 26 | """Store and resolve manual captcha tickets.""" |
| 27 | |
| 28 | def store_manual_ticket( |
| 29 | self, |
| 30 | session: AccountSessionState, |
| 31 | request: ManualCaptchaRequest, |
| 32 | ) -> AccountSessionState: |
| 33 | if request.ret not in (None, 0): |
| 34 | raise BadRequestError( |
| 35 | "验证码回调 ret 不是 0,这票据八成是废的,别拿来糊弄上游。", |
| 36 | details={"ret": request.ret}, |
| 37 | ) |
| 38 | ticket = request.ticket.strip() |
| 39 | randstr = request.randstr.strip() |
| 40 | if not ticket or not randstr: |
| 41 | raise BadRequestError("ticket 和 randstr 不能为空") |
| 42 | session.captcha_ticket = ticket |
| 43 | session.captcha_randstr = randstr |
| 44 | session.captcha_updated_at = utc_now_iso() |
| 45 | return session |
| 46 | |
| 47 | def store_challenge_snapshot( |
| 48 | self, |
| 49 | session: AccountSessionState, |
| 50 | *, |
| 51 | sess: str, |
| 52 | sid: str, |
| 53 | instruction: str, |
| 54 | raw: dict[str, Any], |
| 55 | ocr: dict[str, Any] | None = None, |
| 56 | ) -> AccountSessionState: |
| 57 | session.captcha_challenge_sess = sess.strip() |
| 58 | session.captcha_challenge_sid = sid.strip() |
| 59 | session.captcha_challenge_instruction = instruction.strip() |
| 60 | session.captcha_challenge_raw = raw |
| 61 | session.captcha_challenge_ocr = ocr or {} |
| 62 | session.captcha_challenge_updated_at = utc_now_iso() |
| 63 | return session |
| 64 | |
| 65 | def resolve_preview_captcha( |
| 66 | self, |
| 67 | session: AccountSessionState, |
| 68 | request: PreviewPaymentRequest, |
| 69 | ) -> tuple[str, str]: |
| 70 | ticket = (request.ticket or "").strip() or session.captcha_ticket |
| 71 | randstr = (request.randstr or "").strip() or session.captcha_randstr |
| 72 | if not ticket or not randstr: |
| 73 | raise BadRequestError("支付预览缺少验证码票据,请先提交 ticket / randstr") |
| 74 | return ticket, randstr |
| 75 | |
| 76 | def build_click_answer(self, points: list[CaptchaPoint | dict[str, Any]]) -> list[dict[str, Any]]: |
| 77 | if not points: |
| 78 | raise BadRequestError("缺少点击点位,没法组装腾讯 verify 的 ans") |
| 79 | |
| 80 | normalized: list[tuple[int, int, int]] = [] |
| 81 | for index, item in enumerate(points, start=1): |
| 82 | point = item if isinstance(item, CaptchaPoint) else CaptchaPoint.model_validate(item) |
no outgoing calls
no test coverage detected