[+] Admin page, hide channels

This commit is contained in:
2026-03-12 05:47:42 -04:00
parent 6553e4cbbf
commit 8c31697527
3 changed files with 143 additions and 6 deletions
+33 -2
View File
@@ -6,10 +6,10 @@ import urllib.parse
from pathlib import Path
import requests
from fastapi import FastAPI
from fastapi import FastAPI, Request, HTTPException, Body, Header
from hypy_utils import ensure_dir
from hypy_utils.logging_utils import setup_logger
from starlette.responses import HTMLResponse
from starlette.responses import HTMLResponse, JSONResponse
from starlette.staticfiles import StaticFiles
from telegram import InlineKeyboardButton, InlineKeyboardMarkup, Update
from telegram.ext import Application, CallbackQueryHandler, CommandHandler, ContextTypes, MessageHandler, filters
@@ -545,6 +545,37 @@ def api_tree():
return tree_to_dict("azaneko")
@app.get("/api/admin/channels")
def api_admin_channels(x_admin_password: str = Header(None)):
if x_admin_password not in CONFIG.get("admin-passwords", [CONFIG.get("init-password")]):
raise HTTPException(status_code=403, detail="Invalid admin password")
channels = db.get_all_channels()
return [{"username": c.username, "name": c.name, "parent": c.parent_id, "hidden": c.hidden} for c in channels]
@app.post("/api/admin/channels/{username}/hide")
def api_hide_channel(username: str, hidden: bool = Body(..., embed=True), x_admin_password: str = Header(None)):
if x_admin_password not in CONFIG.get("admin-passwords", [CONFIG.get("init-password")]):
raise HTTPException(status_code=403, detail="Invalid admin password")
if db.set_hidden(username, hidden):
return {"success": True}
else:
raise HTTPException(status_code=404, detail="Channel not found")
@app.delete("/api/admin/channels/{username}")
def api_delete_channel(username: str, x_admin_password: str = Header(None)):
if x_admin_password not in CONFIG.get("admin-passwords", [CONFIG.get("init-password")]):
raise HTTPException(status_code=403, detail="Invalid admin password")
if db.remove_channel(username):
return {"success": True}
else:
raise HTTPException(status_code=404, detail="Channel not found")
@app.get("/c/{channel}", response_class=HTMLResponse)
def channel_info(channel: str):
info = db.channel_info(channel)
+30
View File
@@ -18,6 +18,7 @@ class Channel(BaseModel):
parent = ForeignKeyField('self', null=True, backref='children', on_delete='CASCADE', field='username')
height = IntegerField(default=0) # Tree height (depth)
owner_id = BigIntegerField(null=True) # Telegram user ID of the channel owner
hidden = BooleanField(default=False) # Whether the channel is hidden from the tree
class Vote(BaseModel):
@@ -60,6 +61,35 @@ def channel_info(username: str) -> Channel | None:
return None
def remove_channel(username: str) -> bool:
"""Remove a channel and its descendants (via CASCADE)."""
try:
ch = Channel.get(Channel.username == username)
ch.delete_instance(recursive=True)
return True
except Channel.DoesNotExist:
return False
def set_hidden(username: str, hidden: bool) -> bool:
"""Toggle the hidden status of a channel."""
try:
ch = Channel.get(Channel.username == username)
ch.hidden = hidden
ch.save()
return True
except Channel.DoesNotExist:
return False
def get_all_channels(include_hidden: bool = True) -> list[Channel]:
"""Get all registered channels."""
query = Channel.select()
if not include_hidden:
query = query.where(Channel.hidden == False)
return list(query.order_by(Channel.height, Channel.username))
def register(username: str, name: str, parent_username: str = None, owner_id: int = None):
"""Register a channel using its username and assign the correct height."""
if parent_username:
+80 -4
View File
@@ -14,26 +14,102 @@ def indent(string: str, level: int):
return "\n".join(" " * level + line for line in string.split("\n"))
def dfs(channel: str):
def dfs(channel: str, admin: bool = False):
info = db.channel_info(channel)
if not info or (info.hidden and not admin):
return ""
votes = db.get_votes(channel)
water = f' 💧{votes}' if votes else ''
out = f"""<span class="tree l{info.height}"><a href="https://t.me/{channel}">@{channel}</a> - {info.name}{water}</span>\n"""
# In admin mode, show hidden status and a hide button
hide_icon = "👁️" if info.hidden else "🚫"
admin_info = f' <span style="opacity:0.5;">[HIDDEN]</span>' if info.hidden else ''
hide_btn = f' <button onclick="adminHide(\'{channel}\', {str(not info.hidden).lower()})" title="Hide/Show" style="background:none;border:none;cursor:pointer;font-size:0.8em;opacity:0.6;">{hide_icon}</button>' if admin else ''
del_btn = f' <button onclick="adminDelete(\'{channel}\')" title="Delete" style="background:none;border:none;cursor:pointer;font-size:0.8em;opacity:0.6;">🗑️</button>' if admin else ''
out = (f"""<span class="tree l{info.height}"><a href="https://t.me/{channel}">@{channel}</a> - {info.name}{water}{admin_info}{hide_btn}{del_btn}</span>\n""")
if not info.children:
return out
out += f"""<div class="container l{info.height}">\n"""
for child in info.children:
out += indent(dfs(child.username), 2)
child_out = dfs(child.username, admin)
if child_out:
out += indent(child_out, 2)
out += f"""</div>\n"""
return out
def gen_tree(d: Path = src / "public"):
of = dfs("azaneko")
# Generate public index.html
of = dfs("azaneko", admin=False)
write(d / "index.html", (src / "public/layout-full-tree.html").read_text('utf-8')
.replace("{{CONTENT}}", of))
# Generate admin.html
of_admin = dfs("azaneko", admin=True)
admin_layout = (src / "public/layout-full-tree.html").read_text('utf-8')
admin_script = """
<script>
async function adminHide(username, hide) {
const pw = prompt(`请输入管理员密码以${hide ? '隐藏' : '显示'} @${username}:`);
if (!pw) return;
try {
const res = await fetch(`/api/admin/channels/${username}/hide`, {
method: 'POST',
headers: {
'X-Admin-Password': pw,
'Content-Type': 'application/json'
},
body: JSON.stringify({ hidden: hide })
});
if (res.status === 403) {
alert('密码不对哦');
} else if (res.ok) {
alert('操作成功!');
location.reload();
} else {
const err = await res.json();
alert(`出错啦: ${err.detail || '未知错误'}`);
}
} catch (e) {
alert(`请求失败: ${e.message}`);
}
}
async function adminDelete(username) {
const pw = prompt(`请输入管理员密码以移除 @${username}:`);
if (!pw) return;
if (!confirm(`确定要移除 @${username} 吗?\\n\\n警告:这会同时移除它的所有子频道!`)) return;
try {
const res = await fetch(`/api/admin/channels/${username}`, {
method: 'DELETE',
headers: { 'X-Admin-Password': pw }
});
if (res.status === 403) {
alert('密码不对哦');
} else if (res.ok) {
alert('移除成功!');
location.reload();
} else {
const err = await res.json();
alert(`出错啦: ${err.detail || '未知错误'}`);
}
} catch (e) {
alert(`请求失败: ${e.message}`);
}
}
</script>
"""
write(d / "admin.html", admin_layout.replace("{{CONTENT}}", of_admin).replace("</body>", admin_script + "</body>"))
if __name__ == '__main__':
while True: