'
)
return _respond(body)
rows = ""
for s in sites:
tags_html = ""
if s["tags"]:
tags_html = " " + " ".join(f"[{esc(t)}]" for t in s["tags"])
note_html = f' — {esc(s["note"])}' if s["note"] else ""
rows += (
f'
'
)
return _respond(body)
def handle_api_sites(query=None):
if get_setting("sharing_enabled", "0") != "1":
return _json_response(
{"error": "sharing disabled"},
status=403,
headers={"Access-Control-Allow-Origin": "*"},
)
since = (query or {}).get("since", [""])[0].strip()
db = get_db()
try:
sites = _shared_sites(db, since=since)
all_urls = _shared_all_urls(db) if not since else None
finally:
return_db(db)
data = {"name": get_site_name(), "sites": sites}
if all_urls is not None:
data["all_urls"] = all_urls
return _json_response(data, headers={"Access-Control-Allow-Origin": "*"})
def handle_subscriptions(msg=""):
db = get_db()
try:
subs = db.execute("SELECT * FROM subscriptions ORDER BY id DESC").fetchall()
finally:
return_db(db)
cards = ""
for s in subs:
sub_id = s["id"]
auto_label = "on" if s["auto_sync"] else "off"
last = s["last_sync"] or "never"
sync_status = get_setting(f"sync_status_{sub_id}", "")
is_syncing = sub_id in _sync_threads and _sync_threads[sub_id].is_alive()
if is_syncing:
status_html = '
syncing...
'
elif sync_status.startswith("error:"):
err_msg = sync_status[6:]
status_html = f'
{esc(err_msg)}
'
else:
status_html = ""
if is_syncing:
sync_btn = ''
else:
sync_btn = (
f''
)
cards += (
f'
'
f'{listing}'
f' back',
head_html=head_html,
)
def handle_subscription_add(body):
dest_hash = body.get("dest_hash", [""])[0].strip().replace("<", "").replace(">", "")
if not dest_hash or len(dest_hash) != 32:
return handle_subscriptions("Enter a valid 32-character destination hash.")
try:
int(dest_hash, 16)
except ValueError:
return handle_subscriptions("Invalid destination hash (must be hex).")
try:
data = fetch_remote_sites(dest_hash)
name = data.get("name", "")
except PermissionError:
return handle_subscriptions("That instance has sharing disabled.")
except Exception:
return handle_subscriptions("Could not reach that instance.")
db = get_db()
try:
db.execute(
"INSERT INTO subscriptions (dest_hash, name) VALUES (?, ?) "
"ON CONFLICT(dest_hash) DO UPDATE SET name=excluded.name",
(dest_hash, name),
)
db.commit()
finally:
return_db(db)
return handle_subscriptions(f"Subscribed to {esc(name or dest_hash)}.")
def handle_subscription_browse(sub_id):
db = get_db()
try:
sub = db.execute("SELECT * FROM subscriptions WHERE id = ?", (sub_id,)).fetchone()
if not sub:
return _error(404)
local_urls = set(r["url"] for r in db.execute("SELECT url FROM pages LIMIT ?", (MAX_BROWSE,)).fetchall())
remote_rows = db.execute(
"SELECT url, title, note, tags FROM remote_pages WHERE subscription_id = ? LIMIT ?",
(sub_id, MAX_BROWSE),
).fetchall()
finally:
return_db(db)
if remote_rows:
sites = []
for r in remote_rows:
tags = [t for t in r["tags"].split(",") if t] if r["tags"] else []
sites.append({"url": r["url"], "title": r["title"], "note": r["note"], "tags": tags})
else:
try:
data = fetch_remote_sites(sub["dest_hash"])
sites = data.get("sites", [])
except PermissionError:
return handle_subscriptions("That instance has sharing disabled.")
except Exception:
return handle_subscriptions("Could not fetch sites from that instance.")
new_items = ""
existing_items = ""
new_count = 0
for s in sites:
if s["url"] in local_urls:
existing_items += (
f'
'
)
else:
new_count += 1
note_html = f' — {esc(s["note"])}' if s.get("note") else ""
tags_html = ""
if s.get("tags"):
tags_html = " " + " ".join(f'[{esc(t)}]' for t in s["tags"])
new_items += (
f''
)
buttons = ""
if new_count:
buttons = ' '
return _respond(
f'
browsing: {esc(sub["name"] or sub["dest_hash"])}
'
f'
{len(sites)} site(s) available, {new_count} new
'
f''
f'
already indexed
{existing_items}
'
f'back'
)
def handle_subscription_pick(body):
sub_id = body.get("sub_id", [""])[0]
import_all = body.get("import_all", [""])[0]
db = get_db()
try:
remote_rows = db.execute(
"SELECT url, tags FROM remote_pages WHERE subscription_id = ?", (sub_id,)
).fetchall()
remote_tags = {r["url"]: r["tags"] for r in remote_rows}
if import_all:
local_urls = set(r["url"] for r in db.execute("SELECT url FROM pages LIMIT ?", (MAX_BROWSE,)).fetchall())
urls = [r["url"] for r in remote_rows if r["url"] not in local_urls]
else:
urls = body.get("urls", [])
finally:
return_db(db)
if not urls:
return handle_subscriptions("No sites selected.")
imported = 0
errors = 0
for url in urls:
try:
index_url(url)
tags_str = remote_tags.get(url, "")
if tags_str:
db = get_db()
try:
row = db.execute("SELECT id FROM pages WHERE url = ?", (url,)).fetchone()
if row:
_set_page_tags(row["id"], tags_str, db)
db.commit()
finally:
return_db(db)
imported += 1
except Exception:
errors += 1
return handle_subscriptions(f"Imported {imported} page(s). {errors} error(s).")
def _sync_subscription(sub_id):
set_setting(f"sync_status_{sub_id}", "syncing")
db = get_db()
try:
sub = db.execute("SELECT * FROM subscriptions WHERE id = ?", (sub_id,)).fetchone()
if not sub:
set_setting(f"sync_status_{sub_id}", "error:Subscription not found.")
return
since = sub["last_sync"].replace(" ", "T") if sub["last_sync"] else ""
try:
data = fetch_remote_sites(sub["dest_hash"], since=since)
sites = data.get("sites", [])
all_urls = data.get("all_urls")
remote_name = data.get("name", sub["name"])
except PermissionError:
set_setting(f"sync_status_{sub_id}", "error:That instance has sharing disabled.")
return
except Exception as e:
set_setting(f"sync_status_{sub_id}", f"error:Could not sync \u2014 {e}")
return
if all_urls is not None:
existing = db.execute(
"SELECT id, url FROM remote_pages WHERE subscription_id = ?", (sub_id,)
).fetchall()
remote_url_set = set(all_urls)
for row in existing:
if row["url"] not in remote_url_set:
db.execute("DELETE FROM remote_pages WHERE id = ?", (row["id"],))
synced = 0
for s in sites:
try:
tags_str = ",".join(s.get("tags", []))
db.execute(
"INSERT INTO remote_pages (subscription_id, url, title, note, tags) VALUES (?, ?, ?, ?, ?) "
"ON CONFLICT(subscription_id, url) DO UPDATE SET title=excluded.title, note=excluded.note, tags=excluded.tags",
(sub_id, s["url"], s["title"], s.get("note", ""), tags_str),
)
if get_setting("semantic_search", "0") == "1":
try:
from embeddings import store_remote_embeddings
rp_id = db.execute(
"SELECT id FROM remote_pages WHERE subscription_id = ? AND url = ?",
(sub_id, s["url"]),
).fetchone()["id"]
store_remote_embeddings(rp_id, s["title"], s.get("note", ""), db)
except Exception:
pass
synced += 1
except Exception:
pass
now = datetime.now().strftime("%Y-%m-%dT%H:%M:%S")
db.execute("UPDATE subscriptions SET last_sync = ?, name = ? WHERE id = ?", (now, remote_name, sub_id))
db.commit()
set_setting(f"sync_status_{sub_id}", f"done:{synced}")
except Exception as e:
set_setting(f"sync_status_{sub_id}", f"error:{e}")
finally:
return_db(db)
def handle_subscription_sync(sub_id):
if sub_id in _sync_threads and _sync_threads[sub_id].is_alive():
return _redirect("/subscriptions")
set_setting(f"sync_status_{sub_id}", "syncing")
t = threading.Thread(target=_sync_subscription, args=(sub_id,), daemon=True)
_sync_threads[sub_id] = t
t.start()
return _redirect("/subscriptions")
def handle_subscription_autosync(sub_id):
db = get_db()
try:
db.execute("UPDATE subscriptions SET auto_sync = 1 - auto_sync WHERE id = ?", (sub_id,))
db.commit()
finally:
return_db(db)
return _redirect("/subscriptions")
def handle_subscription_delete(sub_id):
db = get_db()
try:
db.execute("DELETE FROM remote_pages WHERE subscription_id = ?", (sub_id,))
db.execute("DELETE FROM subscriptions WHERE id = ?", (sub_id,))
db.commit()
finally:
return_db(db)
return _redirect("/subscriptions")
def handle_subscription_syncall():
db = get_db()
try:
subs = db.execute("SELECT * FROM subscriptions WHERE auto_sync = 1").fetchall()
finally:
return_db(db)
if not subs:
return handle_subscriptions("No subscriptions have auto-sync enabled.")
for sub in subs:
sub_id = sub["id"]
if sub_id in _sync_threads and _sync_threads[sub_id].is_alive():
continue
set_setting(f"sync_status_{sub_id}", "syncing")
t = threading.Thread(target=_sync_subscription, args=(sub_id,), daemon=True)
_sync_threads[sub_id] = t
t.start()
return _redirect("/subscriptions")