import base64 import io import random import secrets import string import time from pathlib import Path from PIL import Image, ImageDraw, ImageFont CAPTCHA_TTL_SECONDS = 300 CAPTCHA_WIDTH = 148 CAPTCHA_HEIGHT = 48 CAPTCHA_FONT_SIZE = 30 CAPTCHA_FONT_PATHS = ( Path("/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf"), Path("/System/Library/Fonts/SFNS.ttf"), Path("/Library/Fonts/Arial Bold.ttf"), ) _captchas: dict[str, tuple[str, float]] = {} def make_captcha() -> dict[str, str]: clean_expired_captchas() captcha_id = secrets.token_urlsafe(16) code = "".join(random.choice(string.ascii_uppercase + string.digits) for _ in range(4)) _captchas[captcha_id] = (code, time.time() + CAPTCHA_TTL_SECONDS) return {"captcha_id": captcha_id, "image": draw_captcha_image(code)} def verify_captcha(captcha_id: str, captcha_code: str) -> bool: clean_expired_captchas() record = _captchas.pop(captcha_id, None) if not record: return False expected, expires_at = record if expires_at < time.time(): return False return expected.upper() == captcha_code.strip().upper() def clean_expired_captchas() -> None: now = time.time() expired = [captcha_id for captcha_id, (_, expires_at) in _captchas.items() if expires_at < now] for captcha_id in expired: _captchas.pop(captcha_id, None) def draw_captcha_image(code: str) -> str: image = Image.new("RGB", (CAPTCHA_WIDTH, CAPTCHA_HEIGHT), "#f8fafc") draw = ImageDraw.Draw(image) font = load_captcha_font() for i, char in enumerate(code): bounds = draw.textbbox((0, 0), char, font=font) text_height = bounds[3] - bounds[1] y = (CAPTCHA_HEIGHT - text_height) // 2 - bounds[1] + random.randint(-2, 2) draw.text((12 + i * 33, y), char, fill="#111827", font=font) for _ in range(8): x1, y1 = random.randint(0, CAPTCHA_WIDTH), random.randint(0, CAPTCHA_HEIGHT) x2, y2 = random.randint(0, CAPTCHA_WIDTH), random.randint(0, CAPTCHA_HEIGHT) draw.line((x1, y1, x2, y2), fill="#cbd5e1", width=1) buffer = io.BytesIO() image.save(buffer, format="PNG") return "data:image/png;base64," + base64.b64encode(buffer.getvalue()).decode("ascii") def load_captcha_font() -> ImageFont.FreeTypeFont | ImageFont.ImageFont: for path in CAPTCHA_FONT_PATHS: if path.exists(): return ImageFont.truetype(str(path), CAPTCHA_FONT_SIZE) try: return ImageFont.load_default(size=CAPTCHA_FONT_SIZE) except TypeError: return ImageFont.load_default()