update README to match TinyWeb style
This commit is contained in:
parent
ea34a7f5d7
commit
94afb56bc0
2 changed files with 308 additions and 175 deletions
|
|
@ -27,6 +27,7 @@ class ForumHandlers:
|
|||
self.reticulum = reticulum
|
||||
self.site_name = site_name
|
||||
self._request_local = threading.local()
|
||||
self._flash = {}
|
||||
|
||||
def _get_csrf(self):
|
||||
return getattr(self._request_local, 'csrf_token', '')
|
||||
|
|
@ -42,6 +43,12 @@ class ForumHandlers:
|
|||
return False
|
||||
return secrets.compare_digest(token, expected)
|
||||
|
||||
def _set_flash(self, msg):
|
||||
self._flash[self._get_csrf()] = msg
|
||||
|
||||
def _get_flash(self):
|
||||
return self._flash.pop(self._get_csrf(), "")
|
||||
|
||||
def _is_local(self, instance):
|
||||
if instance == "local":
|
||||
return True
|
||||
|
|
@ -57,7 +64,7 @@ class ForumHandlers:
|
|||
def _block_link(self, instance):
|
||||
if self._is_local(instance):
|
||||
return ""
|
||||
return f' <a class="forum-action-inline" href="/forum/blockhash/{instance}">[block]</a>'
|
||||
return f' <a href="/forum/blockhash/{instance}">block</a>'
|
||||
|
||||
def _respond(self, body_html, status=200):
|
||||
return {
|
||||
|
|
@ -67,6 +74,14 @@ class ForumHandlers:
|
|||
"headers": {},
|
||||
}
|
||||
|
||||
def _redirect(self, location):
|
||||
return {
|
||||
"status": 302,
|
||||
"content_type": "text/html; charset=utf-8",
|
||||
"body": "",
|
||||
"headers": {"Location": location},
|
||||
}
|
||||
|
||||
def _json(self, data, status=200):
|
||||
return {
|
||||
"status": status,
|
||||
|
|
@ -84,7 +99,7 @@ class ForumHandlers:
|
|||
}
|
||||
|
||||
def _error(self, status):
|
||||
return self._respond(f"<h2>{status}</h2>", status)
|
||||
return self._respond(f"<h1>{status}</h1>", status)
|
||||
|
||||
def _paginate(self, query):
|
||||
try:
|
||||
|
|
@ -104,7 +119,7 @@ class ForumHandlers:
|
|||
parts.append(f"page {page} of {total_pages}")
|
||||
if page < total_pages:
|
||||
parts.append(f'<a href="{base_url}{sep}p={page + 1}">next »</a>')
|
||||
return f'<p class="pagination">{" | ".join(parts)}</p>'
|
||||
return f'<p>{" | ".join(parts)}</p>'
|
||||
|
||||
def _now(self):
|
||||
return datetime.now().strftime("%Y-%m-%dT%H:%M:%S")
|
||||
|
|
@ -177,10 +192,10 @@ class ForumHandlers:
|
|||
peer_count = len(self.fdb.get_synced_instances())
|
||||
filter_count = self.fdb.get_peer_filter_count()
|
||||
return (
|
||||
f'<div class="forum-status">'
|
||||
f'<span>subscribed: {esc(topics_str)}</span>'
|
||||
f'<span>{peer_count} peers</span>'
|
||||
f'<span>{filter_count} filters</span>'
|
||||
f'<div style="font-size:0.85rem;color:#606060">'
|
||||
f'subscribed: {esc(topics_str)}'
|
||||
f' · {peer_count} peers'
|
||||
f' · {filter_count} filters'
|
||||
f'</div>'
|
||||
)
|
||||
|
||||
|
|
@ -204,30 +219,30 @@ class ForumHandlers:
|
|||
continue
|
||||
if self._is_new(r["created_at"]):
|
||||
new_count += 1
|
||||
badge = f'<span class="thread-badge">share</span>' if r["url"] else f'<span class="thread-badge">request</span>'
|
||||
badge = "share" if r["url"] else "request"
|
||||
mute_label = " [muted]" if is_muted else ""
|
||||
tags_html = ""
|
||||
if r["tags"]:
|
||||
tag_links = " ".join(
|
||||
f'<a href="/forum?tag={esc(t.strip())}" class="tag">{esc(t.strip())}</a>'
|
||||
f'<a href="/forum?tag={esc(t.strip())}">[{esc(t.strip())}]</a>'
|
||||
for t in r["tags"].split(",") if t.strip()
|
||||
)
|
||||
tags_html = f' {tag_links}'
|
||||
reply_label = f"{r['reply_count']} replies" if r['reply_count'] else "no replies"
|
||||
items += (
|
||||
f'<li>'
|
||||
f'<div class="thread-title">'
|
||||
f'{badge}{mute_label} '
|
||||
f'<a href="/forum/t/{esc(r["id"])}">{esc(r["title"])}</a>'
|
||||
f'<div style="border:1px solid #ddd;border-radius:4px;padding:0.9rem 1rem;margin-bottom:0.75rem">'
|
||||
f'<div style="margin-bottom:0.4rem">'
|
||||
f'<b><a href="/forum/t/{esc(r["id"])}" style="text-decoration:none;color:inherit">{esc(r["title"])}</a></b>'
|
||||
f'<small> [{badge}]{mute_label}</small>'
|
||||
f'{tags_html}'
|
||||
f'</div>'
|
||||
f'<div class="thread-meta">'
|
||||
f'<div style="font-size:0.85rem;color:#606060">'
|
||||
f'{esc(self._author_str(r["author_name"], r["author_instance"]))}'
|
||||
f' · {self._time_ago(r["created_at"])}'
|
||||
f' · {r["score"]} upvotes'
|
||||
f' · {reply_label}'
|
||||
f'</div>'
|
||||
f'</li>'
|
||||
f'</div>'
|
||||
)
|
||||
if not items:
|
||||
items = "<p>no threads yet.</p>"
|
||||
|
|
@ -238,43 +253,50 @@ class ForumHandlers:
|
|||
f'</form>'
|
||||
)
|
||||
tag_label = f' — {esc(tag)}' if tag else ""
|
||||
muted_link = f'<a class="forum-action" href="/forum?muted=1">show muted</a>' if not show_muted else f'<a class="forum-action" href="/forum">show all</a>'
|
||||
muted_link = f'<a href="/forum?muted=1">show muted</a>' if not show_muted else f'<a href="/forum">show all</a>'
|
||||
page_url = f'/forum?q={esc(search)}&tag={esc(tag)}&muted=1' if show_muted else (f'/forum?q={esc(search)}&tag={esc(tag)}' if search or tag else '/forum')
|
||||
return self._respond(
|
||||
f"<h2>forum{tag_label}</h2>"
|
||||
f"<h1>forum{tag_label}"
|
||||
f'<span style="font-size:0.85rem;color:#606060;font-weight:normal;vertical-align:middle"> — {total} threads{new_label}</span>'
|
||||
f"</h1>"
|
||||
f'{self._status_bar()}'
|
||||
f'<div class="forum-toolbar">'
|
||||
f"<br>"
|
||||
f'{search_form}'
|
||||
f'<div class="forum-toolbar-actions">'
|
||||
f'<a class="forum-action" href="/forum/new">+ new</a>'
|
||||
f'<a class="forum-action" href="/forum/moderation">mod</a>'
|
||||
f'<a class="forum-action" href="/forum/sync/now">sync now</a>'
|
||||
f'{muted_link}'
|
||||
f'</div></div>'
|
||||
f"<p>{total} threads{new_label}</p>"
|
||||
f'<ul class="forum-list">{items}</ul>'
|
||||
f"<p>"
|
||||
f'<b><a href="/forum/new">+ new thread</a></b>'
|
||||
f'<span style="font-size:0.85rem">'
|
||||
f' · <a href="/forum/moderation">mod</a>'
|
||||
f' · <a href="/forum/sync/now">sync now</a>'
|
||||
f' · {muted_link}'
|
||||
f"</span>"
|
||||
f"</p>"
|
||||
f'<hr>{items}'
|
||||
f"{self._page_nav(page, total, page_url)}"
|
||||
f'<br><a href="/">back</a>'
|
||||
)
|
||||
|
||||
def handle_new_form(self, msg=""):
|
||||
return self._respond(
|
||||
f"<h2>new thread</h2>"
|
||||
f'<form class="forum-form" method="post" action="/forum/new">'
|
||||
f"<h1>new thread</h1>"
|
||||
f"<p>Share a URL or start a discussion.</p>"
|
||||
f'<form method="post" action="/forum/new">'
|
||||
f'{self._csrf_field()}'
|
||||
f'<label>title</label>'
|
||||
f'<input name="title" placeholder="title" required>'
|
||||
f"<small>max {MAX_TITLE_LENGTH} characters</small>"
|
||||
f'<label>URL</label>'
|
||||
f'<input name="url" placeholder="URL you want to share (optional)">'
|
||||
f'<label>body</label>'
|
||||
f'<textarea name="body" rows="6" placeholder="details or context (optional)"></textarea>'
|
||||
f"<small>max {MAX_BODY_LENGTH} characters</small>"
|
||||
f'<label>tags</label>'
|
||||
f'<input name="tags" placeholder="comma-separated (optional)">'
|
||||
f'<label>Title:</label><br>'
|
||||
f'<input name="title" placeholder="title" size="50" required>'
|
||||
f"<br><small>max {MAX_TITLE_LENGTH} characters</small><br><br>"
|
||||
f'<label>URL:</label><br>'
|
||||
f'<input name="url" placeholder="https://example.com (optional)" size="50">'
|
||||
f"<br><br>"
|
||||
f'<label>Body:</label><br>'
|
||||
f'<textarea name="body" rows="6" cols="50" placeholder="details or context (optional)"></textarea>'
|
||||
f"<br><small>max {MAX_BODY_LENGTH} characters</small><br><br>"
|
||||
f'<label>Tags:</label><br>'
|
||||
f'<input name="tags" placeholder="comma-separated (optional)" size="50">'
|
||||
f"<br><br>"
|
||||
f'<button type="submit">post</button>'
|
||||
f"</form>"
|
||||
f"<p>{msg}</p>"
|
||||
f'<div class="forum-nav"><a href="/forum">back</a></div>'
|
||||
f'<a href="/forum">back</a>'
|
||||
)
|
||||
|
||||
def handle_new_submit(self, body):
|
||||
|
|
@ -308,7 +330,7 @@ class ForumHandlers:
|
|||
instance_hash = self.identity.hash.hex() if self.identity else "local"
|
||||
has_upvoted = self.fdb.has_upvoted(thread_id, instance_hash)
|
||||
|
||||
badge = f'<span class="thread-badge">share</span>' if thread["url"] else f'<span class="thread-badge">request</span>'
|
||||
badge = "share" if thread["url"] else "request"
|
||||
url_html = ""
|
||||
if thread["url"]:
|
||||
url_html = (
|
||||
|
|
@ -318,7 +340,7 @@ class ForumHandlers:
|
|||
tags_html = ""
|
||||
if thread["tags"]:
|
||||
tag_links = " ".join(
|
||||
f'<a href="/forum?tag={esc(t.strip())}" class="tag">{esc(t.strip())}</a>'
|
||||
f'<a href="/forum?tag={esc(t.strip())}">[{esc(t.strip())}]</a>'
|
||||
for t in thread["tags"].split(",") if t.strip()
|
||||
)
|
||||
tags_html = f'<p>{tag_links}</p>'
|
||||
|
|
@ -339,8 +361,8 @@ class ForumHandlers:
|
|||
if p["parent_id"]:
|
||||
parent_ref = f' <a href="#post-{esc(p["parent_id"])}">↪ reply</a>'
|
||||
posts_html += (
|
||||
f'<div class="post" id="post-{esc(p["id"])}">'
|
||||
f'<div class="post-meta">'
|
||||
f'<div style="border:1px solid #ddd;border-radius:4px;padding:0.9rem 1rem;margin-bottom:0.75rem" id="post-{esc(p["id"])}">'
|
||||
f'<div style="margin-bottom:0.4rem;font-size:0.85rem;color:#606060">'
|
||||
f'<b>{esc(self._author_str(p["author_name"], p["author_instance"]))}</b>'
|
||||
f'{self._block_link(p["author_instance"])}'
|
||||
f' · {self._time_ago(p["created_at"])}{parent_ref}'
|
||||
|
|
@ -352,32 +374,40 @@ class ForumHandlers:
|
|||
)
|
||||
|
||||
reply_form = (
|
||||
f'<form class="forum-form" method="post" action="/forum/t/{thread["id"]}/reply">'
|
||||
f'<form method="post" action="/forum/t/{thread["id"]}/reply">'
|
||||
f'{self._csrf_field()}'
|
||||
f'<textarea name="body" rows="4" placeholder="share a URL or reply..." required></textarea>'
|
||||
f"<small>max {MAX_BODY_LENGTH} characters</small>"
|
||||
f'<textarea name="body" rows="4" style="width:100%" placeholder="share a URL or reply..." required></textarea>'
|
||||
f"<br><small>max {MAX_BODY_LENGTH} characters</small><br><br>"
|
||||
f'<button type="submit">reply</button>'
|
||||
f"</form>"
|
||||
)
|
||||
|
||||
upvote_label = "-1" if has_upvoted else "+1"
|
||||
return self._respond(
|
||||
f"<h2>{badge} {esc(thread['title'])}</h2>"
|
||||
f'<p>'
|
||||
f"<h1>{esc(thread['title'])}"
|
||||
f'<span style="font-size:0.85rem;color:#606060;font-weight:normal;vertical-align:middle"> [{badge}]</span>'
|
||||
f"</h1>"
|
||||
f'<div style="border:1px solid #ddd;border-radius:4px;padding:0.9rem 1rem;margin-bottom:0.75rem">'
|
||||
f'<div style="font-size:0.85rem;color:#606060">'
|
||||
f'by {esc(self._author_str(thread["author_name"], thread["author_instance"]))}'
|
||||
f'{self._block_link(thread["author_instance"])}'
|
||||
f' · {self._time_ago(thread["created_at"])}'
|
||||
f' · {thread["score"]} upvotes'
|
||||
f' · <a href="{mute_href}">{mute_label}</a>'
|
||||
f' · <a href="/forum/t/{thread["id"]}/upvote">{upvote_label}</a>'
|
||||
f'{self._author_links(thread["id"], thread["author_instance"], instance_hash)}'
|
||||
f'</p>'
|
||||
f'</div>'
|
||||
f'{url_html}'
|
||||
f'{body_html}'
|
||||
f'{tags_html}'
|
||||
f'<div style="margin-top:0.4rem">'
|
||||
f'<b><a href="/forum/t/{thread["id"]}/upvote">{upvote_label}</a></b>'
|
||||
f'<span style="font-size:0.85rem">'
|
||||
f' · <a href="{mute_href}">{mute_label}</a>'
|
||||
f'{self._author_links(thread["id"], thread["author_instance"], instance_hash)}'
|
||||
f'</span>'
|
||||
f'</div>'
|
||||
f"</div>"
|
||||
f"<hr>"
|
||||
f"{posts_html}<br><br>"
|
||||
f"{reply_form}<br><br>"
|
||||
f"{posts_html}<br>"
|
||||
f"{reply_form}<br>"
|
||||
f'<a href="/forum">back to forum</a>'
|
||||
)
|
||||
|
||||
|
|
@ -411,15 +441,22 @@ class ForumHandlers:
|
|||
if thread["author_instance"] != instance_hash:
|
||||
return self._error(403)
|
||||
return self._respond(
|
||||
f"<h2>edit thread</h2>"
|
||||
f'<form class="forum-form" method="post" action="/forum/t/{thread_id}/edit">'
|
||||
f"<h1>edit thread</h1>"
|
||||
f"<p>Update your thread.</p>"
|
||||
f'<form method="post" action="/forum/t/{thread_id}/edit">'
|
||||
f'{self._csrf_field()}'
|
||||
f'<input name="title" value="{esc(thread["title"])}" required>'
|
||||
f"<small>max {MAX_TITLE_LENGTH} characters</small>"
|
||||
f'<input name="url" value="{esc(thread["url"] or "")}" placeholder="URL">'
|
||||
f'<textarea name="body" rows="6" placeholder="details or context">{esc(thread["body"] or "")}</textarea>'
|
||||
f"<small>max {MAX_BODY_LENGTH} characters</small>"
|
||||
f'<input name="tags" value="{esc(thread["tags"] or "")}" placeholder="tags, comma-separated">'
|
||||
f'<label>Title:</label><br>'
|
||||
f'<input name="title" value="{esc(thread["title"])}" size="50" required>'
|
||||
f"<br><small>max {MAX_TITLE_LENGTH} characters</small><br><br>"
|
||||
f'<label>URL:</label><br>'
|
||||
f'<input name="url" value="{esc(thread["url"] or "")}" placeholder="https://example.com (optional)" size="50">'
|
||||
f"<br><br>"
|
||||
f'<label>Body:</label><br>'
|
||||
f'<textarea name="body" rows="6" cols="50" placeholder="details or context (optional)">{esc(thread["body"] or "")}</textarea>'
|
||||
f"<br><small>max {MAX_BODY_LENGTH} characters</small><br><br>"
|
||||
f'<label>Tags:</label><br>'
|
||||
f'<input name="tags" value="{esc(thread["tags"] or "")}" placeholder="comma-separated (optional)" size="50">'
|
||||
f"<br><br>"
|
||||
f'<button type="submit">save</button>'
|
||||
f"</form>"
|
||||
f"<p>{msg}</p>"
|
||||
|
|
@ -484,12 +521,12 @@ class ForumHandlers:
|
|||
def _author_links(self, tid, author_instance, instance_hash):
|
||||
links = ""
|
||||
if author_instance == instance_hash:
|
||||
links += f' · <a class="forum-action-inline" href="/forum/t/{tid}/edit">edit</a>'
|
||||
links += f' · <a class="forum-action-inline" href="/forum/retract/{tid}">retract</a>'
|
||||
links += f' · <a href="/forum/t/{tid}/edit">edit</a>'
|
||||
links += f' · <a href="/forum/retract/{tid}">retract</a>'
|
||||
return links
|
||||
|
||||
def _post_retract_link(self, tid, pid):
|
||||
return f'<a class="forum-action-inline" href="/forum/retract/{tid}/post/{pid}">retract</a>'
|
||||
return f'<a href="/forum/retract/{tid}/post/{pid}">retract</a>'
|
||||
|
||||
def _peer_reports_html(self):
|
||||
counts = self.fdb.get_peer_block_counts()
|
||||
|
|
@ -497,13 +534,61 @@ class ForumHandlers:
|
|||
return "<p>no peer reports yet</p>"
|
||||
auto_blocked = set(h.strip() for h in self.fdb.get_setting("auto_blocked_instances", "").split(",") if h.strip())
|
||||
blocked = self._blocked_instances()
|
||||
items = ""
|
||||
items = []
|
||||
for h, count in sorted(counts.items(), key=lambda x: -x[1]):
|
||||
status = " (blocked)" if h in blocked else " (pending)"
|
||||
items += f"<li>{esc(h[:16])}... — {count} reports{status}</li>"
|
||||
return f"<ul>{items}</ul>"
|
||||
items.append(f"{esc(h[:16])}... — {count} reports{status}")
|
||||
return '<p style="font-size:0.85rem;color:#606060">' + "<br>".join(items) + "</p>"
|
||||
|
||||
def handle_moderation(self, msg=""):
|
||||
def handle_status(self):
|
||||
synced = self.fdb.get_synced_instances()
|
||||
blocked = self._blocked_instances()
|
||||
rows, total = self.fdb.get_threads(per_page=99999)
|
||||
|
||||
from collections import Counter
|
||||
peer_threads = Counter(r["author_instance"] for r in rows)
|
||||
total_posts = sum(len(list(self.fdb.get_posts(r["id"]))) for r in rows)
|
||||
|
||||
local_hash = self.identity.hash.hex() if self.identity else "local"
|
||||
html = (
|
||||
f"<h1>forum status</h1>"
|
||||
f"<table>"
|
||||
f"<tr><td>this instance</td><td>{esc(local_hash[:16])}...</td></tr>"
|
||||
f"<tr><td>threads</td><td>{total}</td></tr>"
|
||||
f"<tr><td>posts</td><td>{total_posts}</td></tr>"
|
||||
f"<tr><td>known peers</td><td>{len(synced)}</td></tr>"
|
||||
f"<tr><td>auto-discover</td><td>{'on' if self.fdb.get_setting('forum_auto_discover', '1') == '1' else 'off'}</td></tr>"
|
||||
f"<tr><td>auto-sync</td><td>{'on' if self.fdb.get_setting('forum_auto_sync', '0') == '1' else 'off'}</td></tr>"
|
||||
f"<tr><td>retention</td><td>{self.fdb.get_setting('forum_retention_days', '30')} days</td></tr>"
|
||||
f"<tr><td>topic filter</td><td>{esc(self.fdb.get_setting('topic_subscriptions', '') or '(none)')}</td></tr>"
|
||||
f"</table>"
|
||||
f"<br>"
|
||||
f"<h2>peers</h2>"
|
||||
)
|
||||
if not synced:
|
||||
html += "<p>no peers known yet</p>"
|
||||
else:
|
||||
html += "<table>"
|
||||
html += "<tr><th>hash</th><th>name</th><th>threads</th><th>last sync</th></tr>"
|
||||
for s in synced:
|
||||
h = esc(s["instance_hash"][:16]) + "..."
|
||||
name = esc(s["name"] or "-")
|
||||
count = peer_threads.get(s["instance_hash"], 0)
|
||||
last = esc(s["last_sync"] or "never")
|
||||
html += f"<tr><td>{h}</td><td>{name}</td><td>{count}</td><td>{last}</td></tr>"
|
||||
html += "</table>"
|
||||
|
||||
if blocked:
|
||||
html += f"<br><h2>blocked</h2><table>"
|
||||
for h in sorted(blocked):
|
||||
html += f"<tr><td>{esc(h[:16])}...</td></tr>"
|
||||
html += "</table>"
|
||||
|
||||
html += f'<br><a href="/forum">forum</a> · <a href="/forum/moderation">moderation</a>'
|
||||
return self._respond(html)
|
||||
|
||||
def handle_moderation(self, query=None):
|
||||
msg = self._get_flash()
|
||||
blocked = self._blocked_instances()
|
||||
auto_blocked = set(h.strip() for h in self.fdb.get_setting("auto_blocked_instances", "").split(",") if h.strip())
|
||||
peer_counts = self.fdb.get_peer_block_counts()
|
||||
|
|
@ -522,111 +607,121 @@ class ForumHandlers:
|
|||
label = "auto" if h in auto_blocked else ""
|
||||
reports = f" ({peer_counts.get(h, 0)} reports)" if h in peer_counts else ""
|
||||
blocked_items += (
|
||||
f'<li>'
|
||||
f'{esc(h[:16])}...'
|
||||
f'{" [" + label + "]" if label else ""}{reports} '
|
||||
f'<form method="post" action="/forum/unblock" style="display:inline">'
|
||||
f'<div style="border:1px solid #ddd;border-radius:4px;padding:0.9rem 1rem;margin-bottom:0.75rem">'
|
||||
f'<div style="margin-bottom:0.4rem"><b>{esc(h[:16])}...</b>'
|
||||
f'{" [" + label + "]" if label else ""}{reports}</div>'
|
||||
f'<form method="post" action="/forum/unblock" style="margin:0">'
|
||||
f'{self._csrf_field()}'
|
||||
f'<input type="hidden" name="instance" value="{esc(h)}">'
|
||||
f'<button>unblock</button></form>'
|
||||
f'</li>'
|
||||
f'<input type="submit" value="unblock">'
|
||||
f"</form>"
|
||||
f'</div>'
|
||||
)
|
||||
blocked_items = f"<ul>{blocked_items}</ul>"
|
||||
blocked_items = blocked_items
|
||||
else:
|
||||
blocked_items = "<p>no instances blocked</p>"
|
||||
|
||||
synced_items = ""
|
||||
for s in synced:
|
||||
synced_items += (
|
||||
f'<li>{esc(s["name"] or s["instance_hash"][:16])}... '
|
||||
f'<form method="post" action="/forum/unsync" style="display:inline">'
|
||||
f'<div style="border:1px solid #ddd;border-radius:4px;padding:0.9rem 1rem;margin-bottom:0.75rem">'
|
||||
f'<div style="margin-bottom:0.4rem"><b>{esc(s["name"] or s["instance_hash"][:16])}...</b></div>'
|
||||
f'<form method="post" action="/forum/unsync" style="display:inline-block;margin:0">'
|
||||
f'{self._csrf_field()}'
|
||||
f'<input type="hidden" name="instance" value="{esc(s["instance_hash"])}">'
|
||||
f'<button>remove</button></form>'
|
||||
f'</li>'
|
||||
f'</div>'
|
||||
)
|
||||
synced_items = f"<ul>{synced_items}</ul>" if synced_items else "<p>no instances synced</p>"
|
||||
synced_items = synced_items or "<p>no instances synced</p>"
|
||||
|
||||
msg_html = f'<p><em>{esc(msg)}</em></p>' if msg else ""
|
||||
|
||||
return self._respond(
|
||||
f"<h2>moderation</h2>"
|
||||
f"<p>{msg}</p>"
|
||||
f"<h1>moderation</h1>"
|
||||
f"{msg_html}"
|
||||
f"<nav>"
|
||||
f'<a href="#subscriptions">subscriptions</a>'
|
||||
f' · <a href="#settings">settings</a>'
|
||||
f' · <a href="#network">network</a>'
|
||||
f' · <a href="#moderation">moderation</a>'
|
||||
f"</nav>"
|
||||
f'{self._status_bar()}'
|
||||
f"<hr>"
|
||||
|
||||
f'<div class="section">'
|
||||
f'<div class="section-title">subscriptions</div>'
|
||||
f'<form class="forum-form" method="post" action="/forum/topics">'
|
||||
f"<section id=\"subscriptions\">"
|
||||
f"<h2>subscriptions</h2>"
|
||||
f'<form method="post" action="/forum/topics">'
|
||||
f'{self._csrf_field()}'
|
||||
f'<p>Only sync content matching these topics.</p>'
|
||||
f'<input name="topics" value="{esc(self._get_subscribed_topics_str())}" placeholder="comma-separated topics (leave empty for all)">'
|
||||
f"<small>only sync content matching these topics</small>"
|
||||
f'<button>save</button>'
|
||||
f'<br><br><input type="submit" value="save subscriptions">'
|
||||
f"</form>"
|
||||
f'</div>'
|
||||
f"</section>"
|
||||
f"<hr>"
|
||||
|
||||
f'<div class="section">'
|
||||
f'<div class="section-title">settings</div>'
|
||||
f'<div class="section-desc">network behavior</div>'
|
||||
f'<form class="forum-form" method="post" action="/forum/auto_discover">'
|
||||
f'{self._csrf_field()}'
|
||||
f'<label class="checkbox-label"><input type="checkbox" name="enabled" value="1"{auto_discover_checked}>'
|
||||
f" auto-discover peers via announces</label>"
|
||||
f'<button>save</button>'
|
||||
f"</form>"
|
||||
f'<form class="forum-form" method="post" action="/forum/auto_sync">'
|
||||
f'{self._csrf_field()}'
|
||||
f'<label class="checkbox-label"><input type="checkbox" name="enabled" value="1"{auto_sync_checked}>'
|
||||
f" auto-sync every 5 minutes</label>"
|
||||
f'<button>save</button>'
|
||||
f"</form>"
|
||||
f'<form class="forum-form" method="post" action="/forum/storage">'
|
||||
f"<section id=\"settings\">"
|
||||
f"<h2>settings</h2>"
|
||||
f"<p>network behavior</p>"
|
||||
f'<form method="post" action="/forum/settings">'
|
||||
f'{self._csrf_field()}'
|
||||
f'<label><input type="checkbox" name="auto_discover" value="1"{auto_discover_checked}>'
|
||||
f" auto-discover peers via announces</label><br>"
|
||||
f'<label><input type="checkbox" name="auto_sync" value="1"{auto_sync_checked}>'
|
||||
f" auto-sync every 5 minutes</label><br>"
|
||||
f'<label>keep threads for '
|
||||
f'<input name="retention_days" value="{esc(retention_days)}" size="5" style="width:50px">'
|
||||
f' days (0 = keep everything)</label>'
|
||||
f'<button>save</button>'
|
||||
f'<input name="retention_days" value="{esc(retention_days)}" size="5">'
|
||||
f' days (0 = keep everything)</label><br><br>'
|
||||
f'<input type="submit" value="save settings">'
|
||||
f"</form>"
|
||||
f'</div>'
|
||||
f"</section>"
|
||||
f"<hr>"
|
||||
|
||||
f'<div class="section">'
|
||||
f'<div class="section-title">network</div>'
|
||||
f'<div class="section-desc">{len(synced)} known peers</div>'
|
||||
f"<section id=\"network\">"
|
||||
f"<h2>network</h2>"
|
||||
f"<p>{len(synced)} known peers</p>"
|
||||
f'{synced_items}'
|
||||
f'<form class="forum-form" method="post" action="/forum/sync/add">'
|
||||
f'<form method="post" action="/forum/sync/add">'
|
||||
f'{self._csrf_field()}'
|
||||
f'<input name="instance" placeholder="instance hash">'
|
||||
f'<input name="name" placeholder="label (optional)">'
|
||||
f'<button>add</button>'
|
||||
f' <input name="name" placeholder="label (optional)">'
|
||||
f'<br><br><input type="submit" value="add">'
|
||||
f"</form>"
|
||||
f'</div>'
|
||||
f"</section>"
|
||||
f"<hr>"
|
||||
|
||||
f'<div class="section">'
|
||||
f'<div class="section-title">moderation</div>'
|
||||
f"<section id=\"moderation\">"
|
||||
f"<h2>moderation</h2>"
|
||||
f"<h3>blocked instances</h3>"
|
||||
f'{blocked_items}'
|
||||
f'<form class="forum-form" method="post" action="/forum/block">'
|
||||
f'<form method="post" action="/forum/block">'
|
||||
f'{self._csrf_field()}'
|
||||
f'<input name="instance" placeholder="instance hash (32 hex chars)">'
|
||||
f'<button>block</button>'
|
||||
f'<br><br><input type="submit" value="block">'
|
||||
f"</form>"
|
||||
f'<div class="section-title">peer reports</div>'
|
||||
f"<h3>peer reports</h3>"
|
||||
f'{self._peer_reports_html()}'
|
||||
f'<div class="section-title">keyword filters</div>'
|
||||
f'<form class="forum-form" method="post" action="/forum/filters">'
|
||||
f"<h3>keyword filters</h3>"
|
||||
f'<form method="post" action="/forum/filters">'
|
||||
f'{self._csrf_field()}'
|
||||
f'<input name="keywords" value="{esc(filters_str)}" placeholder="comma-separated keywords">'
|
||||
f'<button>save</button>'
|
||||
f'<br><br><input type="submit" value="save filters">'
|
||||
f"</form>"
|
||||
f'</div>'
|
||||
f"</section>"
|
||||
|
||||
f'<div class="forum-nav"><a href="/forum">back</a></div>'
|
||||
f'<a href="/forum">back</a>'
|
||||
f' · <a href="/forum/status">status</a>'
|
||||
)
|
||||
|
||||
def handle_block(self, body):
|
||||
instance = body.get("instance", [""])[0].strip().replace("<", "").replace(">", "")
|
||||
if len(instance) != 32:
|
||||
return self.handle_moderation("Invalid instance hash (must be 32 hex chars).")
|
||||
self._set_flash("Invalid instance hash (must be 32 hex chars).")
|
||||
return self._redirect("/forum/moderation")
|
||||
blocked = self._blocked_instances()
|
||||
blocked.add(instance)
|
||||
self.fdb.set_setting("blocked_instances", ",".join(blocked))
|
||||
return self.handle_moderation(f"Blocked {instance[:16]}...")
|
||||
self._set_flash(f"Blocked {instance[:16]}...")
|
||||
return self._redirect("/forum/moderation")
|
||||
|
||||
def handle_unblock(self, body):
|
||||
instance = body.get("instance", [""])[0].strip()
|
||||
|
|
@ -637,7 +732,8 @@ class ForumHandlers:
|
|||
auto.discard(instance)
|
||||
self.fdb.set_setting("auto_blocked_instances", ",".join(auto))
|
||||
self.fdb.clear_peer_block(instance)
|
||||
return self.handle_moderation(f"Unblocked {instance[:16]}...")
|
||||
self._set_flash(f"Unblocked {instance[:16]}...")
|
||||
return self._redirect("/forum/moderation")
|
||||
|
||||
def handle_block_hash(self, instance):
|
||||
if len(instance) == 32 or len(instance) == 64:
|
||||
|
|
@ -656,30 +752,53 @@ class ForumHandlers:
|
|||
def handle_filters(self, body):
|
||||
keywords = body.get("keywords", [""])[0].strip()
|
||||
self.fdb.set_setting("keyword_filters", keywords)
|
||||
return self.handle_moderation("Filters saved.")
|
||||
self._set_flash("Filters saved.")
|
||||
return self._redirect("/forum/moderation")
|
||||
|
||||
def handle_settings(self, body):
|
||||
auto_discover = "1" if body.get("auto_discover") else "0"
|
||||
auto_sync = "1" if body.get("auto_sync") else "0"
|
||||
days = body.get("retention_days", ["30"])[0].strip()
|
||||
try:
|
||||
days = max(0, int(days))
|
||||
except ValueError:
|
||||
self._set_flash("Invalid retention days.")
|
||||
return self._redirect("/forum/moderation")
|
||||
self.fdb.set_setting("forum_auto_discover", auto_discover)
|
||||
self.fdb.set_setting("forum_auto_sync", auto_sync)
|
||||
self.fdb.set_setting("forum_retention_days", str(days))
|
||||
if self.sync:
|
||||
self.sync.set_auto_discover(auto_discover == "1")
|
||||
self.sync.set_auto_sync(auto_sync == "1")
|
||||
self._set_flash("Settings saved.")
|
||||
return self._redirect("/forum/moderation")
|
||||
|
||||
def handle_auto_discover(self, body):
|
||||
enabled = "1" if body.get("enabled") else "0"
|
||||
self.fdb.set_setting("forum_auto_discover", enabled)
|
||||
if self.sync:
|
||||
self.sync.set_auto_discover(enabled == "1")
|
||||
return self.handle_moderation(f"Auto-discovery {'enabled' if enabled == '1' else 'disabled'}.")
|
||||
self._set_flash(f"Auto-discovery {'enabled' if enabled == '1' else 'disabled'}.")
|
||||
return self._redirect("/forum/moderation")
|
||||
|
||||
def handle_storage(self, body):
|
||||
days = body.get("retention_days", ["30"])[0].strip()
|
||||
try:
|
||||
days = max(0, int(days))
|
||||
except ValueError:
|
||||
return self.handle_moderation("Invalid retention days.")
|
||||
self._set_flash("Invalid retention days.")
|
||||
return self._redirect("/forum/moderation")
|
||||
self.fdb.set_setting("forum_retention_days", str(days))
|
||||
return self.handle_moderation(f"Storage retention set to {days} days.")
|
||||
self._set_flash(f"Storage retention set to {days} days.")
|
||||
return self._redirect("/forum/moderation")
|
||||
|
||||
def handle_auto_sync(self, body):
|
||||
enabled = "1" if body.get("enabled") else "0"
|
||||
self.fdb.set_setting("forum_auto_sync", enabled)
|
||||
if self.sync:
|
||||
self.sync.set_auto_sync(enabled == "1")
|
||||
return self.handle_moderation(f"Auto-sync {'enabled' if enabled == '1' else 'disabled'}.")
|
||||
self._set_flash(f"Auto-sync {'enabled' if enabled == '1' else 'disabled'}.")
|
||||
return self._redirect("/forum/moderation")
|
||||
|
||||
def handle_sync_now(self):
|
||||
if not self.sync:
|
||||
|
|
@ -694,19 +813,23 @@ class ForumHandlers:
|
|||
instance = body.get("instance", [""])[0].strip().replace("<", "").replace(">", "")
|
||||
name = body.get("name", [""])[0].strip()
|
||||
if len(instance) != 32:
|
||||
return self.handle_moderation("Invalid instance hash (must be 32 hex chars).")
|
||||
self._set_flash("Invalid instance hash (must be 32 hex chars).")
|
||||
return self._redirect("/forum/moderation")
|
||||
self.fdb.upsert_synced_instance(instance, name)
|
||||
return self.handle_moderation(f"Added {name or instance[:16]}... to sync.")
|
||||
self._set_flash(f"Added {name or instance[:16]}... to sync.")
|
||||
return self._redirect("/forum/moderation")
|
||||
|
||||
def handle_unsync(self, body):
|
||||
instance = body.get("instance", [""])[0].strip()
|
||||
self.fdb.remove_synced_instance(instance)
|
||||
return self.handle_moderation("Removed.")
|
||||
self._set_flash("Removed.")
|
||||
return self._redirect("/forum/moderation")
|
||||
|
||||
def handle_topics(self, body):
|
||||
topics = body.get("topics", [""])[0].strip()
|
||||
self.fdb.set_setting("topic_subscriptions", topics)
|
||||
return self.handle_moderation("Topic subscriptions saved.")
|
||||
self._set_flash("Topic subscriptions saved.")
|
||||
return self._redirect("/forum/moderation")
|
||||
|
||||
# --- Sync endpoint (called over RNS) ---
|
||||
|
||||
|
|
@ -924,7 +1047,9 @@ class ForumHandlers:
|
|||
elif sub == "/new":
|
||||
return self._with_csrf(self.handle_new_form(), csrf_token)
|
||||
elif sub == "/moderation":
|
||||
return self._with_csrf(self.handle_moderation(), csrf_token)
|
||||
return self._with_csrf(self.handle_moderation(query), csrf_token)
|
||||
elif sub == "/status":
|
||||
return self._with_csrf(self.handle_status(), csrf_token)
|
||||
elif sub.startswith("/t/"):
|
||||
tid = sub[3:]
|
||||
if tid.endswith("/upvote"):
|
||||
|
|
@ -949,7 +1074,7 @@ class ForumHandlers:
|
|||
elif method == "POST":
|
||||
if not self._check_csrf(body):
|
||||
return self._with_csrf(
|
||||
self._respond("<h2>403 Forbidden</h2>", status=403), csrf_token
|
||||
self._respond("<h1>403 Forbidden</h1>", status=403), csrf_token
|
||||
)
|
||||
if sub == "/new":
|
||||
return self._with_csrf(self.handle_new_submit(body), csrf_token)
|
||||
|
|
@ -986,6 +1111,8 @@ class ForumHandlers:
|
|||
return self._with_csrf(self.handle_auto_sync(body), csrf_token)
|
||||
elif sub == "/topics":
|
||||
return self._with_csrf(self.handle_topics(body), csrf_token)
|
||||
elif sub == "/settings":
|
||||
return self._with_csrf(self.handle_settings(body), csrf_token)
|
||||
|
||||
return self._with_csrf(self._error(404), csrf_token)
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue