Rename to hkt.sh

This commit is contained in:
mango
2026-03-21 01:10:53 +08:00
parent 76a263d0f9
commit 8f1171fe99
6676 changed files with 1724268 additions and 0 deletions

View File

@@ -0,0 +1,339 @@
#!/usr/bin/env python3
"""VPS 到期提醒 Telegram Bot"""
import os, json, subprocess
from datetime import datetime
from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup
from telegram.ext import Application, CommandHandler, CallbackQueryHandler, MessageHandler, ConversationHandler, filters, ContextTypes
from dotenv import load_dotenv
load_dotenv()
TOKEN = os.getenv("BOT_TOKEN")
ADMIN = int(os.getenv("ADMIN_ID", "0"))
DATA_FILE = os.path.join(os.path.dirname(__file__), "data.json")
def load_data():
try:
with open(DATA_FILE) as f: return json.load(f)
except: return {"vps": [], "remind_days": [1, 3, 7]}
def save_data(data):
with open(DATA_FILE, "w") as f: json.dump(data, f, ensure_ascii=False, indent=2)
def parse_date(text):
"""解析日期简写: 0315 -> 2026-03-15, 3-15 -> 2026-03-15"""
text = text.strip().replace("/", "-").replace(".", "-")
year = datetime.now().year
if len(text) == 4 and text.isdigit():
return f"{year}-{text[:2]}-{text[2:]}"
if len(text) in [3,4] and "-" in text:
parts = text.split("-")
return f"{year}-{parts[0].zfill(2)}-{parts[1].zfill(2)}"
if len(text) == 10:
return text
return text
def days_left(d):
return (datetime.strptime(d, "%Y-%m-%d") - datetime.now()).days
def ping_host(ip):
try:
r = subprocess.run(["ping", "-c", "1", "-W", "2", ip], capture_output=True, timeout=5)
return r.returncode == 0
except: return False
def calc_next_date(date_str, cycle):
"""根据周期计算下一个到期日"""
from dateutil.relativedelta import relativedelta
dt = datetime.strptime(date_str, "%Y-%m-%d")
c = cycle.lower().strip()
if c in ("", "monthly", "month", "1m"):
return (dt + relativedelta(months=1)).strftime("%Y-%m-%d")
elif c in ("", "quarterly", "quarter", "3m"):
return (dt + relativedelta(months=3)).strftime("%Y-%m-%d")
elif c in ("半年", "semi", "6m"):
return (dt + relativedelta(months=6)).strftime("%Y-%m-%d")
elif c in ("", "yearly", "annual", "12m"):
return (dt + relativedelta(years=1)).strftime("%Y-%m-%d")
return None
def main_kb():
return InlineKeyboardMarkup([
[InlineKeyboardButton("📋 VPS列表", callback_data="list")],
[InlineKeyboardButton(" 添加", callback_data="add"), InlineKeyboardButton("✏️ 编辑", callback_data="edit"), InlineKeyboardButton("🗑 删除", callback_data="del")],
[InlineKeyboardButton("📡 Ping全部", callback_data="ping"), InlineKeyboardButton("🔔 测试", callback_data="test"), InlineKeyboardButton("⚙️ 设置", callback_data="settings")]
])
def back_kb():
return InlineKeyboardMarkup([[InlineKeyboardButton("🔙 返回", callback_data="back")]])
async def start(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
await update.message.reply_text("🖥 *VPS 到期提醒*", parse_mode="Markdown", reply_markup=main_kb())
async def button(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
q = update.callback_query
await q.answer()
data = q.data
if data == "back":
await q.edit_message_text("🖥 *VPS 到期提醒*", parse_mode="Markdown", reply_markup=main_kb())
elif data == "list":
d = load_data()
vps = d.get("vps", [])
if not vps:
await q.edit_message_text("📭 暂无VPS", reply_markup=back_kb())
return
groups = {}
for i, v in enumerate(vps):
p = v.get("provider", "未知")
if p not in groups: groups[p] = []
groups[p].append((i, v))
msg = "📋 *VPS 列表*\n"
renew_btns = []
for provider, items in groups.items():
msg += f"\n🏢 *{provider}*\n"
for i, v in sorted(items, key=lambda x: days_left(x[1]["date"])):
dl = days_left(v["date"])
if dl < 0:
icon, dl_text = "", f"已过期{-dl}"
renew_btns.append(InlineKeyboardButton(f"✅ 续费 {v['name']}", callback_data=f"renew_{i}"))
elif dl <= 3:
icon, dl_text = "🔴", f"{dl}"
renew_btns.append(InlineKeyboardButton(f"✅ 续费 {v['name']}", callback_data=f"renew_{i}"))
elif dl <= 7:
icon, dl_text = "🟡", f"{dl}"
else:
icon, dl_text = "🟢", f"{dl}"
msg += f"{icon} {v['name']} - {v['date']} ({dl_text})\n"
kb = [[b] for b in renew_btns]
kb.append([InlineKeyboardButton("🔙 返回", callback_data="back")])
await q.edit_message_text(msg, parse_mode="Markdown", reply_markup=InlineKeyboardMarkup(kb))
elif data.startswith("renew_"):
idx = int(data.split("_")[1])
d = load_data()
v = d["vps"][idx]
cycle = v.get("cycle", "")
next_date = calc_next_date(v["date"], cycle)
kb = []
if next_date:
kb.append([InlineKeyboardButton(f"🔄 自动续期 → {next_date}", callback_data=f"renewauto_{idx}")])
kb.append([InlineKeyboardButton("📝 手动输入日期", callback_data=f"renewmanual_{idx}")])
kb.append([InlineKeyboardButton("🔙 返回", callback_data="list")])
msg = f"✅ 续费 *{v['name']}*\n\n当前到期: {v['date']}\n周期: {cycle}"
if next_date:
msg += f"\n自动续期: {next_date}"
await q.edit_message_text(msg, parse_mode="Markdown", reply_markup=InlineKeyboardMarkup(kb))
elif data.startswith("renewauto_"):
idx = int(data.split("_")[1])
d = load_data()
v = d["vps"][idx]
old_date = v["date"]
new_date = calc_next_date(v["date"], v.get("cycle", ""))
if new_date:
v["date"] = new_date
save_data(d)
await q.edit_message_text(f"✅ *{v['name']}* 已续费\n\n旧日期: {old_date}\n新日期: {new_date}", parse_mode="Markdown", reply_markup=back_kb())
else:
await q.edit_message_text("❌ 无法计算,请手动输入", reply_markup=back_kb())
elif data.startswith("renewmanual_"):
idx = int(data.split("_")[1])
ctx.user_data["renew_idx"] = idx
ctx.user_data["step"] = "renew_date"
d = load_data()
v = d["vps"][idx]
await q.edit_message_text(f"📝 续费 *{v['name']}*\n\n请输入新的到期日期 (如 0401 或 2026-04-01):", parse_mode="Markdown")
elif data == "settings":
d = load_data()
days = d.get("remind_days", [1, 3, 7])
kb = [[InlineKeyboardButton(f"{'' if i in days else ''} {i}", callback_data=f"toggle_{i}") for i in [1, 3, 7]],
[InlineKeyboardButton(f"{'' if i in days else ''} {i}", callback_data=f"toggle_{i}") for i in [14, 30]],
[InlineKeyboardButton("🔙 返回", callback_data="back")]]
await q.edit_message_text(f"⚙️ 提醒天数设置\n当前: {days}", reply_markup=InlineKeyboardMarkup(kb))
elif data.startswith("toggle_"):
day = int(data.split("_")[1])
d = load_data()
days = d.get("remind_days", [1, 3, 7])
if day in days: days.remove(day)
else: days.append(day)
days.sort()
d["remind_days"] = days
save_data(d)
kb = [[InlineKeyboardButton(f"{'' if i in days else ''} {i}", callback_data=f"toggle_{i}") for i in [1, 3, 7]],
[InlineKeyboardButton(f"{'' if i in days else ''} {i}", callback_data=f"toggle_{i}") for i in [14, 30]],
[InlineKeyboardButton("🔙 返回", callback_data="back")]]
await q.edit_message_text(f"⚙️ 提醒天数设置\n当前: {days}", reply_markup=InlineKeyboardMarkup(kb))
elif data == "edit":
d = load_data()
vps = d.get("vps", [])
if not vps:
await q.edit_message_text("📭 暂无VPS", reply_markup=back_kb())
return
kb = [[InlineKeyboardButton(f"✏️ {v['name']}", callback_data=f"editvps_{i}")] for i, v in enumerate(vps)]
kb.append([InlineKeyboardButton("🔙 返回", callback_data="back")])
await q.edit_message_text("选择要编辑的VPS:", reply_markup=InlineKeyboardMarkup(kb))
elif data.startswith("editvps_"):
idx = int(data.split("_")[1])
ctx.user_data["edit_idx"] = idx
d = load_data()
v = d["vps"][idx]
kb = [
[InlineKeyboardButton("📝 名称", callback_data="ed_name"), InlineKeyboardButton("🏢 商家", callback_data="ed_provider")],
[InlineKeyboardButton("🌐 IP", callback_data="ed_ip"), InlineKeyboardButton("📅 日期", callback_data="ed_date")],
[InlineKeyboardButton("💰 价格", callback_data="ed_price"), InlineKeyboardButton("🔄 周期", callback_data="ed_cycle")],
[InlineKeyboardButton("🔙 返回", callback_data="edit")]
]
msg = f"✏️ 编辑 *{v['name']}*\n\n"
msg += f"🏢 商家: {v.get('provider','')}\n"
msg += f"🌐 IP: {v.get('ip','')}\n"
msg += f"📅 日期: {v.get('date','')}\n"
msg += f"💰 价格: {v.get('price','')}\n"
msg += f"🔄 周期: {v.get('cycle','')}"
await q.edit_message_text(msg, parse_mode="Markdown", reply_markup=InlineKeyboardMarkup(kb))
elif data.startswith("ed_"):
field = data.split("_")[1]
ctx.user_data["edit_field"] = field
names = {"name":"名称","provider":"商家","ip":"IP","date":"日期","price":"价格","cycle":"周期"}
await q.edit_message_text(f"请输入新的{names.get(field,field)}:")
elif data == "del":
d = load_data()
vps = d.get("vps", [])
if not vps:
await q.edit_message_text("📭 暂无VPS", reply_markup=back_kb())
return
kb = [[InlineKeyboardButton(f"🗑 {v['name']}", callback_data=f"delvps_{i}")] for i, v in enumerate(vps)]
kb.append([InlineKeyboardButton("🔙 返回", callback_data="back")])
await q.edit_message_text("选择要删除的VPS:", reply_markup=InlineKeyboardMarkup(kb))
elif data.startswith("delvps_"):
idx = int(data.split("_")[1])
d = load_data()
if 0 <= idx < len(d["vps"]):
removed = d["vps"].pop(idx)
save_data(d)
await q.edit_message_text(f"✅ 已删除: {removed['name']}", reply_markup=back_kb())
elif data == "test":
d = load_data()
vps = d.get("vps", [])
if not vps:
await q.edit_message_text("📭 暂无VPS可测试", reply_markup=back_kb())
return
v = min(vps, key=lambda x: days_left(x["date"]))
dl = days_left(v["date"])
msg = f"🔔 *测试提醒*\n\n{v['name']} ({v['provider']})\n📅 {v['date']} (还剩 {dl} 天)"
await q.message.reply_text(msg, parse_mode="Markdown")
await q.edit_message_text("✅ 测试提醒已发送", reply_markup=back_kb())
elif data == "ping":
d = load_data()
vps = [v for v in d.get("vps", []) if v.get("ip")]
if not vps:
await q.edit_message_text("📭 没有可ping的VPS", reply_markup=back_kb())
return
await q.edit_message_text("📡 正在检测...")
msg = "📡 *Ping 结果*\n\n"
for v in vps:
ok = ping_host(v["ip"])
msg += f"{'🟢' if ok else '🔴'} {v['name']} - `{v['ip']}`\n"
await q.edit_message_text(msg, parse_mode="Markdown", reply_markup=back_kb())
elif data == "add":
ctx.user_data["step"] = "name"
await q.edit_message_text("📝 请输入 VPS 名称:")
async def handle_msg(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
step = ctx.user_data.get("step")
text = update.message.text.strip()
# 编辑模式
if ctx.user_data.get("edit_field"):
idx = ctx.user_data.get("edit_idx", 0)
field = ctx.user_data["edit_field"]
d = load_data()
if field == "date":
text = parse_date(text)
d["vps"][idx][field] = text
save_data(d)
ctx.user_data.clear()
await update.message.reply_text("✅ 已更新", reply_markup=main_kb())
return
# 续费模式
if step == "renew_date":
idx = ctx.user_data.get("renew_idx", 0)
d = load_data()
v = d["vps"][idx]
old_date = v["date"]
v["date"] = parse_date(text)
save_data(d)
ctx.user_data.clear()
await update.message.reply_text(f"✅ *{v['name']}* 已续费\n旧日期: {old_date}\n新日期: {v['date']}", parse_mode="Markdown", reply_markup=main_kb())
return
return
if step == "name":
ctx.user_data["name"] = text
ctx.user_data["step"] = "provider"
await update.message.reply_text("🏢 请输入商家名称:")
elif step == "provider":
ctx.user_data["provider"] = text
ctx.user_data["step"] = "ip"
await update.message.reply_text("🌐 请输入 IP (没有输入 0):")
elif step == "ip":
ctx.user_data["ip"] = None if text == "0" else text
ctx.user_data["step"] = "cycle"
await update.message.reply_text("🔄 付款周期 (月/季/年):")
elif step == "cycle":
ctx.user_data["cycle"] = text
ctx.user_data["step"] = "date"
await update.message.reply_text("📅 到期日期 (YYYY-MM-DD):")
elif step == "date":
ctx.user_data["date"] = text
ctx.user_data["step"] = "price"
await update.message.reply_text("💰 价格 (如 /月,没有输入 0):")
elif step == "price":
ctx.user_data["price"] = None if text == "0" else text
d = load_data()
d["vps"].append({
"name": ctx.user_data["name"],
"provider": ctx.user_data["provider"],
"ip": ctx.user_data["ip"],
"cycle": ctx.user_data["cycle"],
"date": ctx.user_data["date"],
"price": ctx.user_data["price"]
})
save_data(d)
ctx.user_data.clear()
await update.message.reply_text(f"✅ 已添加: {ctx.user_data.get('name', 'VPS')}", reply_markup=main_kb())
async def check_expire(ctx: ContextTypes.DEFAULT_TYPE):
d = load_data()
remind_days = d.get("remind_days", [1, 3, 7])
for v in d.get("vps", []):
dl = days_left(v["date"])
if dl in remind_days:
msg = f"⏰ *到期提醒*\n\n{v['name']} ({v['provider']})\n📅 {v['date']} (还剩 {dl} 天)"
await ctx.bot.send_message(ADMIN, msg, parse_mode="Markdown")
def main():
app = Application.builder().token(TOKEN).build()
app.add_handler(CommandHandler("start", start))
app.add_handler(CommandHandler("help", start))
app.add_handler(CallbackQueryHandler(button))
app.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, handle_msg))
app.job_queue.run_daily(check_expire, time=datetime.strptime("09:00", "%H:%M").time())
print("Bot 启动")
app.run_polling(drop_pending_updates=True)
if __name__ == "__main__":
main()