interactions
Build and Push Docker Images / build-and-push (push) Successful in 24s

This commit is contained in:
2026-03-30 10:23:18 +02:00
parent df44afeaff
commit af3c06ec29
2 changed files with 107 additions and 14 deletions
+28 -14
View File
@@ -1,13 +1,13 @@
from fastapi import APIRouter, HTTPException, Header, Response from fastapi import APIRouter, HTTPException, Header, Response
from typing import Optional from typing import Optional, List, Literal, Union, Annotated
from urllib.parse import unquote from urllib.parse import unquote
from app.config import API_KEY from app.config import API_KEY
from app.database import get_cached_data, save_to_cache from app.database import get_cached_data, save_to_cache
from pydantic import BaseModel from pydantic import BaseModel, Field
from app.services.browser import ( from app.services.browser import (
visit_url_service, visit_url_service,
screenshot_url_service, screenshot_url_service,
click_selector_service, interactions_service,
extract_seo_service, extract_seo_service,
extract_meta_tags_service, extract_meta_tags_service,
capture_outgoing_calls_service, capture_outgoing_calls_service,
@@ -16,11 +16,27 @@ from app.services.browser import (
router = APIRouter() router = APIRouter()
class ClickRequest(BaseModel): class ClickInteraction(BaseModel):
url: str action: Literal["click"]
selector: str selector: str
# Default behavior: return screenshot after clicking.
returnType: Optional[str] = "screenshot"
class TypeInteraction(BaseModel):
action: Literal["type"]
selector: str
text: str
Interaction = Annotated[
Union[ClickInteraction, TypeInteraction],
Field(discriminator="action"),
]
class InteractionsRequest(BaseModel):
url: str
returnType: Optional[Literal["screenshot", "html"]] = "screenshot"
interactions: List[Interaction]
@router.get("/") @router.get("/")
async def visit_url(url: str, skipCache: bool = False, x_api_key: Optional[str] = Header(None)): async def visit_url(url: str, skipCache: bool = False, x_api_key: Optional[str] = Header(None)):
@@ -74,21 +90,19 @@ async def screenshot_url(url: str, fullPage: bool = True, x_api_key: Optional[st
print(f"Error taking screenshot for URL {decoded_url}: {e}") print(f"Error taking screenshot for URL {decoded_url}: {e}")
raise HTTPException(status_code=500, detail=str(e)) raise HTTPException(status_code=500, detail=str(e))
@router.post("/click") @router.post("/interactions")
async def click_selector(payload: ClickRequest, x_api_key: Optional[str] = Header(None)): async def run_interactions(payload: InteractionsRequest, x_api_key: Optional[str] = Header(None)):
if not x_api_key or x_api_key != API_KEY: if not x_api_key or x_api_key != API_KEY:
raise HTTPException(status_code=401, detail="Invalid API key") raise HTTPException(status_code=401, detail="Invalid API key")
decoded_url = unquote(payload.url) decoded_url = unquote(payload.url)
return_type = (payload.returnType or "screenshot").lower() return_type = (payload.returnType or "screenshot").lower()
if return_type not in ("screenshot", "html"):
raise HTTPException(status_code=400, detail="Invalid returnType. Use 'screenshot' or 'html'.")
try: try:
result = await click_selector_service(decoded_url, payload.selector, return_type) result = await interactions_service(decoded_url, payload.interactions, return_type)
if isinstance(result, dict) and result.get("status") == "error": if isinstance(result, dict) and result.get("status") == "error":
raise HTTPException(status_code=500, detail=result.get("error", "Click failed")) raise HTTPException(status_code=500, detail=result.get("error", "Interaction failed"))
if return_type == "html": if return_type == "html":
if not isinstance(result, str): if not isinstance(result, str):
@@ -101,7 +115,7 @@ async def click_selector(payload: ClickRequest, x_api_key: Optional[str] = Heade
except HTTPException: except HTTPException:
raise raise
except Exception as e: except Exception as e:
print(f"Error clicking selector on URL {decoded_url}: {e}") print(f"Error running interactions on URL {decoded_url}: {e}")
raise HTTPException(status_code=500, detail=str(e)) raise HTTPException(status_code=500, detail=str(e))
@router.get("/seo") @router.get("/seo")
@@ -175,6 +175,85 @@ async def click_selector_service(decoded_url: str, selector: str, return_type: s
return await safe_browser_operation(decoded_url, click_operation) return await safe_browser_operation(decoded_url, click_operation)
async def interactions_service(decoded_url: str, interactions, return_type: str):
"""Open `decoded_url`, execute click/type interactions in order, then return screenshot or HTML."""
print(f"Running interactions on {decoded_url} (return_type={return_type})")
async def interactions_operation(page):
try:
# Navigate and wait for initial DOM.
await page.goto(decoded_url, wait_until="domcontentloaded", timeout=30000)
await page.wait_for_load_state("load", timeout=30000)
# Ensure the page is fully loaded (helps with JS-driven UIs).
await page.wait_for_function("document.readyState === 'complete'", timeout=30000)
# Best-effort cookie/banner handling (helps avoid click interception).
cookie_buttons = [
"Accept all",
"Accepteer",
"Accepteren",
"Accept",
]
for button in cookie_buttons:
try:
element = await page.query_selector(f'text="{button}"')
if element:
await element.click()
await page.wait_for_timeout(1000)
break
except Exception:
pass
for interaction in interactions:
# Support both Pydantic model instances and raw dicts.
if isinstance(interaction, dict):
action = interaction.get("action")
selector = interaction.get("selector")
text = interaction.get("text")
else:
action = getattr(interaction, "action", None)
selector = getattr(interaction, "selector", None)
text = getattr(interaction, "text", None)
if not action or not selector:
raise ValueError("Interaction must contain action and selector")
if action == "click":
await page.wait_for_selector(selector, timeout=30000)
locator = page.locator(selector)
await locator.scroll_into_view_if_needed(timeout=30000)
await locator.click(force=True, timeout=30000)
elif action == "type":
if text is None:
raise ValueError("Type interaction must contain text")
await page.wait_for_selector(selector, timeout=30000)
locator = page.locator(selector)
await locator.scroll_into_view_if_needed(timeout=30000)
await locator.fill(text)
else:
raise ValueError(f"Unknown interaction action: {action}")
# Let the UI settle after each action.
await page.wait_for_timeout(1000)
try:
await page.wait_for_load_state("networkidle", timeout=10000)
except Exception:
pass
if return_type == "html":
return await page.content()
return await page.screenshot(full_page=True, type="png")
except Exception as e:
print(f"Error during interactions capture: {e}")
return {"status": "error", "url": decoded_url, "error": str(e)}
return await safe_browser_operation(decoded_url, interactions_operation)
async def extract_seo_service(decoded_url): async def extract_seo_service(decoded_url):
"""Service function to extract SEO information from a website""" """Service function to extract SEO information from a website"""
print(f"Extracting SEO from: {decoded_url}") print(f"Extracting SEO from: {decoded_url}")