From d0b27b4c61dc4f2e3854e5b71fb962946d5cf5dd Mon Sep 17 00:00:00 2001 From: blankie Date: Thu, 18 Jun 2026 18:14:12 +0000 Subject: [PATCH 1/2] page view endpoint, standalone site server, RNS-aware SPA nav --- site_server.py | 104 +++++++++++++++++++++++++ src/tinyweb/handlers/__init__.py | 12 +-- src/tinyweb/handlers/pages.py | 130 ++++++++++++++++++++----------- src/tinyweb/handlers/rns.py | 123 ++++++++++------------------- src/tinyweb/handlers/search.py | 29 ++++++- src/tinyweb/handlers/tags.py | 2 +- 6 files changed, 265 insertions(+), 135 deletions(-) create mode 100644 site_server.py diff --git a/site_server.py b/site_server.py new file mode 100644 index 0000000..a1064eb --- /dev/null +++ b/site_server.py @@ -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"

404 Not Found

{req_path}

" + 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"

404 Not Found

" + 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() diff --git a/src/tinyweb/handlers/__init__.py b/src/tinyweb/handlers/__init__.py index cd52dd6..3fde8be 100644 --- a/src/tinyweb/handlers/__init__.py +++ b/src/tinyweb/handlers/__init__.py @@ -18,6 +18,7 @@ from ._helpers import ( from .search import handle_search from .pages import ( handle_add_form, handle_add_submit, handle_add_manual_submit, + handle_page_view, handle_pages, _render_bulk_delete_confirm, handle_bulk_action, handle_edit_form, handle_edit_submit, handle_delete_confirm, handle_delete, @@ -39,7 +40,7 @@ from .data import ( handle_reindex_form, handle_reindex_submit, _reindex_thread, ) 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 @@ -65,6 +66,9 @@ def _dispatch_inner(data): elif path == "/add": prefill_url = query.get("url", [""])[0].strip() 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": return handle_pages(query) elif path.startswith("/edit/"): @@ -97,8 +101,6 @@ def _dispatch_inner(data): if not dest_hash: return _error(404) return handle_rns_browse(path, dest_hash) - elif path == "/rns": - return handle_rns_list(query) elif path == "/reindex": return handle_reindex_form() 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) elif path == "/subscriptions/syncall": return handle_subscription_syncall() - elif path == "/rns/add": - return handle_rns_add(body) elif path == "/rns/delete": - return handle_rns_delete(body) + return handle_rns_delete_hash(body) return _error(404) diff --git a/src/tinyweb/handlers/pages.py b/src/tinyweb/handlers/pages.py index 4eb2a12..be68e18 100644 --- a/src/tinyweb/handlers/pages.py +++ b/src/tinyweb/handlers/pages.py @@ -33,12 +33,23 @@ def handle_add_form(msg="", action_type="index", prefill_url=""): f"

Add a site to your index

" f'
' f'{_csrf_field()}' + f'' f'

' f'

' f'
' f'tag: private to exclude from sharing

' f'' f"
" + f"


" + f"

add mesh site

" + f"

Browse a remote TinyWeb instance over Reticulum

" + f'
' + f'{_csrf_field()}' + f'' + f'

' + f'

' + f'' + f"
" f"

{msg}

" f'back' ) @@ -48,60 +59,63 @@ def handle_add_submit(body): input_type = body.get("input_type", ["url"])[0] url = body.get("url", [""])[0].strip() reticulum_dest = body.get("reticulum_dest", [""])[0].strip().replace("<", "").replace(">", "") + name = body.get("name", [""])[0].strip() note = body.get("note", [""])[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 not url: return handle_add_form("URL is required.") url = clean_url(url) if not url.startswith(("http://", "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"

add url (manual entry)

" + f"

{esc(url)} blocks automated access. " + f"You can still save it manually:

" + f'
' + f'{_csrf_field()}' + f'' + f'' + f'' + f'
' + f'

' + f'
' + f'

' + f'' + f"
" + f'back' + ) + return handle_add_form(f"Error: could not fetch or index that URL. {esc(str(e)[:100])}") else: - if not reticulum_dest: - 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"

add url (manual entry)

" - f"

{esc(url)} blocks automated access. " - f"You can still save it manually:

" - f'
' - f'{_csrf_field()}' - f'' - f'' - f'' - f'
' - f'

' - f'
' - f'

' - f'' - f"
" - f'back' - ) - return handle_add_form(f"Error: could not fetch or index that URL. {esc(str(e)[:100])}") + return handle_add_form("Invalid input type.") def handle_add_manual_submit(body): @@ -148,6 +162,32 @@ def handle_add_manual_submit(body): 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'[{esc(t)}]' for t in tags) if tags else "" + note_html = f'

{esc(row["note"])}

' if row["note"] else "" + title = esc(row["title"] or "(untitled)") + body_html = row["body"] or "(no content)" + url_link = f'

source: {esc(row["url"])}

' + return _respond( + f"

{title}

" + f"{tag_links}" + f"{note_html}" + f"
" + f"{body_html}" + f"
" + f"{url_link}" + f'back to browse' + ) + finally: + return_db(db) + + def handle_pages(query=None): msg = query.get("msg", [""])[0] if query else "" msg_html = f'

{esc(msg)}

' if msg else "" @@ -170,7 +210,7 @@ def handle_pages(query=None): tags_html = f' {tag_links}' items += ( f'
  • {note_html}{tags_html} ' + f'{esc(r["title"])}{note_html}{tags_html} ' f'({esc(r["url"])}) ' f'edit ' f'remove
  • ' diff --git a/src/tinyweb/handlers/rns.py b/src/tinyweb/handlers/rns.py index af699d4..7fe67f3 100644 --- a/src/tinyweb/handlers/rns.py +++ b/src/tinyweb/handlers/rns.py @@ -1,10 +1,7 @@ import json -from urllib.parse import urlparse from tinyweb.db import get_db, return_db from tinyweb.rns_client import fetch_remote_page from tinyweb.templates import esc -from ._helpers import _respond, _redirect, _error -from .customize import _set_flash def _get_mesh_sites(): @@ -15,12 +12,34 @@ def _get_mesh_sites(): 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): - """Rewrite all relative links in HTML to route through /rns//...""" out = [] i = 0 while i < len(html): - # Check for href="... href_start = html.find('href="', i) src_start = html.find('src="', i) action_start = html.find('action="', i) @@ -41,7 +60,6 @@ def _rewrite_links(html, dest_hash): pos, attr, prefix = candidates[0] out.append(html[i:pos + len(prefix)]) - # Find end of attribute value value_start = pos + len(prefix) value_end = html.find('"', value_start) if value_end < 0: @@ -50,10 +68,8 @@ def _rewrite_links(html, dest_hash): value = html[value_start:value_end] if value.startswith("/"): - # Relative path — rewrite out.append(f"/rns/{dest_hash}{value}") else: - # External URL or empty — leave as-is out.append(value) out.append('"') @@ -62,76 +78,7 @@ def _rewrite_links(html, dest_hash): return "".join(out) -def handle_rns_list(query=None): - sites = _get_mesh_sites() - rows_html = "" - for s in sites: - rows_html += ( - f'' - f'{esc(s["name"] or s["hash"][:16])}' - f'{esc(s["hash"])}' - f'{esc(s["added_at"])}' - f'' - f'
    ' - f'' - f'' - f'
    ' - f'' - f'' - ) - - html = ( - f'

    mesh sites

    ' - f'{rows_html}
    sitehashadded
    ' - f'
    ' - f'
    ' - f'' - f'' - f'' - f'
    ' - f'
    back' - ) - 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): - # Extract the sub-path after /rns/ prefix = f"/rns/{dest_hash}" sub_path = path[len(prefix):] if path.startswith(prefix) else "/" if not sub_path: @@ -140,16 +87,30 @@ def handle_rns_browse(path, dest_hash): try: resp = fetch_remote_page(dest_hash, sub_path) except ConnectionError as e: - return _respond(f"

    could not connect

    {esc(str(e))}

    ") + return { + "status": 200, + "content_type": "text/html; charset=utf-8", + "body": f"

    could not connect

    {esc(str(e))}

    ", + "headers": {}, + } except PermissionError: - return _respond("

    forbidden

    the remote instance blocked this request.

    ") + return { + "status": 200, + "content_type": "text/html; charset=utf-8", + "body": "

    forbidden

    the remote instance blocked this request.

    ", + "headers": {}, + } if resp.get("status") != 200: - return _respond(f"

    error

    remote returned status {resp['status']}

    ") + return { + "status": 200, + "content_type": "text/html; charset=utf-8", + "body": f"

    error

    remote returned status {resp['status']}

    ", + "headers": {}, + } body = resp.get("body", "") - # If the remote returned JSON (e.g. /api/sites), wrap for display if resp.get("content_type", "").startswith("application/json"): try: data = json.loads(body) diff --git a/src/tinyweb/handlers/search.py b/src/tinyweb/handlers/search.py index 951b4d4..9d7dbee 100644 --- a/src/tinyweb/handlers/search.py +++ b/src/tinyweb/handlers/search.py @@ -1,6 +1,6 @@ from tinyweb.db import get_db, return_db, get_setting, get_site_name, clean_url 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): @@ -162,6 +162,31 @@ def handle_search(query): sub_count = "" if q and remote_rows: 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'' + f'{esc(s["name"] or s["hash"][:16])}' + f'{esc(s["hash"])}' + f'
    ' + f'{_csrf_field()}' + f'' + f'' + f'
    ' + f'' + ) + mesh_html = ( + f"
    " + f"

    mesh sites

    " + f'{rows_html}
    ' + ) + welcome_html = "" if count == 0 and not q: welcome_html = ( @@ -184,5 +209,5 @@ def handle_search(query): f'{welcome_html}' f'{result_html}' 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}' ) diff --git a/src/tinyweb/handlers/tags.py b/src/tinyweb/handlers/tags.py index 46189d6..5e50567 100644 --- a/src/tinyweb/handlers/tags.py +++ b/src/tinyweb/handlers/tags.py @@ -45,7 +45,7 @@ def handle_tag_browse(tag_name, query=None): tags = _get_page_tags(r["id"], db) tag_links = " ".join(f'[{esc(t)}]' for t in tags) items += ( - f'
  • {esc(r["title"])}{note_html} {tag_links} ' + f'
  • {esc(r["title"])}{note_html} {tag_links} ' f'({esc(r["url"])})
  • ' ) finally: From cc71d9c57761db1670c9d77f4376bf12c5200db0 Mon Sep 17 00:00:00 2001 From: blankie Date: Thu, 18 Jun 2026 20:34:58 +0000 Subject: [PATCH 2/2] unified add form, remove page view endpoint, drop mesh nav/section --- src/tinyweb/handlers/__init__.py | 4 - src/tinyweb/handlers/pages.py | 164 +++++++++++++------------------ src/tinyweb/handlers/rns.py | 35 +++++++ src/tinyweb/handlers/search.py | 39 +++----- src/tinyweb/handlers/tags.py | 2 +- src/tinyweb/templates.py | 2 +- 6 files changed, 117 insertions(+), 129 deletions(-) diff --git a/src/tinyweb/handlers/__init__.py b/src/tinyweb/handlers/__init__.py index 3fde8be..14a37fd 100644 --- a/src/tinyweb/handlers/__init__.py +++ b/src/tinyweb/handlers/__init__.py @@ -18,7 +18,6 @@ from ._helpers import ( from .search import handle_search from .pages import ( handle_add_form, handle_add_submit, handle_add_manual_submit, - handle_page_view, handle_pages, _render_bulk_delete_confirm, handle_bulk_action, handle_edit_form, handle_edit_submit, handle_delete_confirm, handle_delete, @@ -66,9 +65,6 @@ def _dispatch_inner(data): elif path == "/add": prefill_url = query.get("url", [""])[0].strip() 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": return handle_pages(query) elif path.startswith("/edit/"): diff --git a/src/tinyweb/handlers/pages.py b/src/tinyweb/handlers/pages.py index be68e18..9d7f96c 100644 --- a/src/tinyweb/handlers/pages.py +++ b/src/tinyweb/handlers/pages.py @@ -29,93 +29,86 @@ def handle_add_form(msg="", action_type="index", prefill_url=""): ) url_value = f'value="{esc(prefill_url)}" ' if prefill_url else "" return _respond( - f"

    add url

    " - f"

    Add a site to your index

    " + f"

    add site

    " + f"

    Add a site to your index — URL or RNS destination hash

    " f'
    ' f'{_csrf_field()}' - f'' - f'

    ' + f'

    ' f'

    ' f'
    ' f'tag: private to exclude from sharing

    ' f'' f"
    " - f"


    " - f"

    add mesh site

    " - f"

    Browse a remote TinyWeb instance over Reticulum

    " - f'
    ' - f'{_csrf_field()}' - f'' - f'

    ' - f'

    ' - f'' - f"
    " f"

    {msg}

    " f'back' ) def handle_add_submit(body): - input_type = body.get("input_type", ["url"])[0] - url = body.get("url", [""])[0].strip() - reticulum_dest = body.get("reticulum_dest", [""])[0].strip().replace("<", "").replace(">", "") - name = body.get("name", [""])[0].strip() + raw = body.get("url", [""])[0].strip().replace("<", "").replace(">", "") note = body.get("note", [""])[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.") + if not raw: + return handle_add_form("URL or RNS hash is required.") + + is_rns = ( + len(raw) == 32 + and all(c in "0123456789abcdefABCDEF" for c in raw) + ) + if raw.startswith("rns:") or raw.startswith("RNS:"): + raw = raw[4:] + is_rns = ( + len(raw) == 32 + and all(c in "0123456789abcdefABCDEF" for c in raw) + ) + + if is_rns: from .rns import handle_rns_add_hash - handle_rns_add_hash(reticulum_dest, name) + errs = handle_rns_add_hash(raw) + if errs: + return handle_add_form(f"Hash saved but indexing failed: {'; '.join(errs)}") return _redirect("/") - if input_type == "url": - if not url: - return handle_add_form("URL is required.") - url = clean_url(url) - if not url.startswith(("http://", "https://")): - return handle_add_form("URL must start with http:// or https://") + url = clean_url(raw) + if not url.startswith(("http://", "https://")): + return handle_add_form("Enter a URL (http:// or https://) or a 32-char RNS destination hash.") - 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"

    add url (manual entry)

    " - f"

    {esc(url)} blocks automated access. " - f"You can still save it manually:

    " - f'
    ' - f'{_csrf_field()}' - f'' - f'' - f'' - f'
    ' - f'

    ' - f'
    ' - f'

    ' - f'' - f"
    " - f'back' - ) - return handle_add_form(f"Error: could not fetch or index that URL. {esc(str(e)[:100])}") - else: - return handle_add_form("Invalid input type.") + 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"

    add url (manual entry)

    " + f"

    {esc(url)} blocks automated access. " + f"You can still save it manually:

    " + f'
    ' + f'{_csrf_field()}' + f'' + f'' + f'' + f'
    ' + f'

    ' + f'
    ' + f'

    ' + f'' + f"
    " + f'back' + ) + return handle_add_form(f"Error: could not fetch or index that URL. {esc(str(e)[:100])}") def handle_add_manual_submit(body): @@ -162,32 +155,6 @@ def handle_add_manual_submit(body): 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'[{esc(t)}]' for t in tags) if tags else "" - note_html = f'

    {esc(row["note"])}

    ' if row["note"] else "" - title = esc(row["title"] or "(untitled)") - body_html = row["body"] or "(no content)" - url_link = f'

    source: {esc(row["url"])}

    ' - return _respond( - f"

    {title}

    " - f"{tag_links}" - f"{note_html}" - f"
    " - f"{body_html}" - f"
    " - f"{url_link}" - f'back to browse' - ) - finally: - return_db(db) - - def handle_pages(query=None): msg = query.get("msg", [""])[0] if query else "" msg_html = f'

    {esc(msg)}

    ' if msg else "" @@ -208,10 +175,17 @@ def handle_pages(query=None): if tags: tag_links = " ".join(f'[{esc(t)}]' for t in tags) tags_html = f' {tag_links}' + url = r["url"] + if url.startswith("rns:"): + display_url = url + link_url = f"/rns/{esc(url[4:])}/" + else: + display_url = url + link_url = url items += ( f'
  • {note_html}{tags_html} ' - f'({esc(r["url"])}) ' + f'{esc(r["title"])}{note_html}{tags_html} ' + f'({esc(display_url)}) ' f'edit ' f'remove
  • ' ) diff --git a/src/tinyweb/handlers/rns.py b/src/tinyweb/handlers/rns.py index 7fe67f3..8941f99 100644 --- a/src/tinyweb/handlers/rns.py +++ b/src/tinyweb/handlers/rns.py @@ -1,4 +1,5 @@ import json +import traceback from tinyweb.db import get_db, return_db from tinyweb.rns_client import fetch_remote_page from tinyweb.templates import esc @@ -14,15 +15,49 @@ def _get_mesh_sites(): 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"]*>(.*?)", body, re.IGNORECASE | re.DOTALL) + if m: + title = m.group(1).strip() + desc = "" + m = re.search(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 diff --git a/src/tinyweb/handlers/search.py b/src/tinyweb/handlers/search.py index 9d7dbee..10b87fd 100644 --- a/src/tinyweb/handlers/search.py +++ b/src/tinyweb/handlers/search.py @@ -1,6 +1,6 @@ from tinyweb.db import get_db, return_db, get_setting, get_site_name, clean_url from tinyweb.templates import esc -from ._helpers import _sanitize_fts_query, _paginate, _get_page_tags, _respond, _page_nav, _csrf_field, PER_PAGE +from ._helpers import _sanitize_fts_query, _paginate, _get_page_tags, _respond, _page_nav, PER_PAGE def handle_search(query): @@ -83,10 +83,17 @@ def handle_search(query): tag_links = " ".join(f'[{esc(t)}]' for t in tags) tags_html = f'
    {tag_links}
    ' snip_html = f'
    {esc(r["summary"])}' if r["summary"] else "" + url = r["url"] + if url.startswith("rns:"): + display_url = url + link_url = f"/rns/{esc(url[4:])}/" + else: + display_url = url + link_url = url result_html += ( f'
    ' - f'{esc(r["title"])}
    ' - f'{esc(r["url"])}' + f'{esc(r["title"])}
    ' + f'{esc(display_url)}' f'{snip_html}' f'{note_html}{tags_html}' f'
    ' @@ -163,30 +170,6 @@ def handle_search(query): if q and remote_rows: 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'' - f'{esc(s["name"] or s["hash"][:16])}' - f'{esc(s["hash"])}' - f'
    ' - f'{_csrf_field()}' - f'' - f'' - f'
    ' - f'' - ) - mesh_html = ( - f"
    " - f"

    mesh sites

    " - f'{rows_html}
    ' - ) - welcome_html = "" if count == 0 and not q: welcome_html = ( @@ -209,5 +192,5 @@ def handle_search(query): f'{welcome_html}' f'{result_html}' f'{_page_nav(page, total_results, f"/?q={esc(q)}") if q else ""}' - f'{trusted_html}{remote_html}{mesh_html}' + f'{trusted_html}{remote_html}' ) diff --git a/src/tinyweb/handlers/tags.py b/src/tinyweb/handlers/tags.py index 5e50567..46189d6 100644 --- a/src/tinyweb/handlers/tags.py +++ b/src/tinyweb/handlers/tags.py @@ -45,7 +45,7 @@ def handle_tag_browse(tag_name, query=None): tags = _get_page_tags(r["id"], db) tag_links = " ".join(f'[{esc(t)}]' for t in tags) items += ( - f'
  • {esc(r["title"])}{note_html} {tag_links} ' + f'
  • {esc(r["title"])}{note_html} {tag_links} ' f'({esc(r["url"])})
  • ' ) finally: diff --git a/src/tinyweb/templates.py b/src/tinyweb/templates.py index 8cc3535..8208211 100644 --- a/src/tinyweb/templates.py +++ b/src/tinyweb/templates.py @@ -13,7 +13,7 @@ def _nav_html(): return ( f'

    {name}' ' | search | browse' - ' | tags | subscriptions | mesh' + ' | tags | subscriptions' f'{forum_link}' ' | customize | about

    \n' "
    \n"