import hashlib import struct import base64 import uuid from typing import Optional from fastapi import APIRouter, Depends, HTTPException, Query, Request from pydantic import BaseModel from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select from app.config import settings from app.database import get_db from app.middleware.auth import get_current_user, require_director from app.models.user import User from app.services.wecom import wecom_client from app.services.scheduler import check_daily_reporting router = APIRouter(prefix="/wecom", tags=["WeChatWork"]) class RemindRequest(BaseModel): user_ids: list[str] message: Optional[str] = None class AnnouncementRequest(BaseModel): content: str # ── Callback verification helpers ── def _verify_signature(token: str, timestamp: str, nonce: str, encrypted: str, signature: str) -> bool: """Verify WeChat Work callback signature.""" params = sorted([token, timestamp, nonce, encrypted]) raw = "".join(params) computed = hashlib.sha1(raw.encode()).hexdigest() return computed == signature def _decrypt_echostr(encrypted: str) -> str: """Decrypt WeChat Work callback echostr. Returns plaintext.""" key = base64.b64decode(settings.WECOM_ENCODING_AES_KEY + "=") ciphertext = base64.b64decode(encrypted) # AES-256-CBC decrypt from Crypto.Cipher import AES cipher = AES.new(key, AES.MODE_CBC, iv=key[:16]) plaintext = cipher.decrypt(ciphertext) # Strip PKCS#7 padding pad_len = plaintext[-1] plaintext = plaintext[:-pad_len] # Format: random(16) + msg_len(4) + msg + corpid msg_len = struct.unpack(">I", plaintext[16:20])[0] msg = plaintext[20:20 + msg_len].decode("utf-8") return msg @router.get("/callback") async def wecom_callback_verify( msg_signature: str = Query(...), timestamp: str = Query(...), nonce: str = Query(...), echostr: str = Query(...), ): """WeChat Work callback URL verification (GET).""" token = settings.WECOM_TOKEN if not token or not settings.WECOM_ENCODING_AES_KEY: raise HTTPException(status_code=500, detail="WeCom callback not configured") if not _verify_signature(token, timestamp, nonce, echostr, msg_signature): raise HTTPException(status_code=403, detail="Signature verification failed") try: plaintext = _decrypt_echostr(echostr) return plaintext except Exception as e: raise HTTPException(status_code=500, detail=f"Decrypt failed: {e}") @router.post("/callback") async def wecom_callback_event(request: Request): """WeChat Work callback event receiver (POST). Placeholder for future use.""" body = await request.body() # TODO: decrypt and handle events (app install, user enter, etc.) return "ok" # ── Manual actions ── @router.post("/remind") async def send_reminder( data: RemindRequest, current_user: dict = Depends(require_director), db: AsyncSession = Depends(get_db), ): """Director manually sends reminder to specific managers.""" result = await db.execute( select(User.wecom_userid).where(User.id.in_([uuid.UUID(uid) for uid in data.user_ids])) ) wecom_ids = [r[0] for r in result.all() if r[0]] content = data.message or "📋 请及时完成今日拜访记录填报。" success = await wecom_client.send_text_message(wecom_ids, content) return {"success": success, "sent_to": len(wecom_ids)} @router.post("/announcement") async def send_announcement( data: AnnouncementRequest, current_user: dict = Depends(require_director), db: AsyncSession = Depends(get_db), ): """Director sends an announcement to all team members.""" result = await db.execute(select(User.wecom_userid).where(User.wecom_userid.isnot(None))) wecom_ids = [r[0] for r in result.all()] content = f"📢 支局长公告\n\n{data.content}" success = await wecom_client.send_text_message(wecom_ids, content) return {"success": success, "sent_to": len(wecom_ids)} @router.post("/trigger-daily-check") async def trigger_daily_check( current_user: dict = Depends(require_director), db: AsyncSession = Depends(get_db), ): """Manually trigger the daily reporting check (for testing or manual use).""" result = await check_daily_reporting(db) return result