page view endpoint, standalone site server, RNS-aware SPA nav
This commit is contained in:
parent
0c83db7a17
commit
d0b27b4c61
6 changed files with 265 additions and 135 deletions
104
site_server.py
Normal file
104
site_server.py
Normal file
|
|
@ -0,0 +1,104 @@
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
import mimetypes
|
||||||
|
|
||||||
|
SITE_DIR = os.path.expanduser("~/apps/tinyweb-site")
|
||||||
|
DATA_DIR = os.environ.get("TINYWEB_DATA_DIR") or os.path.expanduser("~/.tinyweb")
|
||||||
|
IDENTITY_FILE = "tinyweb-site_identity"
|
||||||
|
|
||||||
|
APP_NAME = "tinyweb"
|
||||||
|
ASPECTS = ["server"]
|
||||||
|
RNS_REQUEST_PATH = "/tinyweb"
|
||||||
|
|
||||||
|
import RNS
|
||||||
|
|
||||||
|
|
||||||
|
def load_or_create_identity():
|
||||||
|
identity_path = os.path.join(DATA_DIR, IDENTITY_FILE)
|
||||||
|
if os.path.isfile(identity_path):
|
||||||
|
return RNS.Identity.from_file(identity_path)
|
||||||
|
identity = RNS.Identity()
|
||||||
|
os.makedirs(DATA_DIR, exist_ok=True)
|
||||||
|
identity.to_file(identity_path)
|
||||||
|
os.chmod(identity_path, 0o600)
|
||||||
|
return identity
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
configdir = os.environ.get("RNS_CONFIG_DIR")
|
||||||
|
reticulum = RNS.Reticulum(configdir=configdir)
|
||||||
|
|
||||||
|
identity = load_or_create_identity()
|
||||||
|
|
||||||
|
destination = RNS.Destination(
|
||||||
|
identity,
|
||||||
|
RNS.Destination.IN,
|
||||||
|
RNS.Destination.SINGLE,
|
||||||
|
APP_NAME,
|
||||||
|
*ASPECTS,
|
||||||
|
)
|
||||||
|
|
||||||
|
destination.register_request_handler(
|
||||||
|
RNS_REQUEST_PATH,
|
||||||
|
response_generator=request_handler,
|
||||||
|
allow=RNS.Destination.ALLOW_ALL,
|
||||||
|
)
|
||||||
|
|
||||||
|
destination.announce()
|
||||||
|
dest_hash = destination.hash.hex()
|
||||||
|
print(f"tinyweb-site server running!")
|
||||||
|
print(f"Destination hash: <{dest_hash}>")
|
||||||
|
print(f"Add this hash to a TinyWeb instance as a mesh site to browse.")
|
||||||
|
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
time.sleep(1)
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print("\nShutting down...")
|
||||||
|
destination.unregister_request_handler()
|
||||||
|
|
||||||
|
|
||||||
|
def request_handler(path, data, request_id, link_id, remote_identity, requested_at):
|
||||||
|
if data is None:
|
||||||
|
data = {"method": "GET", "path": "/", "query": {}, "body": {}, "gateway_host": ""}
|
||||||
|
req_path = data.get("path", "/")
|
||||||
|
|
||||||
|
if req_path in ("/", "/index.html") or not req_path.strip("/"):
|
||||||
|
fs_path = os.path.join(SITE_DIR, "index.html")
|
||||||
|
else:
|
||||||
|
fs_path = os.path.join(SITE_DIR, req_path.lstrip("/"))
|
||||||
|
|
||||||
|
real_path = os.path.realpath(fs_path)
|
||||||
|
site_real = os.path.realpath(SITE_DIR)
|
||||||
|
|
||||||
|
if not real_path.startswith(site_real + os.sep) and real_path != site_real:
|
||||||
|
body = f"<html><body><h1>404 Not Found</h1><p>{req_path}</p></body></html>"
|
||||||
|
return {"status": 404, "content_type": "text/html; charset=utf-8", "body": body, "headers": {}}
|
||||||
|
|
||||||
|
if not os.path.isfile(real_path):
|
||||||
|
real_path = os.path.join(SITE_DIR, "index.html")
|
||||||
|
|
||||||
|
if not os.path.isfile(real_path):
|
||||||
|
body = f"<html><body><h1>404 Not Found</h1></body></html>"
|
||||||
|
return {"status": 404, "content_type": "text/html; charset=utf-8", "body": body, "headers": {}}
|
||||||
|
|
||||||
|
with open(real_path, "rb") as f:
|
||||||
|
content = f.read()
|
||||||
|
|
||||||
|
content_type, _ = mimetypes.guess_type(real_path)
|
||||||
|
if not content_type:
|
||||||
|
content_type = "text/html; charset=utf-8"
|
||||||
|
elif content_type.startswith("text/"):
|
||||||
|
content_type += "; charset=utf-8"
|
||||||
|
|
||||||
|
return {
|
||||||
|
"status": 200,
|
||||||
|
"content_type": content_type,
|
||||||
|
"body": content.decode("utf-8", errors="replace"),
|
||||||
|
"headers": {},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
|
@ -18,6 +18,7 @@ from ._helpers import (
|
||||||
from .search import handle_search
|
from .search import handle_search
|
||||||
from .pages import (
|
from .pages import (
|
||||||
handle_add_form, handle_add_submit, handle_add_manual_submit,
|
handle_add_form, handle_add_submit, handle_add_manual_submit,
|
||||||
|
handle_page_view,
|
||||||
handle_pages, _render_bulk_delete_confirm, handle_bulk_action,
|
handle_pages, _render_bulk_delete_confirm, handle_bulk_action,
|
||||||
handle_edit_form, handle_edit_submit,
|
handle_edit_form, handle_edit_submit,
|
||||||
handle_delete_confirm, handle_delete,
|
handle_delete_confirm, handle_delete,
|
||||||
|
|
@ -39,7 +40,7 @@ from .data import (
|
||||||
handle_reindex_form, handle_reindex_submit, _reindex_thread,
|
handle_reindex_form, handle_reindex_submit, _reindex_thread,
|
||||||
)
|
)
|
||||||
from .rns import (
|
from .rns import (
|
||||||
handle_rns_list, handle_rns_add, handle_rns_delete, handle_rns_browse,
|
handle_rns_delete_hash, handle_rns_browse,
|
||||||
)
|
)
|
||||||
|
|
||||||
forum_plugin = None
|
forum_plugin = None
|
||||||
|
|
@ -65,6 +66,9 @@ def _dispatch_inner(data):
|
||||||
elif path == "/add":
|
elif path == "/add":
|
||||||
prefill_url = query.get("url", [""])[0].strip()
|
prefill_url = query.get("url", [""])[0].strip()
|
||||||
return handle_add_form(prefill_url=prefill_url)
|
return handle_add_form(prefill_url=prefill_url)
|
||||||
|
elif path.startswith("/pages/"):
|
||||||
|
pid = extract_id("/pages/")
|
||||||
|
return handle_page_view(pid) if pid is not None else _error(400)
|
||||||
elif path == "/pages":
|
elif path == "/pages":
|
||||||
return handle_pages(query)
|
return handle_pages(query)
|
||||||
elif path.startswith("/edit/"):
|
elif path.startswith("/edit/"):
|
||||||
|
|
@ -97,8 +101,6 @@ def _dispatch_inner(data):
|
||||||
if not dest_hash:
|
if not dest_hash:
|
||||||
return _error(404)
|
return _error(404)
|
||||||
return handle_rns_browse(path, dest_hash)
|
return handle_rns_browse(path, dest_hash)
|
||||||
elif path == "/rns":
|
|
||||||
return handle_rns_list(query)
|
|
||||||
elif path == "/reindex":
|
elif path == "/reindex":
|
||||||
return handle_reindex_form()
|
return handle_reindex_form()
|
||||||
elif path == "/api/sites":
|
elif path == "/api/sites":
|
||||||
|
|
@ -167,10 +169,8 @@ def _dispatch_inner(data):
|
||||||
return handle_subscription_delete(sid) if sid is not None else _error(400)
|
return handle_subscription_delete(sid) if sid is not None else _error(400)
|
||||||
elif path == "/subscriptions/syncall":
|
elif path == "/subscriptions/syncall":
|
||||||
return handle_subscription_syncall()
|
return handle_subscription_syncall()
|
||||||
elif path == "/rns/add":
|
|
||||||
return handle_rns_add(body)
|
|
||||||
elif path == "/rns/delete":
|
elif path == "/rns/delete":
|
||||||
return handle_rns_delete(body)
|
return handle_rns_delete_hash(body)
|
||||||
|
|
||||||
return _error(404)
|
return _error(404)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -33,12 +33,23 @@ def handle_add_form(msg="", action_type="index", prefill_url=""):
|
||||||
f"<p>Add a site to your index</p>"
|
f"<p>Add a site to your index</p>"
|
||||||
f'<form method="post" action="/add">'
|
f'<form method="post" action="/add">'
|
||||||
f'{_csrf_field()}'
|
f'{_csrf_field()}'
|
||||||
|
f'<input type="hidden" name="input_type" value="url">'
|
||||||
f'<input name="url" placeholder="https://example.com" size="50" {url_value}><br><br>'
|
f'<input name="url" placeholder="https://example.com" size="50" {url_value}><br><br>'
|
||||||
f'<input name="note" placeholder="why are you saving this? (optional)" size="50"><br><br>'
|
f'<input name="note" placeholder="why are you saving this? (optional)" size="50"><br><br>'
|
||||||
f'<input name="tags" placeholder="tags (comma-separated, e.g. solarpunk, mesh)" size="50"><br>'
|
f'<input name="tags" placeholder="tags (comma-separated, e.g. solarpunk, mesh)" size="50"><br>'
|
||||||
f'<small>tag: private to exclude from sharing</small><br><br>'
|
f'<small>tag: private to exclude from sharing</small><br><br>'
|
||||||
f'<button type="submit">index</button>'
|
f'<button type="submit">index</button>'
|
||||||
f"</form>"
|
f"</form>"
|
||||||
|
f"<br><hr><br>"
|
||||||
|
f"<h2>add mesh site</h2>"
|
||||||
|
f"<p>Browse a remote TinyWeb instance over Reticulum</p>"
|
||||||
|
f'<form method="post" action="/add">'
|
||||||
|
f'{_csrf_field()}'
|
||||||
|
f'<input type="hidden" name="input_type" value="rns">'
|
||||||
|
f'<input name="reticulum_dest" placeholder="RNS destination hash (32 hex chars)" size="50" style="font-family:monospace"><br><br>'
|
||||||
|
f'<input name="name" placeholder="name (optional)" size="50"><br><br>'
|
||||||
|
f'<button type="submit">add mesh site</button>'
|
||||||
|
f"</form>"
|
||||||
f"<p>{msg}</p>"
|
f"<p>{msg}</p>"
|
||||||
f'<a href="/">back</a>'
|
f'<a href="/">back</a>'
|
||||||
)
|
)
|
||||||
|
|
@ -48,60 +59,63 @@ def handle_add_submit(body):
|
||||||
input_type = body.get("input_type", ["url"])[0]
|
input_type = body.get("input_type", ["url"])[0]
|
||||||
url = body.get("url", [""])[0].strip()
|
url = body.get("url", [""])[0].strip()
|
||||||
reticulum_dest = body.get("reticulum_dest", [""])[0].strip().replace("<", "").replace(">", "")
|
reticulum_dest = body.get("reticulum_dest", [""])[0].strip().replace("<", "").replace(">", "")
|
||||||
|
name = body.get("name", [""])[0].strip()
|
||||||
note = body.get("note", [""])[0].strip()
|
note = body.get("note", [""])[0].strip()
|
||||||
tags = body.get("tags", [""])[0].strip()
|
tags = body.get("tags", [""])[0].strip()
|
||||||
|
|
||||||
|
if input_type == "rns":
|
||||||
|
if not reticulum_dest:
|
||||||
|
return handle_add_form("RNS destination hash is required.")
|
||||||
|
if len(reticulum_dest) != 32 or not all(c in "0123456789abcdefABCDEF" for c in reticulum_dest):
|
||||||
|
return handle_add_form("Invalid RNS destination hash. Must be 32 hex characters.")
|
||||||
|
from .rns import handle_rns_add_hash
|
||||||
|
handle_rns_add_hash(reticulum_dest, name)
|
||||||
|
return _redirect("/")
|
||||||
|
|
||||||
if input_type == "url":
|
if input_type == "url":
|
||||||
if not url:
|
if not url:
|
||||||
return handle_add_form("URL is required.")
|
return handle_add_form("URL is required.")
|
||||||
url = clean_url(url)
|
url = clean_url(url)
|
||||||
if not url.startswith(("http://", "https://")):
|
if not url.startswith(("http://", "https://")):
|
||||||
return handle_add_form("URL must start with http:// or https://")
|
return handle_add_form("URL must start with http:// or https://")
|
||||||
|
|
||||||
|
try:
|
||||||
|
title = index_url(url, note)
|
||||||
|
if tags:
|
||||||
|
db = get_db()
|
||||||
|
try:
|
||||||
|
row = db.execute("SELECT id FROM pages WHERE url = ?", (url,)).fetchone()
|
||||||
|
if row:
|
||||||
|
_set_page_tags(row["id"], tags, db)
|
||||||
|
db.commit()
|
||||||
|
finally:
|
||||||
|
return_db(db)
|
||||||
|
return handle_add_form(f'Indexed: {esc(url)}')
|
||||||
|
except ValueError as e:
|
||||||
|
return handle_add_form(f"Error: {esc(str(e))}")
|
||||||
|
except Exception as e:
|
||||||
|
error_msg = str(e).lower()
|
||||||
|
if any(x in error_msg for x in ("block", "cloudflare", "403", "ssl", "handshake", "max retries", "timeout", "connection")):
|
||||||
|
return _respond(
|
||||||
|
f"<h1>add url (manual entry)</h1>"
|
||||||
|
f"<p><strong>{esc(url)}</strong> blocks automated access. "
|
||||||
|
f"You can still save it manually:</p>"
|
||||||
|
f'<form method="post" action="/add/manual">'
|
||||||
|
f'{_csrf_field()}'
|
||||||
|
f'<input type="hidden" name="url" value="{esc(url)}">'
|
||||||
|
f'<input type="hidden" name="note" value="{esc(note)}">'
|
||||||
|
f'<input type="hidden" name="tags" value="{esc(tags)}">'
|
||||||
|
f'<label>Title:</label><br>'
|
||||||
|
f'<input name="manual_title" size="50" placeholder="page title" required><br><br>'
|
||||||
|
f'<label>Description:</label><br>'
|
||||||
|
f'<textarea name="manual_description" rows="4" cols="50" placeholder="what is this site about? (optional)"></textarea><br><br>'
|
||||||
|
f'<button type="submit">save manually</button>'
|
||||||
|
f"</form>"
|
||||||
|
f'<a href="/">back</a>'
|
||||||
|
)
|
||||||
|
return handle_add_form(f"Error: could not fetch or index that URL. {esc(str(e)[:100])}")
|
||||||
else:
|
else:
|
||||||
if not reticulum_dest:
|
return handle_add_form("Invalid input type.")
|
||||||
return handle_add_form("Reticulum destination hash is required.")
|
|
||||||
if len(reticulum_dest) != 32 or not all(c in "0123456789abcdefABCDEF" for c in reticulum_dest):
|
|
||||||
return handle_add_form("Invalid reticulum destination hash. Must be 32 hex characters.")
|
|
||||||
url = f"reticulum:{reticulum_dest}"
|
|
||||||
|
|
||||||
try:
|
|
||||||
title = index_url(url, note, reticulum_dest if reticulum_dest else "")
|
|
||||||
if tags:
|
|
||||||
db = get_db()
|
|
||||||
try:
|
|
||||||
row = db.execute("SELECT id FROM pages WHERE url = ?", (url,)).fetchone()
|
|
||||||
if row:
|
|
||||||
_set_page_tags(row["id"], tags, db)
|
|
||||||
db.commit()
|
|
||||||
finally:
|
|
||||||
return_db(db)
|
|
||||||
|
|
||||||
return handle_add_form(f'Indexed: {esc(url)}')
|
|
||||||
|
|
||||||
except ValueError as e:
|
|
||||||
return handle_add_form(f"Error: {esc(str(e))}")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
error_msg = str(e).lower()
|
|
||||||
if any(x in error_msg for x in ("block", "cloudflare", "403", "ssl", "handshake", "max retries", "timeout", "connection")):
|
|
||||||
return _respond(
|
|
||||||
f"<h1>add url (manual entry)</h1>"
|
|
||||||
f"<p><strong>{esc(url)}</strong> blocks automated access. "
|
|
||||||
f"You can still save it manually:</p>"
|
|
||||||
f'<form method="post" action="/add/manual">'
|
|
||||||
f'{_csrf_field()}'
|
|
||||||
f'<input type="hidden" name="url" value="{esc(url)}">'
|
|
||||||
f'<input type="hidden" name="note" value="{esc(note)}">'
|
|
||||||
f'<input type="hidden" name="tags" value="{esc(tags)}">'
|
|
||||||
f'<label>Title:</label><br>'
|
|
||||||
f'<input name="manual_title" size="50" placeholder="page title" required><br><br>'
|
|
||||||
f'<label>Description:</label><br>'
|
|
||||||
f'<textarea name="manual_description" rows="4" cols="50" placeholder="what is this site about? (optional)"></textarea><br><br>'
|
|
||||||
f'<button type="submit">save manually</button>'
|
|
||||||
f"</form>"
|
|
||||||
f'<a href="/">back</a>'
|
|
||||||
)
|
|
||||||
return handle_add_form(f"Error: could not fetch or index that URL. {esc(str(e)[:100])}")
|
|
||||||
|
|
||||||
|
|
||||||
def handle_add_manual_submit(body):
|
def handle_add_manual_submit(body):
|
||||||
|
|
@ -148,6 +162,32 @@ def handle_add_manual_submit(body):
|
||||||
return_db(db)
|
return_db(db)
|
||||||
|
|
||||||
|
|
||||||
|
def handle_page_view(page_id):
|
||||||
|
db = get_db()
|
||||||
|
try:
|
||||||
|
row = db.execute("SELECT id, url, title, body, note FROM pages WHERE id = ?", (page_id,)).fetchone()
|
||||||
|
if not row:
|
||||||
|
return _error(404)
|
||||||
|
tags = _get_page_tags(row["id"], db)
|
||||||
|
tag_links = " ".join(f'<a href="/tags/{esc(t)}">[{esc(t)}]</a>' for t in tags) if tags else ""
|
||||||
|
note_html = f'<p><em>{esc(row["note"])}</em></p>' if row["note"] else ""
|
||||||
|
title = esc(row["title"] or "(untitled)")
|
||||||
|
body_html = row["body"] or "(no content)"
|
||||||
|
url_link = f'<p><small>source: <a href="{esc(row["url"])}" rel="noreferrer noopener">{esc(row["url"])}</a></small></p>'
|
||||||
|
return _respond(
|
||||||
|
f"<h1>{title}</h1>"
|
||||||
|
f"{tag_links}"
|
||||||
|
f"{note_html}"
|
||||||
|
f"<hr>"
|
||||||
|
f"{body_html}"
|
||||||
|
f"<hr>"
|
||||||
|
f"{url_link}"
|
||||||
|
f'<a href="/pages">back to browse</a>'
|
||||||
|
)
|
||||||
|
finally:
|
||||||
|
return_db(db)
|
||||||
|
|
||||||
|
|
||||||
def handle_pages(query=None):
|
def handle_pages(query=None):
|
||||||
msg = query.get("msg", [""])[0] if query else ""
|
msg = query.get("msg", [""])[0] if query else ""
|
||||||
msg_html = f'<p class="success">{esc(msg)}</p>' if msg else ""
|
msg_html = f'<p class="success">{esc(msg)}</p>' if msg else ""
|
||||||
|
|
@ -170,7 +210,7 @@ def handle_pages(query=None):
|
||||||
tags_html = f' {tag_links}'
|
tags_html = f' {tag_links}'
|
||||||
items += (
|
items += (
|
||||||
f'<li><label><input type="checkbox" name="ids" value="{r["id"]}"> '
|
f'<li><label><input type="checkbox" name="ids" value="{r["id"]}"> '
|
||||||
f'{esc(r["title"])}</label>{note_html}{tags_html} '
|
f'<a href="/pages/{r["id"]}">{esc(r["title"])}</a></label>{note_html}{tags_html} '
|
||||||
f'<small>(<a href="{esc(r["url"])}" rel="noreferrer noopener">{esc(r["url"])}</a>)</small> '
|
f'<small>(<a href="{esc(r["url"])}" rel="noreferrer noopener">{esc(r["url"])}</a>)</small> '
|
||||||
f'<a href="/edit/{r["id"]}?p={page}">edit</a> '
|
f'<a href="/edit/{r["id"]}?p={page}">edit</a> '
|
||||||
f'<a href="/delete/{r["id"]}">remove</a></li>'
|
f'<a href="/delete/{r["id"]}">remove</a></li>'
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,7 @@
|
||||||
import json
|
import json
|
||||||
from urllib.parse import urlparse
|
|
||||||
from tinyweb.db import get_db, return_db
|
from tinyweb.db import get_db, return_db
|
||||||
from tinyweb.rns_client import fetch_remote_page
|
from tinyweb.rns_client import fetch_remote_page
|
||||||
from tinyweb.templates import esc
|
from tinyweb.templates import esc
|
||||||
from ._helpers import _respond, _redirect, _error
|
|
||||||
from .customize import _set_flash
|
|
||||||
|
|
||||||
|
|
||||||
def _get_mesh_sites():
|
def _get_mesh_sites():
|
||||||
|
|
@ -15,12 +12,34 @@ def _get_mesh_sites():
|
||||||
return_db(db)
|
return_db(db)
|
||||||
|
|
||||||
|
|
||||||
|
def handle_rns_add_hash(dest_hash, name=""):
|
||||||
|
db = get_db()
|
||||||
|
try:
|
||||||
|
db.execute(
|
||||||
|
"INSERT OR REPLACE INTO mesh_sites (hash, name) VALUES (?, ?)",
|
||||||
|
(dest_hash, name or ""),
|
||||||
|
)
|
||||||
|
db.commit()
|
||||||
|
finally:
|
||||||
|
return_db(db)
|
||||||
|
|
||||||
|
|
||||||
|
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 _rewrite_links(html, dest_hash):
|
def _rewrite_links(html, dest_hash):
|
||||||
"""Rewrite all relative links in HTML to route through /rns/<hash>/..."""
|
|
||||||
out = []
|
out = []
|
||||||
i = 0
|
i = 0
|
||||||
while i < len(html):
|
while i < len(html):
|
||||||
# Check for href="...
|
|
||||||
href_start = html.find('href="', i)
|
href_start = html.find('href="', i)
|
||||||
src_start = html.find('src="', i)
|
src_start = html.find('src="', i)
|
||||||
action_start = html.find('action="', i)
|
action_start = html.find('action="', i)
|
||||||
|
|
@ -41,7 +60,6 @@ def _rewrite_links(html, dest_hash):
|
||||||
pos, attr, prefix = candidates[0]
|
pos, attr, prefix = candidates[0]
|
||||||
out.append(html[i:pos + len(prefix)])
|
out.append(html[i:pos + len(prefix)])
|
||||||
|
|
||||||
# Find end of attribute value
|
|
||||||
value_start = pos + len(prefix)
|
value_start = pos + len(prefix)
|
||||||
value_end = html.find('"', value_start)
|
value_end = html.find('"', value_start)
|
||||||
if value_end < 0:
|
if value_end < 0:
|
||||||
|
|
@ -50,10 +68,8 @@ def _rewrite_links(html, dest_hash):
|
||||||
value = html[value_start:value_end]
|
value = html[value_start:value_end]
|
||||||
|
|
||||||
if value.startswith("/"):
|
if value.startswith("/"):
|
||||||
# Relative path — rewrite
|
|
||||||
out.append(f"/rns/{dest_hash}{value}")
|
out.append(f"/rns/{dest_hash}{value}")
|
||||||
else:
|
else:
|
||||||
# External URL or empty — leave as-is
|
|
||||||
out.append(value)
|
out.append(value)
|
||||||
|
|
||||||
out.append('"')
|
out.append('"')
|
||||||
|
|
@ -62,76 +78,7 @@ def _rewrite_links(html, dest_hash):
|
||||||
return "".join(out)
|
return "".join(out)
|
||||||
|
|
||||||
|
|
||||||
def handle_rns_list(query=None):
|
|
||||||
sites = _get_mesh_sites()
|
|
||||||
rows_html = ""
|
|
||||||
for s in sites:
|
|
||||||
rows_html += (
|
|
||||||
f'<tr>'
|
|
||||||
f'<td><a href="/rns/{esc(s["hash"])}/">{esc(s["name"] or s["hash"][:16])}</a></td>'
|
|
||||||
f'<td style="font-family:monospace;font-size:0.85rem">{esc(s["hash"])}</td>'
|
|
||||||
f'<td>{esc(s["added_at"])}</td>'
|
|
||||||
f'<td>'
|
|
||||||
f'<form action="/rns/delete" method="POST" style="display:inline">'
|
|
||||||
f'<input type="hidden" name="hash" value="{esc(s["hash"])}">'
|
|
||||||
f'<button type="submit">delete</button>'
|
|
||||||
f'</form>'
|
|
||||||
f'</td>'
|
|
||||||
f'</tr>'
|
|
||||||
)
|
|
||||||
|
|
||||||
html = (
|
|
||||||
f'<h1>mesh sites</h1>'
|
|
||||||
f'<table><tr><th>site</th><th>hash</th><th>added</th><th></th></tr>{rows_html}</table>'
|
|
||||||
f'<br>'
|
|
||||||
f'<form action="/rns/add" method="POST">'
|
|
||||||
f'<input type="text" name="hash" placeholder="RNS destination hash" style="width:32ch;font-family:monospace">'
|
|
||||||
f'<input type="text" name="name" placeholder="name (optional)" style="width:20ch">'
|
|
||||||
f'<button type="submit">add site</button>'
|
|
||||||
f'</form>'
|
|
||||||
f'<br><a href="/">back</a>'
|
|
||||||
)
|
|
||||||
return _respond(html)
|
|
||||||
|
|
||||||
|
|
||||||
def handle_rns_add(body):
|
|
||||||
dest_hash = body.get("hash", [""])[0].strip()
|
|
||||||
name = body.get("name", [""])[0].strip()
|
|
||||||
if not dest_hash:
|
|
||||||
return handle_rns_list()
|
|
||||||
try:
|
|
||||||
bytes.fromhex(dest_hash)
|
|
||||||
except ValueError:
|
|
||||||
_set_flash("Invalid hash — must be hex.")
|
|
||||||
return _redirect("/rns")
|
|
||||||
|
|
||||||
db = get_db()
|
|
||||||
try:
|
|
||||||
db.execute(
|
|
||||||
"INSERT OR REPLACE INTO mesh_sites (hash, name) VALUES (?, ?)",
|
|
||||||
(dest_hash, name or ""),
|
|
||||||
)
|
|
||||||
db.commit()
|
|
||||||
finally:
|
|
||||||
return_db(db)
|
|
||||||
return _redirect("/rns")
|
|
||||||
|
|
||||||
|
|
||||||
def handle_rns_delete(body):
|
|
||||||
dest_hash = body.get("hash", [""])[0].strip()
|
|
||||||
if not dest_hash:
|
|
||||||
return _redirect("/rns")
|
|
||||||
db = get_db()
|
|
||||||
try:
|
|
||||||
db.execute("DELETE FROM mesh_sites WHERE hash = ?", (dest_hash,))
|
|
||||||
db.commit()
|
|
||||||
finally:
|
|
||||||
return_db(db)
|
|
||||||
return _redirect("/rns")
|
|
||||||
|
|
||||||
|
|
||||||
def handle_rns_browse(path, dest_hash):
|
def handle_rns_browse(path, dest_hash):
|
||||||
# Extract the sub-path after /rns/<hash>
|
|
||||||
prefix = f"/rns/{dest_hash}"
|
prefix = f"/rns/{dest_hash}"
|
||||||
sub_path = path[len(prefix):] if path.startswith(prefix) else "/"
|
sub_path = path[len(prefix):] if path.startswith(prefix) else "/"
|
||||||
if not sub_path:
|
if not sub_path:
|
||||||
|
|
@ -140,16 +87,30 @@ def handle_rns_browse(path, dest_hash):
|
||||||
try:
|
try:
|
||||||
resp = fetch_remote_page(dest_hash, sub_path)
|
resp = fetch_remote_page(dest_hash, sub_path)
|
||||||
except ConnectionError as e:
|
except ConnectionError as e:
|
||||||
return _respond(f"<h1>could not connect</h1><p>{esc(str(e))}</p>")
|
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:
|
except PermissionError:
|
||||||
return _respond("<h1>forbidden</h1><p>the remote instance blocked this request.</p>")
|
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:
|
if resp.get("status") != 200:
|
||||||
return _respond(f"<h1>error</h1><p>remote returned status {resp['status']}</p>")
|
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", "")
|
body = resp.get("body", "")
|
||||||
|
|
||||||
# If the remote returned JSON (e.g. /api/sites), wrap for display
|
|
||||||
if resp.get("content_type", "").startswith("application/json"):
|
if resp.get("content_type", "").startswith("application/json"):
|
||||||
try:
|
try:
|
||||||
data = json.loads(body)
|
data = json.loads(body)
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
from tinyweb.db import get_db, return_db, get_setting, get_site_name, clean_url
|
from tinyweb.db import get_db, return_db, get_setting, get_site_name, clean_url
|
||||||
from tinyweb.templates import esc
|
from tinyweb.templates import esc
|
||||||
from ._helpers import _sanitize_fts_query, _paginate, _get_page_tags, _respond, _page_nav, PER_PAGE
|
from ._helpers import _sanitize_fts_query, _paginate, _get_page_tags, _respond, _page_nav, _csrf_field, PER_PAGE
|
||||||
|
|
||||||
|
|
||||||
def handle_search(query):
|
def handle_search(query):
|
||||||
|
|
@ -162,6 +162,31 @@ def handle_search(query):
|
||||||
sub_count = ""
|
sub_count = ""
|
||||||
if q and remote_rows:
|
if q and remote_rows:
|
||||||
sub_count = f" + {len(remote_rows)} from subscriptions"
|
sub_count = f" + {len(remote_rows)} from subscriptions"
|
||||||
|
|
||||||
|
mesh_html = ""
|
||||||
|
if not q:
|
||||||
|
from .rns import _get_mesh_sites
|
||||||
|
sites = _get_mesh_sites()
|
||||||
|
if sites:
|
||||||
|
rows_html = ""
|
||||||
|
for s in sites:
|
||||||
|
rows_html += (
|
||||||
|
f'<tr>'
|
||||||
|
f'<td><a href="/rns/{esc(s["hash"])}/">{esc(s["name"] or s["hash"][:16])}</a></td>'
|
||||||
|
f'<td style="font-family:monospace;font-size:0.85rem">{esc(s["hash"])}</td>'
|
||||||
|
f'<td><form action="/rns/delete" method="POST" style="display:inline">'
|
||||||
|
f'{_csrf_field()}'
|
||||||
|
f'<input type="hidden" name="hash" value="{esc(s["hash"])}">'
|
||||||
|
f'<button type="submit">delete</button>'
|
||||||
|
f'</form></td>'
|
||||||
|
f'</tr>'
|
||||||
|
)
|
||||||
|
mesh_html = (
|
||||||
|
f"<br>"
|
||||||
|
f"<h2>mesh sites</h2>"
|
||||||
|
f'<table>{rows_html}</table>'
|
||||||
|
)
|
||||||
|
|
||||||
welcome_html = ""
|
welcome_html = ""
|
||||||
if count == 0 and not q:
|
if count == 0 and not q:
|
||||||
welcome_html = (
|
welcome_html = (
|
||||||
|
|
@ -184,5 +209,5 @@ def handle_search(query):
|
||||||
f'{welcome_html}'
|
f'{welcome_html}'
|
||||||
f'{result_html}'
|
f'{result_html}'
|
||||||
f'{_page_nav(page, total_results, f"/?q={esc(q)}") if q else ""}'
|
f'{_page_nav(page, total_results, f"/?q={esc(q)}") if q else ""}'
|
||||||
f'{trusted_html}{remote_html}'
|
f'{trusted_html}{remote_html}{mesh_html}'
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,7 @@ def handle_tag_browse(tag_name, query=None):
|
||||||
tags = _get_page_tags(r["id"], db)
|
tags = _get_page_tags(r["id"], db)
|
||||||
tag_links = " ".join(f'<a href="/tags/{esc(t)}">[{esc(t)}]</a>' for t in tags)
|
tag_links = " ".join(f'<a href="/tags/{esc(t)}">[{esc(t)}]</a>' for t in tags)
|
||||||
items += (
|
items += (
|
||||||
f'<li>{esc(r["title"])}{note_html} {tag_links} '
|
f'<li><a href="/pages/{r["id"]}">{esc(r["title"])}</a>{note_html} {tag_links} '
|
||||||
f'<small>(<a href="{esc(r["url"])}" rel="noreferrer noopener">{esc(r["url"])}</a>)</small></li>'
|
f'<small>(<a href="{esc(r["url"])}" rel="noreferrer noopener">{esc(r["url"])}</a>)</small></li>'
|
||||||
)
|
)
|
||||||
finally:
|
finally:
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue