tinyweb/src/tinyweb/handlers/rns.py

177 lines
5.3 KiB
Python

import json
import time
import threading
import traceback
from tinyweb.db import get_db, return_db
from tinyweb.rns_client import fetch_remote_page
from tinyweb.templates import esc
class _PageCache:
def __init__(self, maxsize=50, ttl=300):
self._maxsize = maxsize
self._ttl = ttl
self._cache = {}
self._lock = threading.Lock()
def get(self, key):
with self._lock:
entry = self._cache.get(key)
if entry is None:
return None
if time.time() - entry["time"] > self._ttl:
del self._cache[key]
return None
self._cache.pop(key)
self._cache[key] = entry
return entry["value"]
def put(self, key, value):
with self._lock:
if key in self._cache:
self._cache.pop(key)
elif len(self._cache) >= self._maxsize:
oldest = next(iter(self._cache))
del self._cache[oldest]
self._cache[key] = {"value": value, "time": time.time()}
_page_cache = _PageCache()
def _inject_base_tag(html, dest_hash):
base = f'<base href="/rns/{dest_hash}/">'
head_start = html.find("<head")
if head_start >= 0:
close = html.find(">", head_start)
if close >= 0:
return html[:close + 1] + base + html[close + 1:]
return f"<head>{base}</head>{html}"
def _get_mesh_sites():
db = get_db()
try:
return db.execute("SELECT hash, name, added_at FROM mesh_sites ORDER BY added_at DESC").fetchall()
finally:
return_db(db)
def handle_rns_add_hash(dest_hash, name=""):
db = get_db()
errors = []
try:
db.execute(
"INSERT OR REPLACE INTO mesh_sites (hash, name) VALUES (?, ?)",
(dest_hash, name or ""),
)
try:
resp = fetch_remote_page(dest_hash, "/")
if resp.get("status") == 200:
body = resp.get("body", "")
title = name or dest_hash[:16]
import re
m = re.search(r"<title[^>]*>(.*?)</title>", body, re.IGNORECASE | re.DOTALL)
if m:
title = m.group(1).strip()
desc = ""
m = re.search(r'<meta\s+name="description"\s+content="([^"]*)"', body, re.IGNORECASE)
if m:
desc = m.group(1).strip()
if not desc:
text = re.sub(r"<[^>]+>", " ", body)
text = re.sub(r"\s+", " ", text).strip()
desc = text[:200].strip()
url = f"rns:{dest_hash}"
import datetime
now = datetime.datetime.now().strftime("%Y-%m-%dT%H:%M:%S")
db.execute(
"INSERT OR REPLACE INTO pages (url, title, body, last_modified, summary) VALUES (?, ?, ?, ?, ?)",
(url, title, body, now, desc),
)
else:
errors.append(f"Remote returned status {resp.get('status')}")
except Exception as e:
errors.append(str(e))
traceback.print_exc()
db.commit()
finally:
return_db(db)
return errors if errors else None
def handle_rns_delete_hash(body):
dest_hash = body.get("hash", [""])[0].strip() if isinstance(body, dict) else body
db = get_db()
try:
db.execute("DELETE FROM mesh_sites WHERE hash = ?", (dest_hash,))
db.commit()
finally:
return_db(db)
from tinyweb.handlers.pages import _redirect
return _redirect("/")
def handle_rns_browse(path, dest_hash):
prefix = f"/rns/{dest_hash}"
sub_path = path[len(prefix):] if path.startswith(prefix) else "/"
if not sub_path:
sub_path = "/"
cache_key = (dest_hash, sub_path)
cached = _page_cache.get(cache_key)
if cached is not None:
return {
"status": 200,
"content_type": "text/html; charset=utf-8",
"body": cached,
"headers": {},
}
try:
resp = fetch_remote_page(dest_hash, sub_path)
except ConnectionError as e:
return {
"status": 200,
"content_type": "text/html; charset=utf-8",
"body": f"<h1>could not connect</h1><p>{esc(str(e))}</p>",
"headers": {},
}
except PermissionError:
return {
"status": 200,
"content_type": "text/html; charset=utf-8",
"body": "<h1>forbidden</h1><p>the remote instance blocked this request.</p>",
"headers": {},
}
if resp.get("status") != 200:
return {
"status": 200,
"content_type": "text/html; charset=utf-8",
"body": f"<h1>error</h1><p>remote returned status {resp['status']}</p>",
"headers": {},
}
body = resp.get("body", "")
if resp.get("content_type", "").startswith("application/json"):
try:
data = json.loads(body)
body = f"<pre>{esc(json.dumps(data, indent=2))}</pre>"
except (json.JSONDecodeError, TypeError):
body = f"<pre>{esc(body[:2000])}</pre>"
else:
body = _inject_base_tag(body, dest_hash)
_page_cache.put(cache_key, body)
return {
"status": 200,
"content_type": "text/html; charset=utf-8",
"body": body,
"headers": {},
}