from pathlib import Path import json import secrets from urllib.parse import unquote from db import get_db, return_db, get_setting, set_setting, get_site_name, index_url, clean_url from templates import esc from ._helpers import ( _csrf_field, _respond, _redirect, _error, _paginate, _page_nav, _get_page_tags, _set_page_tags, _cleanup_orphaned_tags, _get_bookmark_token, _text_response, BROWSE_PER_PAGE, ) def handle_add_form(msg="", action_type="index", prefill_url=""): if action_type == "subscribe": return _respond( f"

subscribe

" f"

Subscribe to a friend's TinyWeb instance to sync their index

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

' f'' f"
" f"

or add a single site

" f"

{msg}

" f'back' ) url_value = f'value="{esc(prefill_url)}" ' if prefill_url else "" return _respond( f"

add url

" f"

Add a site to your index

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

' f'

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

' 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(">", "") note = body.get("note", [""])[0].strip() tags = body.get("tags", [""])[0].strip() 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://") 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 "block" in error_msg or "cloudflare" in error_msg or "403" in error_msg: 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): url = clean_url(body.get("url", [""])[0].strip()) note = body.get("note", [""])[0].strip() tags = body.get("tags", [""])[0].strip() manual_title = body.get("manual_title", [""])[0].strip() manual_desc = body.get("manual_description", [""])[0].strip() if not url: return handle_add_form("URL is required.") if not manual_title: return handle_add_form("Title is required for manual entry.") db = get_db() try: now = __import__("datetime").datetime.now().strftime("%Y-%m-%dT%H:%M:%S") db.execute( "INSERT INTO pages (url, title, body, note, last_modified, summary) VALUES (?, ?, ?, ?, ?, ?) " "ON CONFLICT(url) DO UPDATE SET title=excluded.title, body=excluded.body, " "note=excluded.note, last_modified=excluded.last_modified, summary=excluded.summary", (url, manual_title, manual_desc, note, now, manual_desc[:200]), ) page_id = db.execute("SELECT id FROM pages WHERE url = ?", (url,)).fetchone()[0] if tags: _set_page_tags(page_id, tags, db) db.commit() if get_setting("semantic_search", "0") == "1": try: from embeddings import store_embeddings store_embeddings(page_id, manual_title, manual_desc, db) db.commit() except Exception as e: print(f"Error generating embeddings: {e}") return handle_add_form(f'Added manually: {esc(manual_title)}') 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 "" page = _paginate(query or {}) offset = (page - 1) * BROWSE_PER_PAGE db = get_db() try: total = db.execute("SELECT count(*) FROM pages").fetchone()[0] rows = db.execute( "SELECT id, url, title, note FROM pages ORDER BY id DESC LIMIT ? OFFSET ?", (BROWSE_PER_PAGE, offset), ).fetchall() items = "" for r in rows: note_html = f' — {esc(r["note"])}' if r["note"] else "" tags = _get_page_tags(r["id"], db) tags_html = "" if tags: tag_links = " ".join(f'[{esc(t)}]' for t in tags) tags_html = f' {tag_links}' items += ( f'
  • {note_html}{tags_html} ' f'({esc(r["url"])}) ' f'edit ' f'remove
  • ' ) finally: return_db(db) return _respond( f"

    indexed pages ({total})

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

    ' f"" f'{_page_nav(page, total, "/pages", BROWSE_PER_PAGE)}' f'
    bulk actions' f'

    ' f'

    ' f' ' f'

    ' f'
    ' f'
    ' f'' f'

    export | import

    ' f'back' ) def _render_bulk_delete_confirm(page_ids): db = get_db() try: placeholders = ",".join("?" * len(page_ids)) rows = db.execute( f"SELECT id, url, title FROM pages WHERE id IN ({placeholders})", page_ids, ).fetchall() finally: return_db(db) if not rows: return _redirect("/pages") items = "".join( f'
  • {esc(r["title"] or r["url"])}
    ' f'{esc(r["url"])}
  • ' for r in rows ) hidden_ids = "".join( f'' for r in rows ) n = len(rows) return _respond( f"

    confirm delete

    " f"

    Remove the following {n} page{'' if n == 1 else 's'}?

    " f"" f'
    ' f'{_csrf_field()}' f'{hidden_ids}' f'' f'' f'' f"
    " f' cancel' ) def handle_bulk_action(body): ids = body.get("ids", []) action = body.get("action", [""])[0] if not ids: return _redirect("/pages") try: page_ids = [int(i) for i in ids] except ValueError: return _error(400) if action == "delete" and body.get("confirmed", [""])[0] != "1": return _render_bulk_delete_confirm(page_ids) db = get_db() try: if action == "delete": for pid in page_ids: db.execute("DELETE FROM page_tags WHERE page_id = ?", (pid,)) db.execute("DELETE FROM links WHERE page_id = ?", (pid,)) db.execute("DELETE FROM pages WHERE id = ?", (pid,)) _cleanup_orphaned_tags(db) db.commit() elif action == "retag": bulk_tags = body.get("bulk_tags", [""])[0].strip() tag_mode = body.get("tag_mode", ["add"])[0] if bulk_tags: for pid in page_ids: if tag_mode == "add": existing = _get_page_tags(pid, db) new_tags = [t.strip().lower() for t in bulk_tags.split(",") if t.strip()] merged = ", ".join(sorted(set(existing + new_tags))) _set_page_tags(pid, merged, db) else: _set_page_tags(pid, bulk_tags, db) _cleanup_orphaned_tags(db) db.commit() finally: return_db(db) return _redirect("/pages") def handle_edit_form(page_id, msg=""): db = get_db() try: row = db.execute("SELECT id, url, title, body, note, summary FROM pages WHERE id = ?", (page_id,)).fetchone() if not row: return _error(404) tags = ", ".join(_get_page_tags(page_id, db)) finally: return_db(db) return _respond( f"

    edit page

    " f"

    {esc(row['title'])}
    " f"{esc(row['url'])}

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

    ' f'
    ' f'

    ' f'
    ' f'

    ' f'
    ' f' ' f'(tag: private to keep private)

    ' f'' f"
    " f"

    {msg}

    " f'back' ) def handle_edit_submit(page_id, body): title = body.get("title", [""])[0].strip() summary = body.get("summary", [""])[0].strip() note = body.get("note", [""])[0].strip() tags = body.get("tags", [""])[0].strip() db = get_db() try: db.execute( "UPDATE pages SET title = ?, summary = ?, note = ? WHERE id = ?", (title, summary, note, page_id) ) _set_page_tags(page_id, tags, db) _cleanup_orphaned_tags(db) db.commit() finally: return_db(db) return _redirect("/pages") def handle_delete_confirm(page_id): db = get_db() try: row = db.execute("SELECT id, url, title FROM pages WHERE id = ?", (page_id,)).fetchone() finally: return_db(db) if not row: return _error(404) return _respond( f"

    confirm delete

    " f"

    Remove {esc(row['title'])}
    " f"{esc(row['url'])}

    " f'
    ' f'{_csrf_field()}' f'' f"
    " f' cancel' ) def handle_delete(page_id): db = get_db() try: db.execute("DELETE FROM page_tags WHERE page_id = ?", (page_id,)) db.execute("DELETE FROM links WHERE page_id = ?", (page_id,)) db.execute("DELETE FROM pages WHERE id = ?", (page_id,)) _cleanup_orphaned_tags(db) db.commit() finally: return_db(db) return _redirect("/pages") def handle_bookmark(query): token = query.get("token", [""])[0] expected = _get_bookmark_token() if not token or not secrets.compare_digest(token, expected): return _text_response("error: invalid or missing token", status=403, headers={"Access-Control-Allow-Origin": "*"}) url = clean_url(query.get("url", [""])[0].strip()) if not url or not url.startswith(("http://", "https://")): return _text_response("error: invalid url", headers={"Access-Control-Allow-Origin": "*"}) try: title = index_url(url) msg = f"ok: {title}" except Exception as e: msg = f"error: {e}" return _text_response(msg, headers={"Access-Control-Allow-Origin": "*"})