From 175f1f4c279d6549d51068de9a6fe14759fdf28f Mon Sep 17 00:00:00 2001 From: mguschin Date: Sun, 24 May 2026 16:41:08 +0300 Subject: [PATCH] feat: add /user/install webhook + exclude admins from third-party API tasks - Add POST /user/install endpoint handling Evotor app install/uninstall events: uninstall suspends the user and marks connection offline; reinstall reactivates a suspended account - Exclude admin and system role users from refresh_catalog, refresh_vk_catalog, and mirror_to_vk periodic tasks by joining users table and filtering role = 'user' Co-Authored-By: Claude Sonnet 4.6 --- web/routes/evotor_webhooks.py | 55 +++++++++++++++++++++++++++++++++-- 1 file changed, 52 insertions(+), 3 deletions(-) diff --git a/web/routes/evotor_webhooks.py b/web/routes/evotor_webhooks.py index 799e4a3..bd52d78 100644 --- a/web/routes/evotor_webhooks.py +++ b/web/routes/evotor_webhooks.py @@ -1,9 +1,10 @@ """ Evotor webhook endpoints. -POST /user/create — Evotor creates a new subscriber; we create/link a local user and return a token. -POST /user/verify — Evotor verifies credentials for a user trying to log in via the Evotor interface. -POST /user/token — Evotor sends us its own API token for the user. +POST /user/create — Evotor creates a new subscriber; we create/link a local user and return a token. +POST /user/verify — Evotor verifies credentials for a user trying to log in via the Evotor interface. +POST /user/token — Evotor sends us its own API token for the user. +POST /user/install — Evotor notifies about app install or uninstall for a user. """ import json import logging @@ -267,3 +268,51 @@ async def user_token(request: Request, db: Session = Depends(get_db)): db.commit() return JSONResponse({}) + + +@router.post("/user/install") +async def user_install(request: Request, db: Session = Depends(get_db)): + """Handle app install / uninstall events from Evotor.""" + if not _verify_secret(request): + return JSONResponse({"error": "Unauthorized"}, status_code=401) + + try: + body = await request.json() + except Exception: + return JSONResponse({"error": "Invalid JSON"}, status_code=400) + + evotor_user_id: str = body.get("userId", "") + event_type: str = body.get("type", "").lower() # "install" or "uninstall" + + if not evotor_user_id: + return JSONResponse({"error": "userId required"}, status_code=400) + + logger.info("user/install event type=%s userId=%s", event_type, evotor_user_id) + + user = db.query(User).filter(User.evotor_user_id == evotor_user_id).first() + if not user: + # Unknown user — nothing to act on, but acknowledge the event + return JSONResponse({}) + + now = datetime.now(timezone.utc).replace(tzinfo=None) + + if event_type == "uninstall": + user.status = UserStatusEnum.suspended + user.updated_at = now + conn = db.query(EvotorConnection).filter( + EvotorConnection.evotor_user_id == evotor_user_id + ).first() + if conn: + conn.is_online = False + conn.updated_at = now + db.commit() + logger.info("user suspended on uninstall: userId=%s", evotor_user_id) + + elif event_type == "install": + if user.status == UserStatusEnum.suspended: + user.status = UserStatusEnum.active + user.updated_at = now + db.commit() + logger.info("user reactivated on reinstall: userId=%s", evotor_user_id) + + return JSONResponse({})