[O] Save state, water info, unknown message feedback

This commit is contained in:
2026-03-12 04:24:21 -04:00
parent 382949ff88
commit 24ca6db6fa
2 changed files with 63 additions and 14 deletions
+58 -11
View File
@@ -1,4 +1,5 @@
import html
import json
import re
import time
import urllib.parse
@@ -30,8 +31,28 @@ channels_dir = ensure_dir(data_dir / "channels")
validating = set()
# State tracking for tree hole conversations
# user_id -> {"action": "treehole"|"reply", "channel": str, "sender_id": int (for reply)}
user_states: dict[int, dict] = {}
# user_id -> {"action": "treehole"|"reply"|"leaf", ...}
states_file = data_dir / "user_states.json"
def load_states() -> dict[int, dict]:
if states_file.exists():
raw = json.loads(states_file.read_text(encoding='utf-8'))
return {int(k): v for k, v in raw.items()}
return {}
def set_state(user_id: int, state: dict | None):
"""Set or clear the state for a user, and persist to disk."""
if state is None:
user_states.pop(user_id, None)
else:
user_states[user_id] = state
states_file.write_text(json.dumps({str(k): v for k, v in user_states.items()},
ensure_ascii=False), encoding='utf-8')
user_states: dict[int, dict] = load_states()
# Rate limiting for tree hole messages: user_id -> last send timestamp
treehole_rate_limit: dict[int, float] = {}
@@ -157,7 +178,7 @@ async def handle_leaf(update: Update, user_id: int, parent: str):
if not db.channel_info(parent):
return await update.message.reply_text("上级频道还不在树上... 是不是打错了 qwq")
user_states[user_id] = {"action": "leaf", "parent": parent}
set_state(user_id, {"action": "leaf", "parent": parent})
await update.message.reply_html(f"🌿 <b>成为树叶</b>\n\n你想让哪个频道成为 @{parent} 的树叶呢?(请发送你的频道的 @用户名)")
@@ -205,7 +226,7 @@ async def handle_treehole(update: Update, user_id: int, channel: str):
remaining = int(TREEHOLE_COOLDOWN - (time.time() - last_time))
return await update.message.reply_text(f"发送太频繁了,请 {remaining} 秒后再试~")
user_states[user_id] = {"action": "treehole", "channel": channel}
set_state(user_id, {"action": "treehole", "channel": channel})
return await update.message.reply_html(f"🕳️ <b>树洞模式</b>\n\n想对频道 @{channel} 的主人说什么呢?"
f"(发送文字消息即可,消息将会匿名发送)")
@@ -229,7 +250,7 @@ async def reply_callback(update: Update, context: ContextTypes.DEFAULT_TYPE):
if actual_owner != owner_id:
return await query.answer("只有频道主人才能回复哦~", show_alert=False)
user_states[owner_id] = {"action": "reply", "sender_id": sender_id, "channel": channel}
set_state(owner_id, {"action": "reply", "sender_id": sender_id, "channel": channel})
await query.answer()
return await context.bot.send_message(
chat_id=owner_id,
@@ -271,7 +292,8 @@ async def handle_message(update: Update, context: ContextTypes.DEFAULT_TYPE):
state = user_states.get(user_id)
if not state:
return
return await update.message.reply_text("🌳 不太明白你在说什么哦~ 请点击频道消息上的按钮来互动吧!\n"
"(如果哪里不对的话,应该是 bot 重启了,重新点击按钮就好 ;-;)")
if state["action"] == "leaf":
parent = state["parent"]
@@ -296,7 +318,7 @@ async def handle_message(update: Update, context: ContextTypes.DEFAULT_TYPE):
"以及需要输入频道的 @用户名,不是显示名哦~")
# Success paths — clear state
del user_states[user_id]
set_state(user_id, None)
info = utils.extract_meta_tags(text)
if sha in text:
@@ -316,13 +338,13 @@ async def handle_message(update: Update, context: ContextTypes.DEFAULT_TYPE):
不过上树之前,为了防止被滥用,需要先验证一下你是 {channel} 的管理员...
请编辑频道简介加入验证码 {sha} 再点击下面的「添加好了」吧~(加在哪里都可以的 > < 验证完就可以删掉)
""".strip(), reply_markup=verify_btn)
请编辑频道简介加入验证码 <code>{sha}</code> 再点击下面的「添加好了」吧~(加在哪里都可以的 > < 验证完就可以删掉)
""".strip(), reply_markup=verify_btn, parse_mode="HTML")
validating.add(sha)
elif state["action"] == "treehole":
channel = state["channel"]
del user_states[user_id]
set_state(user_id, None)
# Update rate limit
treehole_rate_limit[user_id] = time.time()
@@ -376,7 +398,7 @@ async def handle_message(update: Update, context: ContextTypes.DEFAULT_TYPE):
elif state["action"] == "reply":
sender_id = state["sender_id"]
channel = state["channel"]
del user_states[user_id]
set_state(user_id, None)
try:
escaped_text = html.escape(update.message.text)
@@ -435,6 +457,10 @@ async def verify_callback(update: Update, context: ContextTypes.DEFAULT_TYPE):
text = channel_html(channel)
if sha in text:
# Check if already registered (e.g. double-click)
if db.channel_info(channel):
return await query.answer("这个频道已经在树上了哦~", show_alert=False)
info = utils.extract_meta_tags(text)
title = info.title or channel
logger.info(f"> 🌿 Registering channel {channel} with parent {parent}.")
@@ -482,6 +508,24 @@ layout_html = (Path(__file__).parent.parent / "public" / "layout.html").read_tex
fmt_html = lambda x: layout_html.replace("{{CONTENT}}", x).replace("\n", "")
def tree_to_dict(channel: str) -> dict | None:
"""Recursively build a dict representation of the tree for the API."""
info = db.channel_info(channel)
if not info:
return None
return {
"username": channel,
"name": info.name,
"water": db.get_votes(channel),
"children": [tree_to_dict(c.username) for c in info.children],
}
@app.get("/api/tree")
def api_tree():
return tree_to_dict("azaneko")
@app.get("/c/{channel}", response_class=HTMLResponse)
def channel_info(channel: str):
info = db.channel_info(channel)
@@ -493,6 +537,8 @@ def channel_info(channel: str):
""")
leaf_txt = '树枝' if info.children else '树叶'
votes = db.get_votes(channel)
water_html = f'<p>💧 这个频道已经被浇了 {votes} 次水~</p>' if votes else ''
return fmt_html(f"""
<h1>TGCN 频道树!</h1>
@@ -500,6 +546,7 @@ def channel_info(channel: str):
f'在频道树的第 {info.height + 1} 层,是 <a href="https://t.me/{info.parent}">@{info.parent}</a> (<a href="/c/{info.parent}">🔗</a>) 的{leaf_txt}哦~'
if info.parent else "是树根哦~"
}</p>
{water_html}
{f"""<p>下面这些是这个频道的树枝:</p>
<ul>
{"".join(f'<li><a href="https://t.me/{child.username}">@{child.username}</a> (<a href="/c/{child.username}">🔗</a>) - {child.name}</li>' for child in info.children)}
+5 -3
View File
@@ -16,7 +16,9 @@ def indent(string: str, level: int):
def dfs(channel: str):
info = db.channel_info(channel)
out = f"""<span class="tree l{info.height}"><a href="https://t.me/{channel}">@{channel}</a> - {info.name}</span>\n"""
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"""
if not info.children:
return out
@@ -37,5 +39,5 @@ if __name__ == '__main__':
while True:
print("Generating tree...")
gen_tree()
print("Done! Sleeping for 60 seconds...")
time.sleep(60)
print("Done! Sleeping for 10 seconds...")
time.sleep(10)