[+] Admin page, hide channels
This commit is contained in:
+33
-2
@@ -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)
|
||||
|
||||
@@ -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
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user