diff --git a/src/bot.py b/src/bot.py index d76c58c..06e42f9 100644 --- a/src/bot.py +++ b/src/bot.py @@ -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) diff --git a/src/db.py b/src/db.py index 1295e62..88ca209 100644 --- a/src/db.py +++ b/src/db.py @@ -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: diff --git a/src/gentree.py b/src/gentree.py index ba69948..b535d74 100644 --- a/src/gentree.py +++ b/src/gentree.py @@ -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"""@{channel} - {info.name}{water}\n""" + + # In admin mode, show hidden status and a hide button + hide_icon = "👁️" if info.hidden else "🚫" + admin_info = f' [HIDDEN]' if info.hidden else '' + hide_btn = f' ' if admin else '' + del_btn = f' ' if admin else '' + + out = (f"""@{channel} - {info.name}{water}{admin_info}{hide_btn}{del_btn}\n""") + if not info.children: return out out += f"""
\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"""
\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 = """ + + """ + write(d / "admin.html", admin_layout.replace("{{CONTENT}}", of_admin).replace("", admin_script + "")) + if __name__ == '__main__': while True: