367 lines
16 KiB
Python
367 lines
16 KiB
Python
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 .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"):
|
|
template = get_setting("custom_template") or DEFAULT_TEMPLATE
|
|
name = get_site_name()
|
|
sharing = get_setting("sharing_enabled", "0")
|
|
checked = " checked" if sharing == "1" else ""
|
|
sharing_mode = get_setting("sharing_mode", "exclude_private")
|
|
forum = get_setting("forum_enabled", "0")
|
|
forum_checked = " checked" if forum == "1" else ""
|
|
exclude_checked = " checked" if sharing_mode != "require_public" else ""
|
|
require_checked = " checked" if sharing_mode == "require_public" else ""
|
|
shared_count = _count_shared_pages()
|
|
semantic = get_setting("semantic_search", "0")
|
|
semantic_checked = " checked" if semantic == "1" else ""
|
|
reranker = get_setting("use_reranker", "0")
|
|
reranker_checked = " checked" if reranker == "1" else ""
|
|
disabled = "" if semantic == "1" else " disabled"
|
|
tcp_enabled = get_setting("tcp_enabled", "1")
|
|
tcp_checked = " checked" if tcp_enabled == "1" else ""
|
|
tcp_disabled = "" if tcp_enabled == "1" else " disabled"
|
|
transport_host = get_setting("transport_host", "rnode.bre.land")
|
|
transport_port = get_setting("transport_port", "4242")
|
|
compress = get_setting("compress_embeddings", "0")
|
|
compress_checked = " checked" if compress == "1" else ""
|
|
lora_enabled = get_setting("lora_enabled", "0")
|
|
lora_checked = " checked" if lora_enabled == "1" else ""
|
|
lora_disabled = "" if lora_enabled == "1" else " disabled"
|
|
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"<section id=\"forum\">"
|
|
f'<form method="post" action="/style">'
|
|
f"{csrf}"
|
|
f'<input type="hidden" name="_action" value="forum">'
|
|
f"<h2>forum</h2>"
|
|
f'<label><input type="checkbox" name="forum_enabled" value="1"{forum_checked}>'
|
|
f" enable forum (shared URL discussion board)</label><br>"
|
|
f"<small>Share URLs and discuss them with other TinyWeb instances.</small><br><br>"
|
|
f'<input type="submit" value="save forum">'
|
|
f"</form>"
|
|
f"</section>"
|
|
)
|
|
forum_nav = ' · <a href="#forum">forum</a>'
|
|
else:
|
|
forum_body = ""
|
|
forum_nav = ""
|
|
|
|
msg = _get_flash() or msg
|
|
msg_html = ""
|
|
if msg:
|
|
msg_html = '<p><em>{msg}</em></p>'.format(msg=esc(msg))
|
|
|
|
return _respond(
|
|
f"<h1>customize</h1>"
|
|
f"{msg_html}"
|
|
f'<nav>'
|
|
f'<a href="#site-name">site name</a>'
|
|
f' · <a href="#sharing">sharing</a>'
|
|
f'{forum_nav}'
|
|
f' · <a href="#search">search</a>'
|
|
f' · <a href="#mesh">mesh</a>'
|
|
f' · <a href="#template">template</a>'
|
|
f' · <a href="#tools">tools</a>'
|
|
f'</nav>'
|
|
f"<hr>"
|
|
f"<section id=\"site-name\">"
|
|
f'<form method="post" action="/style">'
|
|
f"{csrf}"
|
|
f'<input type="hidden" name="_action" value="name">'
|
|
f"<h2>site name</h2>"
|
|
f'<input name="site_name" value="{esc(name)}" placeholder="tinyweb" size="30">'
|
|
f' <input type="submit" value="save name">'
|
|
f"</form>"
|
|
f"</section>"
|
|
f"<hr>"
|
|
f"<section id=\"sharing\">"
|
|
f'<form method="post" action="/style">'
|
|
f"{csrf}"
|
|
f'<input type="hidden" name="_action" value="sharing">'
|
|
f"<h2>sharing</h2>"
|
|
f'<label><input type="checkbox" name="sharing_enabled" value="1"{checked}>'
|
|
f" share your site list publicly at /api/sites</label><br>"
|
|
f'<div style="margin-top:0.6rem">'
|
|
f"<small>What to share:</small><br>"
|
|
f'<label><input type="radio" name="sharing_mode" value="exclude_private"{exclude_checked}>'
|
|
f' share all pages except those tagged <code>private</code></label><br>'
|
|
f'<label><input type="radio" name="sharing_mode" value="require_public"{require_checked}>'
|
|
f' share only pages tagged <code>public</code></label><br>'
|
|
f"</div>"
|
|
f'<p style="margin-top:0.6rem">'
|
|
f'Currently sharing <b>{shared_count}</b> page(s). '
|
|
f'<a href="/share/preview">preview</a>'
|
|
f"</p>"
|
|
f'<input type="submit" value="save sharing">'
|
|
f"</form>"
|
|
f"</section>"
|
|
f"<hr>"
|
|
f"{forum_body}"
|
|
f"<hr>"
|
|
f"<section id=\"search\">"
|
|
f'<form method="post" action="/style">'
|
|
f"{csrf}"
|
|
f'<input type="hidden" name="_action" value="search">'
|
|
f"<h2>search</h2>"
|
|
f'<label><input type="checkbox" name="semantic_search" value="1"{semantic_checked}>'
|
|
f" semantic search</label><br><br>"
|
|
f'<label><input type="checkbox" name="use_reranker" value="1"{reranker_checked}{disabled}>'
|
|
f" cross-encoder reranking</label><br><br>"
|
|
f'<label><input type="checkbox" name="compress_embeddings" value="1"{compress_checked}{disabled}>'
|
|
f" compress embeddings</label><br><br>"
|
|
f'<a href="/reindex">manage semantic index</a>'
|
|
f'<br><input type="submit" value="save search">'
|
|
f"</form>"
|
|
f"</section>"
|
|
f"<hr>"
|
|
f"<section id=\"mesh\">"
|
|
f'<form method="post" action="/style">'
|
|
f"{csrf}"
|
|
f'<input type="hidden" name="_action" value="mesh">'
|
|
f"<h2>mesh network</h2>"
|
|
f"<p>Choose how to connect to the mesh.</p>"
|
|
f"<h3>internet</h3>"
|
|
f'<label><input type="checkbox" name="tcp_enabled" value="1"{tcp_checked}>'
|
|
f" connect via internet transport node</label><br>"
|
|
f"<small>Default: rnode.bre.land:4242</small><br>"
|
|
f'<input name="transport_host" value="{esc(transport_host)}" size="30"{tcp_disabled}>'
|
|
f' <input name="transport_port" value="{esc(transport_port)}" size="6"{tcp_disabled}><br>'
|
|
f"<h3>LoRa</h3>"
|
|
f'<label><input type="checkbox" name="lora_enabled" value="1"{lora_checked}>'
|
|
f" connect via LoRa radio</label><br>"
|
|
f'<label>Serial port: <input id="lora-port" name="lora_port" value="{esc(lora_port)}" '
|
|
f'placeholder="/dev/ttyUSB0" size="20"{lora_disabled}></label><br>'
|
|
f"<details><summary>advanced radio settings</summary>"
|
|
f'<label>Frequency (Hz): <input name="lora_frequency" value="{esc(lora_frequency)}" size="12"{lora_disabled}></label><br>'
|
|
f"<small>ISM band frequency. Default: 867200000 (868 MHz EU). US: 915000000.</small><br><br>"
|
|
f'<label>Bandwidth (Hz): <input name="lora_bandwidth" value="{esc(lora_bandwidth)}" size="8"{lora_disabled}></label><br>'
|
|
f"<small>Default: 125000</small><br><br>"
|
|
f'<label>TX Power (dBm): <input name="lora_txpower" value="{esc(lora_txpower)}" size="4"{lora_disabled}></label><br>'
|
|
f"<small>0-17 typical. Check local regulations.</small><br><br>"
|
|
f'<label>Spreading Factor: <input name="lora_sf" value="{esc(lora_sf)}" size="4"{lora_disabled}></label><br>'
|
|
f"<small>5-12. Higher = longer range, slower speed.</small><br><br>"
|
|
f'<label>Coding Rate: <input name="lora_cr" value="{esc(lora_cr)}" size="4"{lora_disabled}></label><br>'
|
|
f"<small>5-8. Higher = more error correction.</small>"
|
|
f"</details>"
|
|
f'<br><input type="submit" value="save mesh">'
|
|
f"</form>"
|
|
f"</section>"
|
|
f"<hr>"
|
|
f"<section id=\"template\">"
|
|
f"<h2>template</h2>"
|
|
f"<p>Edit the full page template.</p>"
|
|
f'<form method="post" action="/style/template">'
|
|
f"{csrf}"
|
|
f'<textarea name="template" rows="12" cols="60" style="width:100%">{esc(template)}</textarea><br><br>'
|
|
f'<input type="submit" value="save template">'
|
|
f"</form>"
|
|
f"</section>"
|
|
f"<hr>"
|
|
f"<section id=\"tools\">"
|
|
f"<h2>tools</h2>"
|
|
f"<h3>bookmarklet</h3>"
|
|
f"<p>Drag this link to your bookmarks bar.</p>"
|
|
f'<p><a href="javascript:void(fetch(\'{esc(scheme)}://{esc(gateway_host or "localhost:8080")}/bookmark?url=\'+encodeURIComponent(location.href)+\'&token={_get_bookmark_token()}\').then(r=>r.text()).then(t=>alert(t)).catch(()=>alert(\'tinyweb not running\')))">+ save to {esc(name)}</a></p>'
|
|
f"<h3>reset template</h3>"
|
|
f'<form method="post" action="/style/reset">'
|
|
f"{csrf}"
|
|
f'<input type="submit" value="reset to default">'
|
|
f"</form>"
|
|
f"<h3>vacuum database</h3>"
|
|
f'<form method="post" action="/style/vacuum">'
|
|
f"{csrf}"
|
|
f'<input type="submit" value="vacuum">'
|
|
f"</form>"
|
|
f"</section>"
|
|
f'<a href="/">back</a>',
|
|
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"):
|
|
template = body.get("template", [""])[0].replace("\r\n", "\n").replace("\r", "\n")
|
|
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":
|
|
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":
|
|
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
|
|
templates_mod.FORUM_ENABLED = (value == "1")
|
|
set_setting(key, value)
|
|
return _json_response({"status": "ok", "message": ""})
|
|
|
|
|
|
def handle_about():
|
|
name = get_site_name()
|
|
dest_hash = get_setting("dest_hash")
|
|
sharing = get_setting("sharing_enabled", "0") == "1"
|
|
db = get_db()
|
|
try:
|
|
page_count = db.execute("SELECT count(*) FROM pages").fetchone()[0]
|
|
tag_count = db.execute("SELECT count(DISTINCT tag_id) FROM page_tags").fetchone()[0]
|
|
sub_count = db.execute("SELECT count(*) FROM subscriptions").fetchone()[0]
|
|
finally:
|
|
return_db(db)
|
|
|
|
sharing_html = (
|
|
'<p>This instance shares its index publicly. Subscribe to join the network.</p>'
|
|
if sharing else
|
|
'<p>This instance is private.</p>'
|
|
)
|
|
|
|
hash_html = ""
|
|
if dest_hash:
|
|
hash_html = (
|
|
f'<h2>subscribe</h2>'
|
|
f'<p>To subscribe to this instance, add this destination hash in your TinyWeb:</p>'
|
|
f'<pre>{esc(dest_hash)}</pre>'
|
|
)
|
|
|
|
return _respond(
|
|
f'<h1>{esc(name)}</h1>'
|
|
f'<p>A personal, decentralized search engine.</p>'
|
|
f'<p>You save pages you find. They are stored locally and shared over a mesh network '
|
|
f'so other people can find them too.</p>'
|
|
f'<p>Search results come from your index and the indexes of people you are connected to.</p>'
|
|
f'<ul>'
|
|
f'<li><b>{page_count}</b> page(s) indexed</li>'
|
|
f'<li><b>{tag_count}</b> tag(s)</li>'
|
|
f'<li><b>{sub_count}</b> subscription(s)</li>'
|
|
f'</ul>'
|
|
f'{sharing_html}'
|
|
f'{hash_html}'
|
|
f'<h2>your data</h2>'
|
|
f'<p>Everything is stored locally under <code>~/.tinyweb/</code>:</p>'
|
|
f'<ul>'
|
|
f'<li><code>tinyweb_identity</code> — your permanent mesh identity. '
|
|
f'If you lose this file, your destination hash changes and subscribers '
|
|
f'have to re-subscribe to the new one.</li>'
|
|
f'<li><code>index.db</code> — your full reading history: every page, '
|
|
f'note, tag, and synced remote page.</li>'
|
|
f'<li><code>models/</code> — the semantic search model if you enabled it '
|
|
f'(redownloadable, safe to delete).</li>'
|
|
f'</ul>'
|
|
f'<p><b>Back up <code>~/.tinyweb/</code> periodically.</b> '
|
|
f'Copying the whole directory to another device preserves your identity and index together. '
|
|
f'The <a href="/export">export</a> page gives you a JSON dump of pages only — '
|
|
f'it does not preserve your identity or subscription state, so it is a migration aid, '
|
|
f'not a substitute for a full backup.</p>'
|
|
f'<p><a href="/">search</a> | <a href="/pages">browse</a> | <a href="/tags">tags</a></p>'
|
|
)
|