diff --git a/Dockerfile b/Dockerfile
index 3f73263..713df67 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -13,7 +13,9 @@ RUN pip install --no-cache-dir -r requirements.txt
COPY . .
-RUN mkdir -p /data
+RUN mkdir -p /data \
+ && ln -sf /data/index.db index.db \
+ && ln -sf /data/tinyweb_identity tinyweb_identity
ENV PYTHONUNBUFFERED=1
diff --git a/README.md b/README.md
index 991c981..2405ee0 100644
--- a/README.md
+++ b/README.md
@@ -105,6 +105,14 @@ tmux new-session -d -s tinyweb 'python app.py'
nohup python app.py &
```
+### Forum plugin (optional)
+
+```bash
+pip install tinyweb-forum
+```
+
+Enable the forum on the `/style` page. See the [tinyweb-forum README](https://codeberg.org/tinyweb/tinyweb-forum) for details.
+
### Docker
A `docker-compose.yml` is included for containerized setups. Build and run:
diff --git a/app.py b/app.py
index 035eca0..6e70196 100644
--- a/app.py
+++ b/app.py
@@ -16,7 +16,7 @@ from gateway import GatewayState, GatewayHandler
IDENTITY_FILE = "tinyweb_identity"
DEFAULT_TRANSPORT_HOST = "rnode.bre.land"
DEFAULT_TRANSPORT_PORT = 4242
-DATA_DIR = os.environ.get("TINYWEB_DATA_DIR") or os.path.expanduser("~/.tinyweb")
+DATA_DIR = os.path.expanduser("~/.tinyweb")
def get_transport_config():
diff --git a/db.py b/db.py
index 97378d9..ec13254 100644
--- a/db.py
+++ b/db.py
@@ -6,7 +6,7 @@ import os
from urllib.parse import urlparse, urljoin, parse_qs, urlencode, urlunparse, quote
from bs4 import BeautifulSoup
-DATA_DIR = os.environ.get("TINYWEB_DATA_DIR") or os.path.expanduser("~/.tinyweb")
+DATA_DIR = os.path.expanduser("~/.tinyweb")
DATABASE = os.path.join(DATA_DIR, "index.db")
BLOCKED_NETWORKS = [
diff --git a/entrypoint.sh b/entrypoint.sh
index b741c4c..7385146 100755
--- a/entrypoint.sh
+++ b/entrypoint.sh
@@ -29,7 +29,6 @@ if [ ! -f "$CONFIG_FILE" ]; then
EOF
fi
-export TINYWEB_DATA_DIR="/data"
export RNS_CONFIG_DIR="$CONFIG_DIR"
# Bind to 0.0.0.0 inside the container; isolation is handled by Docker's port mapping.
exec python app.py --bind 0.0.0.0 "$@"
diff --git a/gateway.py b/gateway.py
index fa4b076..7d3fd53 100644
--- a/gateway.py
+++ b/gateway.py
@@ -125,7 +125,6 @@ class GatewayHandler(BaseHTTPRequestHandler):
"body": body,
"cookies": cookies,
"gateway_host": self.headers.get("Host", f"localhost:{GATEWAY_PORT}"),
- "scheme": self.headers.get("X-Forwarded-Proto", "http"),
}
try:
@@ -165,15 +164,12 @@ class GatewayHandler(BaseHTTPRequestHandler):
"style-src 'self' 'unsafe-inline'; "
"script-src 'self' 'unsafe-inline'; "
"img-src 'self' data:")
- resp_body = resp.get("body", "")
- encoded = resp_body.encode() if isinstance(resp_body, str) else resp_body
- if encoded:
- self.send_header("Content-Length", str(len(encoded)))
for k, v in resp.get("headers", {}).items():
self.send_header(k, v)
self.end_headers()
- if encoded:
- self.wfile.write(encoded)
+ resp_body = resp.get("body", "")
+ if resp_body:
+ self.wfile.write(resp_body.encode() if isinstance(resp_body, str) else resp_body)
except ConnectionError as e:
GatewayState.link = None
diff --git a/handlers/__init__.py b/handlers/__init__.py
index 228508e..95d1753 100644
--- a/handlers/__init__.py
+++ b/handlers/__init__.py
@@ -32,7 +32,7 @@ from .subscriptions import (
handle_subscription_delete, handle_subscription_syncall,
_sync_threads,
)
-from .customize import handle_style_form, handle_style_submit, handle_style_template_submit, handle_field_save, handle_about, _set_flash
+from .customize import handle_style_form, handle_style_submit, handle_about
from .tags import handle_tags, handle_tag_browse
from .data import (
handle_export, handle_import_form, handle_import_submit,
@@ -48,7 +48,6 @@ def _dispatch_inner(data):
query = data.get("query", {})
body = data.get("body", {})
gateway_host = data.get("gateway_host", "")
- scheme = data.get("scheme", "http")
def extract_id(prefix):
try:
@@ -60,20 +59,24 @@ def _dispatch_inner(data):
if path == "/":
return handle_search(query)
elif path == "/add":
+ action_type = query.get("type", ["index"])[0]
prefill_url = query.get("url", [""])[0].strip()
- return handle_add_form(prefill_url=prefill_url)
+ return handle_add_form(
+ action_type=action_type if action_type == "subscribe" else "index",
+ prefill_url=prefill_url,
+ )
elif path == "/pages":
return handle_pages(query)
elif path.startswith("/edit/"):
pid = extract_id("/edit/")
- return handle_edit_form(pid, page=query.get("p", [""])[0]) if pid is not None else _error(400)
+ return handle_edit_form(pid) if pid is not None else _error(400)
elif path.startswith("/delete/"):
pid = extract_id("/delete/")
return handle_delete_confirm(pid) if pid is not None else _error(400)
elif path == "/bookmark":
return handle_bookmark(query)
elif path == "/style":
- return handle_style_form(gateway_host=gateway_host, scheme=scheme)
+ return handle_style_form()
elif path == "/share/preview":
return handle_share_preview()
elif path == "/about":
@@ -93,8 +96,6 @@ def _dispatch_inner(data):
return handle_api_sites(query)
elif path == "/subscriptions":
return handle_subscriptions()
- elif path == "/subscriptions/add":
- return handle_add_form(action_type="subscribe")
elif path.startswith("/subscriptions/browse/"):
sid = extract_id("/subscriptions/browse/")
return handle_subscription_browse(sid) if sid is not None else _error(400)
@@ -122,20 +123,14 @@ def _dispatch_inner(data):
pid = extract_id("/delete/")
return handle_delete(pid) if pid is not None else _error(400)
elif path == "/style":
- return handle_style_submit(body, gateway_host=gateway_host, scheme=scheme)
- elif path == "/style/template":
- return handle_style_template_submit(body, gateway_host=gateway_host, scheme=scheme)
- elif path == "/style/field":
- return handle_field_save(body)
+ return handle_style_submit(body)
elif path == "/style/reset":
set_setting("custom_template", "")
- _set_flash("Template reset to default.")
- return _redirect("/style")
+ return handle_style_form("Template reset to default.")
elif path == "/style/vacuum":
from db import vacuum_db
vacuum_db()
- _set_flash("Database vacuumed.")
- return _redirect("/style")
+ return handle_style_form("Database vacuumed.")
elif path == "/import":
return handle_import_submit(body)
elif path == "/reindex":
diff --git a/handlers/_helpers.py b/handlers/_helpers.py
index 2611ce7..3fecb71 100644
--- a/handlers/_helpers.py
+++ b/handlers/_helpers.py
@@ -65,11 +65,11 @@ def _get_bookmark_token():
return token
-def _respond(body_html, status=200, use_default=False, head_html=""):
+def _respond(body_html, status=200, use_default=False):
return {
"status": status,
"content_type": "text/html; charset=utf-8",
- "body": wrap_page(body_html, use_default=use_default, head_html=head_html),
+ "body": wrap_page(body_html, use_default=use_default),
"headers": {},
}
diff --git a/handlers/customize.py b/handlers/customize.py
index 6b3786d..1f2568b 100644
--- a/handlers/customize.py
+++ b/handlers/customize.py
@@ -1,21 +1,11 @@
from db import get_db, return_db, get_setting, set_setting, get_site_name
import templates as templates_mod
from templates import esc, DEFAULT_TEMPLATE
-from ._helpers import _respond, _redirect, _json_response, _csrf_field, _get_bookmark_token, _request_local
+from ._helpers import _respond, _csrf_field, _get_bookmark_token
from .subscriptions import _count_shared_pages
-_flash = {}
-
-def _set_flash(msg):
- _flash[_request_local.csrf_token] = msg
-
-
-def _get_flash():
- return _flash.pop(_request_local.csrf_token, "")
-
-
-def handle_style_form(msg="", gateway_host="", scheme="http"):
+def handle_style_form(msg=""):
template = get_setting("custom_template") or DEFAULT_TEMPLATE
name = get_site_name()
sharing = get_setting("sharing_enabled", "0")
@@ -31,6 +21,7 @@ def handle_style_form(msg="", gateway_host="", scheme="http"):
reranker = get_setting("use_reranker", "0")
reranker_checked = " checked" if reranker == "1" else ""
disabled = "" if semantic == "1" else " disabled"
+ dimmed = ' style="opacity:0.4"' if semantic != "1" else ""
tcp_enabled = get_setting("tcp_enabled", "1")
tcp_checked = " checked" if tcp_enabled == "1" else ""
tcp_disabled = "" if tcp_enabled == "1" else " disabled"
@@ -41,65 +32,31 @@ def handle_style_form(msg="", gateway_host="", scheme="http"):
lora_enabled = get_setting("lora_enabled", "0")
lora_checked = " checked" if lora_enabled == "1" else ""
lora_disabled = "" if lora_enabled == "1" else " disabled"
+ lora_dimmed = ' style="opacity:0.4"' if lora_enabled != "1" else ""
lora_port = get_setting("lora_port", "")
lora_frequency = get_setting("lora_frequency", "867200000")
lora_bandwidth = get_setting("lora_bandwidth", "125000")
lora_txpower = get_setting("lora_txpower", "7")
lora_sf = get_setting("lora_sf", "8")
lora_cr = get_setting("lora_cr", "5")
- csrf = _csrf_field()
from handlers import forum_plugin as _fp
if _fp is not None:
- forum_body = (
- f"tinyweb-forum — "
+ f'more info.
'
)
- forum_nav = ' · forum'
else:
- forum_body = ""
- forum_nav = ""
-
- msg = _get_flash() or msg
- msg_html = ""
- if msg:
- msg_html = '
{msg}
'.format(msg=esc(msg)) - + forum_section = "" return _respond( f"Edit the full page template.
" - f'" - f"Drag this link to your bookmarks bar.
" - f'r.text()).then(t=>alert(t)).catch(()=>alert(\'tinyweb not running\')))">+ save to {esc(name)}
' - f"{msg}
" f'back', use_default=True, ) -def handle_style_submit(body, gateway_host="", scheme="http"): - action = body.get("_action", [""])[0] - - if action == "name": - name = body.get("site_name", ["tinyweb"])[0].strip() - set_setting("site_name", name or "tinyweb") - _set_flash("Saved.") - return _redirect("/style") - - if action == "sharing": - sharing = "1" if body.get("sharing_enabled") else "0" - sharing_mode = body.get("sharing_mode", ["exclude_private"])[0] - if sharing_mode not in ("exclude_private", "require_public"): - sharing_mode = "exclude_private" - set_setting("sharing_mode", sharing_mode) - set_setting("sharing_enabled", sharing) - _set_flash("Saved.") - return _redirect("/style") - - if action == "forum": - forum_enabled = "1" if body.get("forum_enabled") else "0" - current_forum = get_setting("forum_enabled", "0") - if forum_enabled != current_forum: - from handlers import forum_plugin - if forum_enabled == "1" and forum_plugin is None: - _set_flash("Forum plugin not installed. Run: pip install tinyweb-forum") - return _redirect("/style") - if forum_enabled == "1": - forum_plugin.enable() - try: - forum_plugin.fdb.set_setting("forum_enabled", "1") - except Exception: - pass - else: - forum_plugin.disable() - try: - forum_plugin.fdb.set_setting("forum_enabled", "0") - except Exception: - pass - set_setting("forum_enabled", forum_enabled) - templates_mod.FORUM_ENABLED = (forum_enabled == "1") - _set_flash("Saved.") - return _redirect("/style") - - if action == "search": - semantic = "1" if body.get("semantic_search") else "0" - reranker = "1" if body.get("use_reranker") else "0" - compress = "1" if body.get("compress_embeddings") else "0" - set_setting("semantic_search", semantic) - set_setting("use_reranker", reranker) - set_setting("compress_embeddings", compress) - _set_flash("Saved.") - return _redirect("/style") - - if action == "mesh": - tcp_enabled = "1" if body.get("tcp_enabled") else "0" - transport_host = body.get("transport_host", [""])[0].strip() - transport_port = body.get("transport_port", [""])[0].strip() - set_setting("tcp_enabled", tcp_enabled) - if transport_host: - set_setting("transport_host", transport_host) - if transport_port: - set_setting("transport_port", transport_port) - lora_enabled = "1" if body.get("lora_enabled") else "0" - set_setting("lora_enabled", lora_enabled) - set_setting("lora_port", body.get("lora_port", [""])[0].strip()) - set_setting("lora_frequency", body.get("lora_frequency", ["867200000"])[0].strip()) - set_setting("lora_bandwidth", body.get("lora_bandwidth", ["125000"])[0].strip()) - set_setting("lora_txpower", body.get("lora_txpower", ["7"])[0].strip()) - set_setting("lora_sf", body.get("lora_sf", ["8"])[0].strip()) - set_setting("lora_cr", body.get("lora_cr", ["5"])[0].strip()) - _set_flash("Saved.") - return _redirect("/style") - - -def handle_style_template_submit(body, gateway_host="", scheme="http"): +def handle_style_submit(body): template = body.get("template", [""])[0].replace("\r\n", "\n").replace("\r", "\n") + name = body.get("site_name", ["tinyweb"])[0].strip() + sharing = "1" if body.get("sharing_enabled") else "0" + sharing_mode = body.get("sharing_mode", ["exclude_private"])[0] + if sharing_mode not in ("exclude_private", "require_public"): + sharing_mode = "exclude_private" + set_setting("sharing_mode", sharing_mode) + semantic = "1" if body.get("semantic_search") else "0" + reranker = "1" if body.get("use_reranker") else "0" + compress = "1" if body.get("compress_embeddings") else "0" + tcp_enabled = "1" if body.get("tcp_enabled") else "0" + transport_host = body.get("transport_host", [""])[0].strip() + transport_port = body.get("transport_port", [""])[0].strip() set_setting("custom_template", template if template.strip() != DEFAULT_TEMPLATE.strip() else "") - _set_flash("Template saved.") - return _redirect("/style") - - -def handle_field_save(body): - key = body.get("key", [""])[0].strip() - value = body.get("value", [""])[0].strip() - if not key: - return _json_response({"status": "error", "message": "No key provided."}, 400) - if key == "forum_enabled": + set_setting("site_name", name or "tinyweb") + set_setting("sharing_enabled", sharing) + set_setting("semantic_search", semantic) + set_setting("use_reranker", reranker) + set_setting("compress_embeddings", compress) + set_setting("tcp_enabled", tcp_enabled) + if transport_host: + set_setting("transport_host", transport_host) + if transport_port: + set_setting("transport_port", transport_port) + lora_enabled = "1" if body.get("lora_enabled") else "0" + set_setting("lora_enabled", lora_enabled) + set_setting("lora_port", body.get("lora_port", [""])[0].strip()) + set_setting("lora_frequency", body.get("lora_frequency", ["867200000"])[0].strip()) + set_setting("lora_bandwidth", body.get("lora_bandwidth", ["125000"])[0].strip()) + set_setting("lora_txpower", body.get("lora_txpower", ["7"])[0].strip()) + set_setting("lora_sf", body.get("lora_sf", ["8"])[0].strip()) + set_setting("lora_cr", body.get("lora_cr", ["5"])[0].strip()) + forum_enabled = "1" if body.get("forum_enabled") else "0" + current_forum = get_setting("forum_enabled", "0") + if forum_enabled != current_forum: from handlers import forum_plugin - if value == "1" and forum_plugin is None: - return _json_response({"status": "error", "message": "Forum plugin not installed."}, 400) - if value == "1": + if forum_enabled == "1" and forum_plugin is None: + return handle_style_form( + "Forum plugin not installed. Run: pip install tinyweb-forum" + ) + if forum_enabled == "1": forum_plugin.enable() try: forum_plugin.fdb.set_setting("forum_enabled", "1") @@ -303,9 +206,9 @@ def handle_field_save(body): forum_plugin.fdb.set_setting("forum_enabled", "0") except Exception: pass - templates_mod.FORUM_ENABLED = (value == "1") - set_setting(key, value) - return _json_response({"status": "ok", "message": ""}) + set_setting("forum_enabled", forum_enabled) + templates_mod.FORUM_ENABLED = (forum_enabled == "1") + return handle_style_form("Saved. Restart TinyWeb for mesh network changes to take effect.") def handle_about(): diff --git a/handlers/pages.py b/handlers/pages.py index 0799251..3e213e0 100644 --- a/handlers/pages.py +++ b/handlers/pages.py @@ -83,7 +83,7 @@ def handle_add_submit(body): 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")): + if "block" in error_msg or "cloudflare" in error_msg or "403" in error_msg: return _respond( f"{esc(url)} blocks automated access. " @@ -172,7 +172,7 @@ def handle_pages(query=None): f'
{esc(row['title'])}
"
f"{esc(row['url'])}
{msg}
" - f'back' + f'back' ) @@ -338,11 +335,7 @@ def handle_edit_submit(page_id, body): finally: return_db(db) - page = body.get("_page", [""])[0].strip() - target = "/pages" - if page: - target += "?p=" + page - return _redirect(target) + return _redirect("/pages") def handle_delete_confirm(page_id): diff --git a/handlers/search.py b/handlers/search.py index d9ee7c1..967b59c 100644 --- a/handlers/search.py +++ b/handlers/search.py @@ -41,23 +41,6 @@ def handle_search(query): else: fused_ids = bm25_ids - # Also match by tag - search_terms = [w.lower() for w in q.split() if w] - if search_terms: - placeholders = ",".join("?" * len(search_terms)) - tag_rows = db.execute( - f"SELECT DISTINCT pt.page_id FROM page_tags pt " - f"JOIN tags t ON t.id = pt.tag_id " - f"WHERE LOWER(t.name) IN ({placeholders})", - search_terms, - ).fetchall() - tag_ids = {r["page_id"] for r in tag_rows} - seen = set(fused_ids) - for pid in tag_ids: - if pid not in seen: - fused_ids.append(pid) - seen.add(pid) - total_results = len(fused_ids) page_ids = fused_ids[offset:offset + PER_PAGE] diff --git a/handlers/subscriptions.py b/handlers/subscriptions.py index 97f20b2..b54a253 100644 --- a/handlers/subscriptions.py +++ b/handlers/subscriptions.py @@ -162,7 +162,7 @@ def handle_subscriptions(msg=""): sync_btn = '' else: sync_btn = ( - f'' ) @@ -173,22 +173,18 @@ def handle_subscriptions(msg=""): f'{msg}
' f'{name}' ' | search | browse' ' | tags | subscriptions' f'{forum_link}' ' | customize | about
\n' - "