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