diff --git a/bot.py b/bot.py new file mode 100644 index 0000000..9e058e0 --- /dev/null +++ b/bot.py @@ -0,0 +1,172 @@ +import re +import tomllib +import urllib.parse +from pathlib import Path + +import requests +from fastapi import FastAPI +from hypy_utils import ensure_dir +from hypy_utils.logging_utils import setup_logger +from starlette.responses import HTMLResponse +from starlette.staticfiles import StaticFiles +from telegram import Update +from telegram.ext import Application, CommandHandler, ContextTypes + +import db +import utils +from utils import gen_sha, CONFIG + + +BOT_TOKEN = CONFIG["token"] +BOT_NAME = CONFIG["name"] + +app = FastAPI() +logger = setup_logger() +bot = Application.builder().token(BOT_TOKEN).build() + +data_dir = Path(__file__).parent / "data" +channels_dir = ensure_dir(data_dir / "channels") + +validating = set() + + +def user_info(update: Update): + return (f"{update.message.from_user.id} {update.message.from_user.username or ''} " + f"{update.message.from_user.first_name or ''} {update.message.from_user.last_name or ''}") + + +async def start(update: Update, context: ContextTypes.DEFAULT_TYPE): + logger.info(f"/start from {user_info(update)}") + await update.message.reply_text("🌳🌳🌳") + + +def channel_html(channel: str): + if (channels_dir / f"{channel}.html").exists(): + return (channels_dir / f"{channel}.html").read_text() + return requests.get(f"https://t.me/{channel}").text + + +async def plant(update: Update, context: ContextTypes.DEFAULT_TYPE): + if update.message.chat.type != "private": + return + + logger.info(f"/leaf from {user_info(update)}: {update.message.text}") + + args, uid = context.args, update.message.from_user.id + if len(args) != 2: + logger.info(f"> Invalid args.") + return await update.message.reply_text("用法是 /leaf <上级频道名> <你的频道名> 哦~") + + parent, channel = args + parent, channel = parent.strip("<>{} @"), channel.strip("<>{} @") + sha = gen_sha(channel, uid, parent) + + # Check if channel only contains 0-9, a-z, A-Z and _ + if not re.match(r"^[0-9a-zA-Z_]+$", channel): + logger.info(f"> Invalid channel name.") + return await update.message.reply_text("没有找到这个频道... 只有公开的频道可以参与,以及需要输入频道的 @用户名,不是显示名哦~") + + if not db.channel_info(parent): + logger.info(f"> Parent channel not found.") + return await update.message.reply_text("上级频道还不在树上... 是不是打错了 qwq") + + # 检查在不在树上 + if db.channel_info(channel): + logger.info(f"> Channel already exists.") + return await update.message.reply_text(f"这个频道已经在树上了哦~ https://tree.aza.moe/c/{channel}") + + # 检查验证码 + text = channel_html(channel) + if 'noindex, nofollow' in text: + logger.info(f"> Channel noindex, nofollow.") + return await update.message.reply_text("没有找到这个频道... 只有公开的频道可以参与,以及需要输入频道的 @用户名,不是显示名哦~") + + info = utils.extract_meta_tags(text) + if sha in text: + logger.info(f"> 🌿 Registering channel {channel} with parent {parent}.") + height = db.register(channel, info.title, parent) + await update.message.reply_text(f"""频道 {channel} 上树成功!把下面这条转发到频道里吧~""".strip()) + url_enc = urllib.parse.quote_plus(f"https://tree.aza.moe/c/{channel}") + return await update.message.reply_html(f""" +今天是植树节,想试试和大家一起种一颗 tgcn 频道树 🌳 qwq + +这里是 {info.title},是 @{parent} 的树枝 🌿 在频道树的第 {height + 1} 层哦~ + +(如果你也有公开频道,想成为这个频道的树叶的话,就去给 @tgtreebot 发送 /leaf {channel} {{你的频道名}} 吧! > <) \u200e +""".strip()) + + if sha not in validating: + logger.info(f"> Channel not validated, asking for validation.") + await update.message.reply_text(f""" +好耶! + +不过上树之前,为了防止被滥用,需要先验证一下你是 {channel} 的管理员... + +请编辑频道简介加入验证码 {sha} 再重新执行这条指令吧~(加在哪里都可以的 > < 验证完就可以删掉) +""".strip()) + validating.add(sha) + else: + logger.info(f"> Channel not validated, asking again for validation.") + await update.message.reply_text("(看了一下好像频道信息还没有更新的样子... 确定加上了吗?再试试吧)") + + +# Add handlers +bot.add_handler(CommandHandler("start", start)) +bot.add_handler(CommandHandler("leaf", plant)) + + +@app.on_event("startup") +async def startup_event(): + """Starts the bot.""" + logger.info("Starting bot...") + await bot.initialize() + await bot.start() + await bot.updater.start_polling() + + +@app.on_event("shutdown") +async def shutdown_event(): + """Stops the bot on shutdown.""" + logger.info("Stopping bot...") + await bot.updater.stop() + await bot.stop() + await bot.shutdown() + + +layout_html = (Path(__file__).parent / "public" / "layout.html").read_text() +fmt_html = lambda x: layout_html.replace("{{CONTENT}}", x).replace("\n", "") + + +@app.get("/c/{channel}", response_class=HTMLResponse) +def channel_info(channel: str): + info = db.channel_info(channel) + + if not info: + return fmt_html(f""" +

TGCN 频道树!

+

频道 @{channel} 不在树上哦~

+ """) + + leaf_txt = '树枝' if info.children else '树叶' + + return fmt_html(f""" +

TGCN 频道树!

+

这里是频道 @{channel} - {info.name},{ + f'在频道树的第 {info.height + 1} 层,是 @{info.parent} (🔗) 的{leaf_txt}哦~' + if info.parent else "是树根哦~" + }

+ {f"""

下面这些是这个频道的树枝:

+ """ if info.children else "这个频道是树叶哦~"} +

(如果你也有公开频道,想成为这个频道的树叶的话,就去给 @tgtreebot 发送 /leaf {channel} {{你的频道名}} 吧! > <)

+ """) + + +app.mount("/", StaticFiles(directory="public", html=True)) + + +if __name__ == '__main__': + import uvicorn + + uvicorn.run(app, host="0.0.0.0", port=9498)