unified add form, remove page view endpoint, drop mesh nav/section
This commit is contained in:
parent
d0b27b4c61
commit
cc71d9c577
6 changed files with 117 additions and 129 deletions
|
|
@ -18,7 +18,6 @@ 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,
|
||||||
|
|
@ -66,9 +65,6 @@ 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/"):
|
||||||
|
|
|
||||||
|
|
@ -29,55 +29,50 @@ def handle_add_form(msg="", action_type="index", prefill_url=""):
|
||||||
)
|
)
|
||||||
url_value = f'value="{esc(prefill_url)}" ' if prefill_url else ""
|
url_value = f'value="{esc(prefill_url)}" ' if prefill_url else ""
|
||||||
return _respond(
|
return _respond(
|
||||||
f"<h1>add url</h1>"
|
f"<h1>add site</h1>"
|
||||||
f"<p>Add a site to your index</p>"
|
f"<p>Add a site to your index — URL or RNS destination hash</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 or 32-char RNS hash" 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>'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def handle_add_submit(body):
|
def handle_add_submit(body):
|
||||||
input_type = body.get("input_type", ["url"])[0]
|
raw = body.get("url", [""])[0].strip().replace("<", "").replace(">", "")
|
||||||
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()
|
note = body.get("note", [""])[0].strip()
|
||||||
tags = body.get("tags", [""])[0].strip()
|
tags = body.get("tags", [""])[0].strip()
|
||||||
|
|
||||||
if input_type == "rns":
|
if not raw:
|
||||||
if not reticulum_dest:
|
return handle_add_form("URL or RNS hash is required.")
|
||||||
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):
|
is_rns = (
|
||||||
return handle_add_form("Invalid RNS destination hash. Must be 32 hex characters.")
|
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
|
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("/")
|
return _redirect("/")
|
||||||
|
|
||||||
if input_type == "url":
|
url = clean_url(raw)
|
||||||
if not url:
|
|
||||||
return handle_add_form("URL is required.")
|
|
||||||
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("Enter a URL (http:// or https://) or a 32-char RNS destination hash.")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
title = index_url(url, note)
|
title = index_url(url, note)
|
||||||
|
|
@ -114,8 +109,6 @@ def handle_add_submit(body):
|
||||||
f'<a href="/">back</a>'
|
f'<a href="/">back</a>'
|
||||||
)
|
)
|
||||||
return handle_add_form(f"Error: could not fetch or index that URL. {esc(str(e)[:100])}")
|
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.")
|
|
||||||
|
|
||||||
|
|
||||||
def handle_add_manual_submit(body):
|
def handle_add_manual_submit(body):
|
||||||
|
|
@ -162,32 +155,6 @@ 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 ""
|
||||||
|
|
@ -208,10 +175,17 @@ def handle_pages(query=None):
|
||||||
if tags:
|
if tags:
|
||||||
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)
|
||||||
tags_html = f' {tag_links}'
|
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 += (
|
items += (
|
||||||
f'<li><label><input type="checkbox" name="ids" value="{r["id"]}"> '
|
f'<li><label><input type="checkbox" name="ids" value="{r["id"]}"> '
|
||||||
f'<a href="/pages/{r["id"]}">{esc(r["title"])}</a></label>{note_html}{tags_html} '
|
f'{esc(r["title"])}</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(link_url)}" rel="noreferrer noopener">{esc(display_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,4 +1,5 @@
|
||||||
import json
|
import json
|
||||||
|
import traceback
|
||||||
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
|
||||||
|
|
@ -14,15 +15,49 @@ def _get_mesh_sites():
|
||||||
|
|
||||||
def handle_rns_add_hash(dest_hash, name=""):
|
def handle_rns_add_hash(dest_hash, name=""):
|
||||||
db = get_db()
|
db = get_db()
|
||||||
|
errors = []
|
||||||
try:
|
try:
|
||||||
db.execute(
|
db.execute(
|
||||||
"INSERT OR REPLACE INTO mesh_sites (hash, name) VALUES (?, ?)",
|
"INSERT OR REPLACE INTO mesh_sites (hash, name) VALUES (?, ?)",
|
||||||
(dest_hash, name or ""),
|
(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()
|
db.commit()
|
||||||
finally:
|
finally:
|
||||||
return_db(db)
|
return_db(db)
|
||||||
|
|
||||||
|
return errors if errors else None
|
||||||
|
|
||||||
|
|
||||||
def handle_rns_delete_hash(body):
|
def handle_rns_delete_hash(body):
|
||||||
dest_hash = body.get("hash", [""])[0].strip() if isinstance(body, dict) else body
|
dest_hash = body.get("hash", [""])[0].strip() if isinstance(body, dict) else 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, _csrf_field, PER_PAGE
|
from ._helpers import _sanitize_fts_query, _paginate, _get_page_tags, _respond, _page_nav, PER_PAGE
|
||||||
|
|
||||||
|
|
||||||
def handle_search(query):
|
def handle_search(query):
|
||||||
|
|
@ -83,10 +83,17 @@ def handle_search(query):
|
||||||
tag_links = " ".join(f'<a href="/tags/{esc(t)}" class="tag">[{esc(t)}]</a>' for t in tags)
|
tag_links = " ".join(f'<a href="/tags/{esc(t)}" class="tag">[{esc(t)}]</a>' for t in tags)
|
||||||
tags_html = f'<div class="tags">{tag_links}</div>'
|
tags_html = f'<div class="tags">{tag_links}</div>'
|
||||||
snip_html = f'<br>{esc(r["summary"])}' if r["summary"] else ""
|
snip_html = f'<br>{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 += (
|
result_html += (
|
||||||
f'<div class="result">'
|
f'<div class="result">'
|
||||||
f'<a href="{esc(r["url"])}" rel="noreferrer noopener">{esc(r["title"])}</a><br>'
|
f'<a href="{esc(link_url)}" rel="noreferrer noopener">{esc(r["title"])}</a><br>'
|
||||||
f'<small>{esc(r["url"])}</small>'
|
f'<small>{esc(display_url)}</small>'
|
||||||
f'{snip_html}'
|
f'{snip_html}'
|
||||||
f'{note_html}{tags_html}'
|
f'{note_html}{tags_html}'
|
||||||
f'</div>'
|
f'</div>'
|
||||||
|
|
@ -163,30 +170,6 @@ def handle_search(query):
|
||||||
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 = (
|
||||||
|
|
@ -209,5 +192,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}{mesh_html}'
|
f'{trusted_html}{remote_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><a href="/pages/{r["id"]}">{esc(r["title"])}</a>{note_html} {tag_links} '
|
f'<li>{esc(r["title"])}{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:
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ def _nav_html():
|
||||||
return (
|
return (
|
||||||
f'<p><b><a href="/">{name}</a></b>'
|
f'<p><b><a href="/">{name}</a></b>'
|
||||||
' | <a href="/">search</a> | <a href="/pages">browse</a>'
|
' | <a href="/">search</a> | <a href="/pages">browse</a>'
|
||||||
' | <a href="/tags">tags</a> | <a href="/subscriptions">subscriptions</a> | <a href="/rns">mesh</a>'
|
' | <a href="/tags">tags</a> | <a href="/subscriptions">subscriptions</a>'
|
||||||
f'{forum_link}'
|
f'{forum_link}'
|
||||||
' | <a href="/style">customize</a> | <a href="/about">about</a></p>\n'
|
' | <a href="/style">customize</a> | <a href="/about">about</a></p>\n'
|
||||||
"<hr>\n"
|
"<hr>\n"
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue