177 lines
5.3 KiB
Python
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": {},
|
|
}
|