180 lines
6.6 KiB
Python
180 lines
6.6 KiB
Python
#!/usr/bin/env python3
|
||
"""
|
||
Telegram 订阅处理器
|
||
每 30 分钟由 GitHub Actions 触发,处理 @GGmanm_bot 的订阅/退订请求。
|
||
|
||
用户命令:
|
||
/subscribe 或 /start subscribe → 加入每日投资早报
|
||
/unsubscribe → 取消订阅
|
||
/start → 欢迎消息 + 订阅说明
|
||
"""
|
||
|
||
import json
|
||
import os
|
||
import sys
|
||
from datetime import datetime, timezone
|
||
from pathlib import Path
|
||
|
||
import requests
|
||
|
||
# ─── Config ──────────────────────────────────────────────────────────────────
|
||
TELEGRAM_TOKEN = os.environ.get("TELEGRAM_TOKEN", "8518954174:AAHIWuxR4DDTtqxqjFzeUi33WxFUtnLyQQc")
|
||
API_BASE = f"https://api.telegram.org/bot{TELEGRAM_TOKEN}"
|
||
|
||
ROOT_DIR = Path(__file__).resolve().parent.parent
|
||
SUBS_FILE = ROOT_DIR / "data" / "investment-subscribers.json"
|
||
OFFSET_FILE = ROOT_DIR / "data" / "telegram-offset.json"
|
||
|
||
WELCOME_MSG = """👋 欢迎关注 *金融日报* @GGmanm_bot!
|
||
|
||
📊 每个交易日自动推送每日投资早报,包含:
|
||
• 全球主要指数涨跌
|
||
• AI & 科技 / 新能源 / 加密行情
|
||
• A股 / 港股最新动态
|
||
• 大宗商品(黄金、原油、铜)
|
||
• 趋势判断 + 市场情绪
|
||
|
||
发送 /subscribe 开始接收每日推送
|
||
发送 /unsubscribe 取消订阅
|
||
|
||
🌐 网页版:https://yang1bai.github.io/finance-daily-site/"""
|
||
|
||
SUBSCRIBE_OK = "✅ 订阅成功!明天早上 8:30 Toronto 时间,你将收到第一份每日投资早报。\n\n发送 /unsubscribe 可随时取消。"
|
||
ALREADY_SUBBED = "👌 你已经订阅了,无需重复操作。\n\n发送 /unsubscribe 可取消订阅。"
|
||
UNSUBSCRIBE_OK = "🔕 已取消订阅,你不会再收到每日早报推送。\n\n发送 /subscribe 可随时重新订阅。"
|
||
NOT_SUBBED = "ℹ️ 你当前没有活跃订阅。\n\n发送 /subscribe 开始接收每日投资早报。"
|
||
|
||
|
||
# ─── Helpers ─────────────────────────────────────────────────────────────────
|
||
|
||
def load_subscribers() -> dict:
|
||
try:
|
||
return json.loads(SUBS_FILE.read_text(encoding="utf-8"))
|
||
except Exception:
|
||
return {"subscribers": {}, "stats": {}}
|
||
|
||
|
||
def save_subscribers(data: dict):
|
||
SUBS_FILE.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")
|
||
|
||
|
||
def load_offset() -> int:
|
||
try:
|
||
return json.loads(OFFSET_FILE.read_text())["offset"]
|
||
except Exception:
|
||
return 0
|
||
|
||
|
||
def save_offset(offset: int):
|
||
OFFSET_FILE.write_text(json.dumps({"offset": offset}))
|
||
|
||
|
||
def send_message(chat_id: str | int, text: str):
|
||
resp = requests.post(f"{API_BASE}/sendMessage", json={
|
||
"chat_id": chat_id,
|
||
"text": text,
|
||
"parse_mode": "Markdown",
|
||
}, timeout=10)
|
||
return resp.ok
|
||
|
||
|
||
def get_updates(offset: int) -> list:
|
||
resp = requests.get(f"{API_BASE}/getUpdates", params={
|
||
"offset": offset,
|
||
"timeout": 5,
|
||
"limit": 100,
|
||
}, timeout=15)
|
||
if resp.ok:
|
||
return resp.json().get("result", [])
|
||
return []
|
||
|
||
|
||
# ─── Main ─────────────────────────────────────────────────────────────────────
|
||
|
||
def main():
|
||
print("🤖 Telegram Subscription Handler starting...")
|
||
offset = load_offset()
|
||
updates = get_updates(offset)
|
||
|
||
if not updates:
|
||
print(" ✅ No new updates")
|
||
save_offset(offset) # ensure file exists even when idle
|
||
return
|
||
|
||
data = load_subscribers()
|
||
subs = data.setdefault("subscribers", {})
|
||
changed = False
|
||
|
||
for update in updates:
|
||
update_id = update["update_id"]
|
||
offset = update_id + 1
|
||
|
||
msg = update.get("message") or update.get("edited_message")
|
||
if not msg:
|
||
continue
|
||
|
||
chat_id = str(msg["chat"]["id"])
|
||
text = (msg.get("text") or "").strip()
|
||
name = msg.get("from", {}).get("first_name", "朋友")
|
||
|
||
print(f" 📨 [{chat_id}] {text!r}")
|
||
|
||
# /start (with optional deep-link payload)
|
||
if text.startswith("/start"):
|
||
payload = text[6:].strip()
|
||
if payload in ("subscribe", "sub"):
|
||
# Deep-link subscribe
|
||
if subs.get(chat_id, {}).get("active"):
|
||
send_message(chat_id, ALREADY_SUBBED)
|
||
else:
|
||
subs[chat_id] = {"active": True, "joined": datetime.now(timezone.utc).strftime("%Y-%m-%d"), "name": name}
|
||
send_message(chat_id, SUBSCRIBE_OK)
|
||
changed = True
|
||
print(f" ✅ Subscribed: {chat_id}")
|
||
else:
|
||
send_message(chat_id, WELCOME_MSG)
|
||
|
||
# /subscribe
|
||
elif text in ("/subscribe", "/sub"):
|
||
if subs.get(chat_id, {}).get("active"):
|
||
send_message(chat_id, ALREADY_SUBBED)
|
||
else:
|
||
subs[chat_id] = {"active": True, "joined": datetime.now(timezone.utc).strftime("%Y-%m-%d"), "name": name}
|
||
send_message(chat_id, SUBSCRIBE_OK)
|
||
changed = True
|
||
print(f" ✅ Subscribed: {chat_id}")
|
||
|
||
# /unsubscribe
|
||
elif text in ("/unsubscribe", "/unsub", "/stop"):
|
||
if subs.get(chat_id, {}).get("active"):
|
||
subs[chat_id]["active"] = False
|
||
subs[chat_id]["left"] = datetime.now(timezone.utc).strftime("%Y-%m-%d")
|
||
send_message(chat_id, UNSUBSCRIBE_OK)
|
||
changed = True
|
||
print(f" 🔕 Unsubscribed: {chat_id}")
|
||
else:
|
||
send_message(chat_id, NOT_SUBBED)
|
||
|
||
# /status
|
||
elif text == "/status":
|
||
sub_info = subs.get(chat_id)
|
||
if sub_info and sub_info.get("active"):
|
||
stats = data.get("stats", {})
|
||
last_bc = stats.get("last_broadcast", "暂无")
|
||
msg_text = f"✅ 你已订阅每日投资早报\n📅 加入时间:{sub_info.get('joined','未知')}\n📡 上次推送:{last_bc}"
|
||
else:
|
||
msg_text = "❌ 你当前未订阅\n\n发送 /subscribe 开始接收每日早报"
|
||
send_message(chat_id, msg_text)
|
||
|
||
save_offset(offset)
|
||
if changed:
|
||
save_subscribers(data)
|
||
count = sum(1 for s in subs.values() if s.get("active"))
|
||
print(f" 💾 Saved subscribers ({count} active)")
|
||
else:
|
||
print(f" ℹ️ No subscription changes ({sum(1 for s in subs.values() if s.get('active'))} active)")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|