[+] 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 from pathlib import Path
import requests import requests
from fastapi import FastAPI from fastapi import FastAPI, Request, HTTPException, Body, Header
from hypy_utils import ensure_dir from hypy_utils import ensure_dir
from hypy_utils.logging_utils import setup_logger 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 starlette.staticfiles import StaticFiles
from telegram import InlineKeyboardButton, InlineKeyboardMarkup, Update from telegram import InlineKeyboardButton, InlineKeyboardMarkup, Update
from telegram.ext import Application, CallbackQueryHandler, CommandHandler, ContextTypes, MessageHandler, filters from telegram.ext import Application, CallbackQueryHandler, CommandHandler, ContextTypes, MessageHandler, filters
@@ -545,6 +545,37 @@ def api_tree():
return tree_to_dict("azaneko") 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) @app.get("/c/{channel}", response_class=HTMLResponse)
def channel_info(channel: str): def channel_info(channel: str):
info = db.channel_info(channel) 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') parent = ForeignKeyField('self', null=True, backref='children', on_delete='CASCADE', field='username')
height = IntegerField(default=0) # Tree height (depth) height = IntegerField(default=0) # Tree height (depth)
owner_id = BigIntegerField(null=True) # Telegram user ID of the channel owner 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): class Vote(BaseModel):
@@ -60,6 +61,35 @@ def channel_info(username: str) -> Channel | None:
return 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): 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.""" """Register a channel using its username and assign the correct height."""
if parent_username: 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")) 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) info = db.channel_info(channel)
if not info or (info.hidden and not admin):
return ""
votes = db.get_votes(channel) votes = db.get_votes(channel)
water = f' 💧{votes}' if votes else '' 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: if not info.children:
return out return out
out += f"""<div class="container l{info.height}">\n""" out += f"""<div class="container l{info.height}">\n"""
for child in info.children: 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""" out += f"""</div>\n"""
return out return out
def gen_tree(d: Path = src / "public"): 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') write(d / "index.html", (src / "public/layout-full-tree.html").read_text('utf-8')
.replace("{{CONTENT}}", of)) .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__': if __name__ == '__main__':
while True: while True: