125 lines
3.8 KiB
Python
125 lines
3.8 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""HTTP-only Chrome extension installation and status routes."""
|
|
# pylint: disable=relative-beyond-top-level
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import secrets
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
from fastapi import APIRouter, HTTPException
|
|
from pydantic import BaseModel, ConfigDict
|
|
|
|
from ..extension_setup import (
|
|
atomic_write_json_0600,
|
|
extension_install_status,
|
|
BridgeEndpointUnavailable,
|
|
open_chrome_extensions_page,
|
|
open_extension_folder,
|
|
require_bridge_endpoint,
|
|
setup_extension_files,
|
|
)
|
|
from ..transport.state import get_nm_bridge_route_state
|
|
|
|
api_router = APIRouter(tags=["chrome"])
|
|
router = api_router
|
|
DEFAULT_CONFIG_PATH = Path.home() / ".qwenpaw" / "nm-bridge.json"
|
|
_state = get_nm_bridge_route_state()
|
|
|
|
|
|
class ExtensionSetupRequest(BaseModel):
|
|
"""Request for installing or repairing the local Chrome extension."""
|
|
|
|
model_config = ConfigDict(extra="forbid")
|
|
install_mode: str = "unpacked"
|
|
reset: bool = False
|
|
|
|
|
|
def _read_existing_token(config_path: Path) -> str | None:
|
|
"""Return the installed core bridge token, if one is available."""
|
|
if not config_path.exists():
|
|
return None
|
|
try:
|
|
config = json.loads(config_path.read_text(encoding="utf-8"))
|
|
except (OSError, json.JSONDecodeError):
|
|
return None
|
|
token = str(config.get("token") or "").strip()
|
|
return token or None
|
|
|
|
|
|
def _write_private_bridge_config(
|
|
config_path: Path,
|
|
*,
|
|
token: str,
|
|
ws_url: str,
|
|
) -> None:
|
|
"""Atomically update the configuration shared with the core WS handler."""
|
|
atomic_write_json_0600(config_path, {"ws_url": ws_url, "token": token})
|
|
|
|
|
|
def configure_nm_bridge(
|
|
*,
|
|
token: str | None = None,
|
|
config_path: Path = DEFAULT_CONFIG_PATH,
|
|
) -> str:
|
|
"""Write the Native Messaging host's single backend configuration."""
|
|
ws_url = require_bridge_endpoint()
|
|
path = Path(config_path)
|
|
if token is not None and not token.strip():
|
|
raise ValueError("Native Messaging bridge token must not be empty")
|
|
token = token or _read_existing_token(path) or secrets.token_urlsafe(32)
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
_write_private_bridge_config(path, token=token, ws_url=ws_url)
|
|
_state.token = token
|
|
_state.ws_url = ws_url
|
|
_state.config_path = path
|
|
return token
|
|
|
|
|
|
async def get_extension_status() -> dict[str, Any]:
|
|
"""Return install state plus the core endpoint the host will contact."""
|
|
status = extension_install_status()
|
|
bridge_endpoint = require_bridge_endpoint()
|
|
configured_endpoint = status.pop("ws_url", None)
|
|
return {
|
|
**status,
|
|
"bridge_endpoint": bridge_endpoint,
|
|
"configured_endpoint": configured_endpoint,
|
|
"bridge_endpoint_stale": configured_endpoint != bridge_endpoint,
|
|
}
|
|
|
|
|
|
@api_router.get("/install-status")
|
|
async def extension_status() -> dict[str, Any]:
|
|
"""Return Chrome extension installation status."""
|
|
return await get_extension_status()
|
|
|
|
|
|
@api_router.post("/setup")
|
|
async def extension_setup(
|
|
request: ExtensionSetupRequest,
|
|
) -> dict[str, Any]:
|
|
"""Install or repair the extension and its Native Messaging host."""
|
|
try:
|
|
result = setup_extension_files(
|
|
install_mode=request.install_mode,
|
|
reset=request.reset,
|
|
)
|
|
except BridgeEndpointUnavailable as exc:
|
|
raise HTTPException(status_code=409, detail=str(exc)) from exc
|
|
return {**result, **await get_extension_status()}
|
|
|
|
|
|
@api_router.post("/open-chrome-extensions")
|
|
async def open_chrome_extensions() -> dict[str, str | bool]:
|
|
"""Open Chrome's extensions management page."""
|
|
return open_chrome_extensions_page()
|
|
|
|
|
|
@api_router.post("/open-extension-folder")
|
|
async def open_local_extension_folder() -> dict[str, str | bool | None]:
|
|
"""Open the unpacked extension directory."""
|
|
return open_extension_folder()
|