Skip to content

Commit 6f24f47

Browse files
committed
First Deploy
0 parents  commit 6f24f47

13 files changed

Lines changed: 827 additions & 0 deletions

File tree

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
bot_data.json
2+
.env
3+
__pycache__/
4+
.venv/

Dockerfile

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# 1. 基础镜像:使用与您本地一致的 Python 3.13 (精简版)
2+
FROM python:3.13-slim
3+
4+
# 2. 关键步骤:设置时区为上海 (UTC+8)
5+
# 这一步确保您的“凌晨4点清理”和“限流冷却”时间准确无误
6+
RUN apt-get update && apt-get install -y tzdata \
7+
&& ln -fs /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
8+
&& dpkg-reconfigure -f noninteractive tzdata \
9+
&& apt-get clean \
10+
&& rm -rf /var/lib/apt/lists/*
11+
12+
# 3. 设置容器内的工作目录
13+
WORKDIR /app
14+
15+
# 4. 复制依赖清单到容器中
16+
COPY requirements.txt .
17+
18+
# 5. 安装依赖
19+
# --no-cache-dir 可以减小镜像体积,加快构建速度
20+
RUN pip install --no-cache-dir -r requirements.txt
21+
22+
# 6. 复制当前目录下所有代码到容器中
23+
COPY . .
24+
25+
# 7. 启动命令
26+
CMD ["python", "main.py"]

config.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import os
2+
from dotenv import load_dotenv
3+
4+
load_dotenv()
5+
6+
BOT_TOKEN = os.getenv("BOT_TOKEN")
7+
ADMIN_ID_RAW = os.getenv("ADMIN_GROUP_ID")
8+
9+
try:
10+
ADMIN_ID = int(ADMIN_ID_RAW) if ADMIN_ID_RAW else None
11+
except:
12+
ADMIN_ID = None
13+
14+
DATA_FILE = "bot_data.json"
15+
16+
# 时间设置
17+
VERIFY_TIMEOUT = 60 # 验证超时 (秒)
18+
CACHE_TTL = 3600 # API缓存 (秒)
19+
BTN_RATE_LIMIT = 60 # 互动按钮冷却 (秒)
20+
CLEANUP_DAYS = 3 # 数据保留天数
21+
CLEANUP_TIME_HOUR = 4 # 每天清理时间 (小时, UTC+8)

database/storage.py

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import json
2+
import os
3+
import logging
4+
import time
5+
from config import DATA_FILE, CLEANUP_DAYS
6+
7+
logger = logging.getLogger("Database")
8+
9+
# 内存数据结构
10+
bot_data = {
11+
"groups": {},
12+
"pending_req": [],
13+
"msg_map": {}
14+
}
15+
16+
def load_data():
17+
global bot_data
18+
if not os.path.exists(DATA_FILE):
19+
save_data()
20+
return
21+
22+
try:
23+
with open(DATA_FILE, 'r', encoding='utf-8') as f:
24+
data = json.load(f)
25+
26+
# === 自动修复 Bug 1: 兼容旧数据 ===
27+
if "pending_req" in data:
28+
fixed_reqs = []
29+
for req in data["pending_req"]:
30+
# 如果旧数据用的是 'id',改为 'req_id'
31+
if "id" in req and "req_id" not in req:
32+
req["req_id"] = req.pop("id")
33+
fixed_reqs.append(req)
34+
data["pending_req"] = fixed_reqs
35+
# ================================
36+
37+
bot_data["groups"] = data.get("groups", {})
38+
bot_data["pending_req"] = data.get("pending_req", [])
39+
bot_data["msg_map"] = data.get("msg_map", {})
40+
41+
logger.info("✅ 数据加载并自动修复完成")
42+
except Exception as e:
43+
logger.error(f"❌ 加载失败: {e}")
44+
45+
def save_data():
46+
try:
47+
with open(DATA_FILE, 'w', encoding='utf-8') as f:
48+
json.dump(bot_data, f, ensure_ascii=False, indent=2)
49+
except Exception as e:
50+
logger.error(f"❌ 保存失败: {e}")
51+
52+
def cleanup_old_data(context=None):
53+
"""定时任务:清理超过3天的数据"""
54+
logger.info("🧹 开始执行每日数据清理...")
55+
now = time.time()
56+
expire_seconds = CLEANUP_DAYS * 86400
57+
58+
# 1. 清理消息映射 (msg_map)
59+
# msg_map 结构: {"admin_msg_id": {"map": {...}, "ts": 123456}}
60+
initial_len = len(bot_data["msg_map"])
61+
keys_to_del = []
62+
63+
for msg_id, info in bot_data["msg_map"].items():
64+
# 兼容旧数据(没有ts的)直接删,或者给个默认时间
65+
ts = info.get("ts", 0)
66+
if now - ts > expire_seconds:
67+
keys_to_del.append(msg_id)
68+
69+
for k in keys_to_del:
70+
del bot_data["msg_map"][k]
71+
72+
# 2. 清理过期申请
73+
# ... (申请量通常不大,暂不清理,以免误删未审批的)
74+
75+
if keys_to_del:
76+
save_data()
77+
logger.info(f"✅ 清理完成。删除了 {len(keys_to_del)} 条过期广播记录。")
78+
else:
79+
logger.info("✅ 暂无过期数据需要清理。")

handlers/__init__.py

Whitespace-only changes.

handlers/admin.py

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
from typing import List, Dict, Optional
2+
from datetime import datetime
3+
import html
4+
from telegram import (
5+
Update,
6+
InlineKeyboardButton,
7+
InlineKeyboardMarkup,
8+
BotCommand,
9+
BotCommandScopeChat,
10+
BotCommandScopeAllPrivateChats,
11+
BotCommandScopeAllChatAdministrators,
12+
BotCommandScopeAllGroupChats,
13+
BotCommandScopeDefault
14+
)
15+
from telegram.ext import ContextTypes, Application
16+
from telegram.constants import ParseMode
17+
from config import ADMIN_ID
18+
from database.storage import bot_data, save_data
19+
20+
# 检查管理员权限
21+
async def check_is_admin(update: Update) -> bool:
22+
if not ADMIN_ID: return False
23+
user_id = update.effective_user.id
24+
chat_id = update.effective_chat.id
25+
return user_id == ADMIN_ID or chat_id == ADMIN_ID
26+
27+
# ================= 补回丢失的函数:获取用户管理的群组 =================
28+
async def get_user_managed_groups(context: ContextTypes.DEFAULT_TYPE, user_id: int) -> List[dict]:
29+
"""获取用户有管理权的群组列表 (用于 /setup 菜单)"""
30+
managed = []
31+
# 遍历数据库中已登记的群组
32+
for chat_id_str, info in bot_data["groups"].items():
33+
try:
34+
# 调用 Telegram API 检查该用户在群里的身份
35+
member = await context.bot.get_chat_member(chat_id=int(chat_id_str), user_id=user_id)
36+
if member.status in ['creator', 'administrator']:
37+
managed.append({"id": chat_id_str, "title": info.get("title", "未知群组")})
38+
except Exception:
39+
# 如果Bot被踢了或者没权限看,跳过
40+
continue
41+
return managed
42+
# ===================================================================
43+
44+
async def post_init(application: Application):
45+
"""配置指令菜单 (Bot启动时执行)"""
46+
bot = application.bot
47+
await bot.delete_my_commands(scope=BotCommandScopeDefault())
48+
49+
# 1. 公共指令
50+
public_cmds = [
51+
BotCommand("fear", "😨 恐慌指数"),
52+
BotCommand("help", "💡 帮助")
53+
]
54+
await bot.set_my_commands(public_cmds, scope=BotCommandScopeAllGroupChats())
55+
56+
# 2. 私聊指令
57+
private_cmds = public_cmds + [
58+
BotCommand("start", "开始"),
59+
BotCommand("setup", "⚙️ 配置"),
60+
BotCommand("id", "获取ID")
61+
]
62+
await bot.set_my_commands(private_cmds, scope=BotCommandScopeAllPrivateChats())
63+
64+
# 3. 群管指令
65+
admin_cmds = public_cmds + [
66+
BotCommand("request_broadcast", "📢 申请广播"),
67+
BotCommand("id", "🆔 群ID")
68+
]
69+
await bot.set_my_commands(admin_cmds, scope=BotCommandScopeAllChatAdministrators())
70+
71+
# 4. 终极管理员指令
72+
if ADMIN_ID:
73+
root_cmds = public_cmds + [
74+
BotCommand("report", "📊 审计"),
75+
BotCommand("revoke", "🚫 移除"),
76+
BotCommand("id", "🆔 获取ID")
77+
]
78+
try:
79+
await bot.set_my_commands(root_cmds, scope=BotCommandScopeChat(chat_id=ADMIN_ID))
80+
except Exception:
81+
pass
82+
83+
# 申请广播
84+
async def cmd_request_broadcast(update: Update, context: ContextTypes.DEFAULT_TYPE):
85+
chat = update.effective_chat
86+
if chat.type == 'private' or not ADMIN_ID: return
87+
88+
req_id = f"{chat.id}_{int(datetime.now().timestamp())}"
89+
bot_data["pending_req"].append({
90+
"req_id": req_id,
91+
"chat_id": chat.id,
92+
"title": chat.title
93+
})
94+
save_data()
95+
96+
kb = [[InlineKeyboardButton("✅ 批准", callback_data=f"appr_yes_{req_id}"), InlineKeyboardButton("❌ 拒绝", callback_data=f"appr_no_{req_id}")]]
97+
await context.bot.send_message(ADMIN_ID, f"📩 **广播申请**\n群组: {chat.title}", reply_markup=InlineKeyboardMarkup(kb), parse_mode=ParseMode.MARKDOWN)
98+
await update.message.reply_text("✅ 已提交申请,等待审核。")
99+
100+
# 审批回调
101+
async def callback_approval(update: Update, context: ContextTypes.DEFAULT_TYPE):
102+
query = update.callback_query
103+
parts = query.data.split("_", 2)
104+
action = parts[1]
105+
req_id = parts[2]
106+
107+
req = next((r for r in bot_data["pending_req"] if r.get("req_id") == req_id), None)
108+
109+
if not req:
110+
await query.edit_message_text("⚠️ 请求失效或已处理")
111+
return
112+
113+
if action == "yes":
114+
cid = str(req["chat_id"])
115+
if cid not in bot_data["groups"]: bot_data["groups"][cid] = {"title": req["title"]}
116+
117+
bot_data["groups"][cid].update({
118+
"is_broadcast": True,
119+
"is_paused": False,
120+
"sub_news": True, "sub_marketing": True, "sub_analysis": True, "sub_signal": True
121+
})
122+
save_data()
123+
await query.edit_message_text(f"✅ 已批准: {req['title']}")
124+
try: await context.bot.send_message(req["chat_id"], "🎉 恭喜!广播申请已通过。管理员请私聊 Bot 使用 /setup 配置。")
125+
except: pass
126+
else:
127+
await query.edit_message_text(f"❌ 已拒绝: {req['title']}")
128+
129+
bot_data["pending_req"].remove(req)
130+
save_data()
131+
132+
async def cmd_report(update: Update, context: ContextTypes.DEFAULT_TYPE):
133+
if not await check_is_admin(update): return
134+
txt = "📊 **群组审计**\n\n"
135+
for cid, i in bot_data["groups"].items():
136+
if not i.get("is_broadcast"): continue
137+
status = "🔴暂停" if i.get("is_paused") else "🟢正常"
138+
txt += f"**{html.escape(i['title'])}**\nID: `{cid}`\n状态: {status}\n话题: News:`{i.get('topic_news')}` Sig:`{i.get('topic_signal')}`\n\n"
139+
if len(txt) > 4000: txt = txt[:4000]
140+
await update.message.reply_text(txt, parse_mode=ParseMode.MARKDOWN)
141+
142+
async def cmd_revoke(update: Update, context: ContextTypes.DEFAULT_TYPE):
143+
if not await check_is_admin(update) or not context.args: return
144+
tid = context.args[0]
145+
if tid in bot_data["groups"]:
146+
bot_data["groups"][tid]["is_broadcast"] = False
147+
save_data()
148+
await update.message.reply_text(f"✅ 已移除权限: {tid}")

0 commit comments

Comments
 (0)