Files
blackroad-operating-system/backend/app/routers/browser.py
2025-11-16 06:41:33 -06:00

415 lines
11 KiB
Python

"""
RoadView Browser API Router
Provides web browsing capabilities:
- URL fetching with proxy
- Bookmark management
- Browsing history
- Search engine integration
"""
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from typing import List, Optional
import httpx
from urllib.parse import urlparse, quote
import hashlib
from ..database import get_db
from ..auth import get_current_user
from ..models import User
from pydantic import BaseModel, HttpUrl
from ..utils import utc_now
router = APIRouter(prefix="/api/browser", tags=["browser"])
class Bookmark(BaseModel):
title: str
url: str
folder: Optional[str] = "Bookmarks"
class HistoryEntry(BaseModel):
url: str
title: str
@router.get("/fetch")
async def fetch_url(
url: str = Query(..., description="URL to fetch"),
current_user: User = Depends(get_current_user)
):
"""
Fetch a web page through proxy
Returns HTML content with modified links for proxy routing
"""
try:
# Validate URL
parsed = urlparse(url)
if not parsed.scheme:
url = f"https://{url}"
async with httpx.AsyncClient(
timeout=30.0,
follow_redirects=True,
headers={
"User-Agent": "Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.112 Safari/537.36"
}
) as client:
response = await client.get(url)
# Get content type
content_type = response.headers.get("content-type", "text/html")
return {
"url": str(response.url),
"status_code": response.status_code,
"content_type": content_type,
"content": response.text if "text" in content_type else None,
"headers": dict(response.headers),
"is_html": "text/html" in content_type
}
except httpx.TimeoutException:
raise HTTPException(status_code=408, detail="Request timeout")
except httpx.RequestError as e:
raise HTTPException(status_code=400, detail=f"Failed to fetch URL: {str(e)}")
except Exception as e:
raise HTTPException(status_code=500, detail=f"Error fetching URL: {str(e)}")
@router.get("/bookmarks")
async def get_bookmarks(
folder: Optional[str] = None,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""Get user's bookmarks"""
# In production, this would be stored in a Bookmarks table
# For now, returning demo bookmarks
demo_bookmarks = [
{
"id": 1,
"title": "GitHub",
"url": "https://github.com",
"folder": "Development",
"created_at": "2024-01-01T00:00:00Z"
},
{
"id": 2,
"title": "Stack Overflow",
"url": "https://stackoverflow.com",
"folder": "Development",
"created_at": "2024-01-02T00:00:00Z"
},
{
"id": 3,
"title": "Hacker News",
"url": "https://news.ycombinator.com",
"folder": "News",
"created_at": "2024-01-03T00:00:00Z"
},
{
"id": 4,
"title": "Hugging Face",
"url": "https://huggingface.co",
"folder": "AI/ML",
"created_at": "2024-01-04T00:00:00Z"
}
]
if folder:
demo_bookmarks = [b for b in demo_bookmarks if b["folder"] == folder]
return {
"bookmarks": demo_bookmarks,
"folders": ["Development", "News", "AI/ML"]
}
@router.post("/bookmarks")
async def add_bookmark(
bookmark: Bookmark,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""Add a new bookmark"""
# In production, save to database
new_bookmark = {
"id": hashlib.md5(bookmark.url.encode()).hexdigest()[:8],
"title": bookmark.title,
"url": bookmark.url,
"folder": bookmark.folder,
"created_at": utc_now().isoformat()
}
return {
"message": "Bookmark added",
"bookmark": new_bookmark
}
@router.delete("/bookmarks/{bookmark_id}")
async def delete_bookmark(
bookmark_id: int,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""Delete a bookmark"""
return {"message": "Bookmark deleted"}
@router.get("/history")
async def get_history(
limit: int = Query(50, ge=1, le=1000),
offset: int = Query(0, ge=0),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""Get browsing history"""
# In production, this would be stored in a BrowserHistory table
demo_history = [
{
"id": 1,
"url": "https://github.com/trending",
"title": "Trending - GitHub",
"visited_at": "2024-01-10T15:30:00Z",
"visit_count": 5
},
{
"id": 2,
"url": "https://stackoverflow.com/questions/tagged/python",
"title": "Newest Python Questions - Stack Overflow",
"visited_at": "2024-01-10T14:20:00Z",
"visit_count": 2
},
{
"id": 3,
"url": "https://news.ycombinator.com",
"title": "Hacker News",
"visited_at": "2024-01-10T13:15:00Z",
"visit_count": 12
}
]
return {
"history": demo_history[offset:offset+limit],
"total": len(demo_history)
}
@router.post("/history")
async def add_history_entry(
entry: HistoryEntry,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""Add a history entry"""
new_entry = {
"id": hashlib.md5(f"{entry.url}{utc_now()}".encode()).hexdigest()[:8],
"url": entry.url,
"title": entry.title,
"visited_at": utc_now().isoformat()
}
return {
"message": "History entry added",
"entry": new_entry
}
@router.delete("/history")
async def clear_history(
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""Clear all browsing history"""
return {"message": "History cleared"}
@router.get("/search")
async def web_search(
q: str = Query(..., min_length=1, description="Search query"),
engine: str = Query("duckduckgo", pattern="^(duckduckgo|google|bing)$"),
current_user: User = Depends(get_current_user)
):
"""
Perform a web search using the specified search engine
Returns redirect URL to search results
"""
search_urls = {
"duckduckgo": f"https://duckduckgo.com/?q={quote(q)}",
"google": f"https://www.google.com/search?q={quote(q)}",
"bing": f"https://www.bing.com/search?q={quote(q)}"
}
return {
"query": q,
"engine": engine,
"url": search_urls[engine]
}
@router.get("/quicklinks")
async def get_quicklinks(
current_user: User = Depends(get_current_user)
):
"""Get quick access links (like a speed dial)"""
return {
"quicklinks": [
{
"title": "GitHub",
"url": "https://github.com",
"icon": "🐙",
"category": "Development"
},
{
"title": "Stack Overflow",
"url": "https://stackoverflow.com",
"icon": "📚",
"category": "Development"
},
{
"title": "Hacker News",
"url": "https://news.ycombinator.com",
"icon": "📰",
"category": "News"
},
{
"title": "Hugging Face",
"url": "https://huggingface.co",
"icon": "🤗",
"category": "AI"
},
{
"title": "Reddit",
"url": "https://reddit.com",
"icon": "🔴",
"category": "Social"
},
{
"title": "YouTube",
"url": "https://youtube.com",
"icon": "▶️",
"category": "Video"
},
{
"title": "Wikipedia",
"url": "https://wikipedia.org",
"icon": "📖",
"category": "Reference"
},
{
"title": "DigitalOcean",
"url": "https://digitalocean.com",
"icon": "🌊",
"category": "Cloud"
}
]
}
@router.get("/settings")
async def get_browser_settings(
current_user: User = Depends(get_current_user)
):
"""Get browser settings"""
return {
"settings": {
"default_search_engine": "duckduckgo",
"homepage": "about:newtab",
"enable_javascript": True,
"enable_cookies": True,
"enable_images": True,
"user_agent": "RoadView/1.0 (BlackRoad OS)"
}
}
@router.put("/settings")
async def update_browser_settings(
settings: dict,
current_user: User = Depends(get_current_user)
):
"""Update browser settings"""
return {
"message": "Settings updated",
"settings": settings
}
@router.get("/download")
async def download_file(
url: str = Query(..., description="File URL to download"),
current_user: User = Depends(get_current_user)
):
"""
Download a file through proxy
Returns file metadata and download token
"""
try:
async with httpx.AsyncClient(timeout=30.0) as client:
# HEAD request to get file info
response = await client.head(url)
content_type = response.headers.get("content-type", "application/octet-stream")
content_length = response.headers.get("content-length", "unknown")
filename = url.split("/")[-1] or "download"
return {
"url": url,
"filename": filename,
"content_type": content_type,
"size": content_length,
"message": "File info retrieved. In production, this would initiate a download."
}
except Exception as e:
raise HTTPException(status_code=400, detail=f"Failed to access file: {str(e)}")
@router.get("/tabs")
async def get_open_tabs(
current_user: User = Depends(get_current_user)
):
"""Get currently open tabs (session management)"""
# In production, store in Redis for session management
return {
"tabs": [
{
"id": 1,
"url": "https://github.com",
"title": "GitHub",
"active": True
}
]
}
@router.post("/tabs")
async def open_new_tab(
url: str,
current_user: User = Depends(get_current_user)
):
"""Open a new tab"""
return {
"tab": {
"id": hashlib.md5(f"{url}{utc_now()}".encode()).hexdigest()[:8],
"url": url,
"title": "Loading...",
"active": True
}
}
@router.delete("/tabs/{tab_id}")
async def close_tab(
tab_id: str,
current_user: User = Depends(get_current_user)
):
"""Close a tab"""
return {"message": "Tab closed"}