feat: add investment broadcast, subscription system & GoatCounter analytics
- scripts/daily-investment.py: Telegram 每日投资早报 (yfinance, 8 类别, 趋势分析) - scripts/handle_subscriptions.py: Telegram bot 订阅处理器 (/subscribe /unsubscribe) - data/investment-subscribers.json: 订阅者数据 - .github/workflows/investment-broadcast.yml: 每日 8:30 AM Toronto 推送 - .github/workflows/subscription-handler.yml: 每 30 分钟处理订阅请求 - index.html: 加入 Telegram 订阅按钮 + 订阅人数显示 + GoatCounter 统计
This commit is contained in:
@@ -0,0 +1,41 @@
|
||||
name: Investment Daily Broadcast
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# 8:30 AM Toronto (EDT = UTC-4) → 12:30 UTC, weekdays
|
||||
- cron: '30 12 * * 1-5'
|
||||
# Weekend: 9:00 AM (13:00 UTC)
|
||||
- cron: '0 13 * * 0,6'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
broadcast:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
TELEGRAM_TOKEN: ${{ secrets.TELEGRAM_TOKEN }}
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.11'
|
||||
|
||||
- name: Install dependencies
|
||||
run: pip install yfinance requests pytz
|
||||
|
||||
- name: Run investment broadcast
|
||||
run: python scripts/daily-investment.py
|
||||
|
||||
- name: Commit subscriber stats
|
||||
run: |
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
git add data/investment-subscribers.json
|
||||
git diff --cached --quiet || git commit -m "chore: broadcast stats $(date -u +'%Y-%m-%d %H:%M UTC')"
|
||||
git push
|
||||
@@ -0,0 +1,39 @@
|
||||
name: Subscription Handler
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# Every 30 minutes
|
||||
- cron: '*/30 * * * *'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
handle:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
TELEGRAM_TOKEN: ${{ secrets.TELEGRAM_TOKEN }}
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.11'
|
||||
|
||||
- name: Install dependencies
|
||||
run: pip install requests
|
||||
|
||||
- name: Process subscription updates
|
||||
run: python scripts/handle_subscriptions.py
|
||||
|
||||
- name: Commit changes
|
||||
run: |
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
git add data/investment-subscribers.json data/telegram-offset.json
|
||||
git diff --cached --quiet || git commit -m "chore: subscription update $(date -u +'%H:%M UTC')"
|
||||
git push
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"subscribers": {
|
||||
"8727904480": {
|
||||
"active": true,
|
||||
"joined": "2026-05-03",
|
||||
"name": "Yang Bai"
|
||||
}
|
||||
},
|
||||
"stats": {
|
||||
"total_broadcasts": 0,
|
||||
"last_broadcast": null,
|
||||
"broadcast_history": []
|
||||
}
|
||||
}
|
||||
+24
-1
@@ -2017,12 +2017,16 @@
|
||||
<p>每个交易日自动更新,开盘前 + 收盘后各一次</p>
|
||||
</div>
|
||||
<div style="display:flex;gap:10px;flex-wrap:wrap;align-items:center;">
|
||||
<a href="https://t.me/GGmanm_bot?start=subscribe" target="_blank" class="sub-btn" style="background:linear-gradient(135deg,#229ED9,#1a8bbf);color:#fff;display:flex;align-items:center;gap:6px;">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor" style="flex-shrink:0"><path d="M12 0C5.373 0 0 5.373 0 12s5.373 12 12 12 12-5.373 12-12S18.627 0 12 0zm5.894 8.221-1.97 9.28c-.145.658-.537.818-1.084.508l-3-2.21-1.447 1.394c-.16.16-.295.295-.605.295l.213-3.053 5.56-5.023c.242-.213-.054-.333-.373-.12L7.44 13.55l-2.955-.924c-.643-.204-.657-.643.136-.953l11.566-4.461c.537-.194 1.006.131.707.009z"/></svg>
|
||||
Telegram 订阅每日早报
|
||||
</a>
|
||||
<a href="feed.xml" target="_blank" class="sub-btn rss-btn">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor" style="flex-shrink:0"><circle cx="6.18" cy="17.82" r="2.18"/><path d="M4 4.44v2.83c7.03 0 12.73 5.7 12.73 12.73h2.83c0-8.59-6.97-15.56-15.56-15.56zm0 5.66v2.83c3.9 0 7.07 3.17 7.07 7.07h2.83c0-5.47-4.43-9.9-9.9-9.9z"/></svg>
|
||||
RSS 订阅
|
||||
</a>
|
||||
<a href="https://yang1bai.github.io/finance-daily-site/feed.xml" id="copy-rss" onclick="copyRSS(event)" class="sub-btn copy-btn">📋 复制链接</a>
|
||||
<span style="font-size:0.7rem;color:var(--dimmed);">邮件订阅配置中…</span>
|
||||
<span id="subscriber-count" style="font-size:0.7rem;color:var(--dimmed);"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2391,7 +2395,26 @@
|
||||
});
|
||||
|
||||
|
||||
|
||||
// ── Subscriber count ──────────────────────────────────────────────────────
|
||||
(function loadSubCount() {
|
||||
const el = document.getElementById('subscriber-count');
|
||||
if (!el) return;
|
||||
fetch('data/investment-subscribers.json')
|
||||
.then(r => r.json())
|
||||
.then(d => {
|
||||
const count = Object.values(d.subscribers || {}).filter(s => s.active).length;
|
||||
if (count > 0) el.textContent = `📱 ${count} 人已订阅`;
|
||||
})
|
||||
.catch(() => {});
|
||||
})();
|
||||
</script>
|
||||
|
||||
<!-- GoatCounter analytics (privacy-friendly) -->
|
||||
<!-- To activate: create free account at https://www.goatcounter.com/ -->
|
||||
<!-- Then replace YOURCODE below with your GoatCounter site code -->
|
||||
<script data-goatcounter="https://YOURCODE.goatcounter.com/count"
|
||||
async src="//gc.zgo.at/count.js"></script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -0,0 +1,264 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Daily Investment Briefing
|
||||
每日投资早报 — 每天早上 8:30 AM Toronto 推送
|
||||
"""
|
||||
|
||||
import yfinance as yf
|
||||
import requests
|
||||
import json
|
||||
import os
|
||||
from datetime import datetime
|
||||
import pytz
|
||||
|
||||
TORONTO_TZ = pytz.timezone("America/Toronto")
|
||||
|
||||
# Telegram 配置
|
||||
TELEGRAM_TOKEN = os.environ.get("TELEGRAM_TOKEN", "8518954174:AAHIWuxR4DDTtqxqjFzeUi33WxFUtnLyQQc")
|
||||
TELEGRAM_CHAT_ID = "8727904480"
|
||||
|
||||
# 主要追踪标的
|
||||
WATCHLIST = {
|
||||
"美股指数": {
|
||||
"S&P 500": "^GSPC",
|
||||
"纳斯达克": "^IXIC",
|
||||
"道琼斯": "^DJI",
|
||||
},
|
||||
"AI & 科技": {
|
||||
"英伟达 NVDA": "NVDA",
|
||||
"费城半导体 SOX": "^SOX",
|
||||
"QQQ": "QQQ",
|
||||
},
|
||||
"新能源 ETF": {
|
||||
"锂电 LIT": "LIT",
|
||||
"清洁能源 ICLN": "ICLN",
|
||||
"太阳能 TAN": "TAN",
|
||||
"铀矿 URA": "URA",
|
||||
},
|
||||
"A股/港股": {
|
||||
"沪深300 ETF": "510300.SS",
|
||||
"恒生ETF": "2800.HK",
|
||||
"新能源 ETF": "159806.SZ",
|
||||
},
|
||||
"大宗商品": {
|
||||
"黄金": "GC=F",
|
||||
"白银": "SI=F",
|
||||
"铜": "HG=F",
|
||||
"原油": "CL=F",
|
||||
"天然气": "NG=F",
|
||||
},
|
||||
"新能源金属": {
|
||||
"钯金": "PA=F",
|
||||
"铂金": "PL=F",
|
||||
},
|
||||
"加密": {
|
||||
"BTC-USD": "BTC-USD",
|
||||
"ETH-USD": "ETH-USD",
|
||||
},
|
||||
}
|
||||
|
||||
def fmt_change(pct: float) -> str:
|
||||
if pct >= 2:
|
||||
arrow = "🚀 +"
|
||||
elif pct >= 0:
|
||||
arrow = "🟢 +"
|
||||
elif pct >= -2:
|
||||
arrow = "🔴 "
|
||||
else:
|
||||
arrow = "💥 "
|
||||
return f"{arrow}{pct:.2f}%"
|
||||
|
||||
def trend_label(pct_5d: float, pct_30d: float) -> str:
|
||||
"""简单趋势判断"""
|
||||
if pct_30d > 10 and pct_5d > 2:
|
||||
return "📈 强势上行"
|
||||
elif pct_30d > 5 and pct_5d > 0:
|
||||
return "↗️ 温和上涨"
|
||||
elif pct_30d < -10 and pct_5d < -2:
|
||||
return "📉 持续下行"
|
||||
elif pct_30d < -5 and pct_5d < 0:
|
||||
return "↘️ 弱势调整"
|
||||
elif abs(pct_30d) < 3:
|
||||
return "➡️ 横盘整理"
|
||||
elif pct_5d > 0 and pct_30d < 0:
|
||||
return "⚡ 超跌反弹"
|
||||
elif pct_5d < 0 and pct_30d > 0:
|
||||
return "⚠️ 短线回调"
|
||||
else:
|
||||
return "〰️ 震荡"
|
||||
|
||||
def fetch_quotes() -> dict:
|
||||
results = {}
|
||||
for category, items in WATCHLIST.items():
|
||||
results[category] = {}
|
||||
for name, ticker in items.items():
|
||||
try:
|
||||
t = yf.Ticker(ticker)
|
||||
hist = t.history(period="1mo")
|
||||
if len(hist) < 2:
|
||||
results[category][name] = {"error": "no data"}
|
||||
continue
|
||||
|
||||
last = hist["Close"].iloc[-1]
|
||||
prev = hist["Close"].iloc[-2]
|
||||
d30 = hist["Close"].iloc[0]
|
||||
|
||||
pct_1d = (last - prev) / prev * 100
|
||||
pct_5d = (last - hist["Close"].iloc[-6]) / hist["Close"].iloc[-6] * 100 if len(hist) >= 6 else None
|
||||
pct_30d = (last - d30) / d30 * 100
|
||||
|
||||
results[category][name] = {
|
||||
"price": last,
|
||||
"pct_1d": pct_1d,
|
||||
"pct_5d": pct_5d,
|
||||
"pct_30d": pct_30d,
|
||||
}
|
||||
except Exception as e:
|
||||
results[category][name] = {"error": str(e)[:60]}
|
||||
return results
|
||||
|
||||
|
||||
def fmt_price(price: float) -> str:
|
||||
if price > 10000:
|
||||
return f"{price:,.0f}"
|
||||
elif price > 100:
|
||||
return f"{price:,.2f}"
|
||||
elif price > 1:
|
||||
return f"{price:.3f}"
|
||||
else:
|
||||
return f"{price:.5f}"
|
||||
|
||||
|
||||
def build_message(quotes: dict) -> str:
|
||||
now = datetime.now(TORONTO_TZ)
|
||||
date_str = now.strftime("%Y-%m-%d %a")
|
||||
|
||||
lines = [
|
||||
f"📊 *每日投资早报* — {date_str}",
|
||||
"",
|
||||
]
|
||||
|
||||
for category, items in quotes.items():
|
||||
lines.append(f"━━━ *{category}* ━━━")
|
||||
for name, data in items.items():
|
||||
if "error" in data:
|
||||
lines.append(f" {name}: —")
|
||||
continue
|
||||
|
||||
price_str = fmt_price(data["price"])
|
||||
change_str = fmt_change(data["pct_1d"])
|
||||
|
||||
pct_5d = data.get("pct_5d")
|
||||
pct_30d = data.get("pct_30d")
|
||||
|
||||
# 主行:价格 + 日涨跌
|
||||
line = f" *{name}*: {price_str} {change_str}"
|
||||
lines.append(line)
|
||||
|
||||
# 副行:5日/月涨跌 + 趋势
|
||||
if pct_5d is not None and pct_30d is not None:
|
||||
sign5 = "+" if pct_5d >= 0 else ""
|
||||
sign30 = "+" if pct_30d >= 0 else ""
|
||||
trend = trend_label(pct_5d, pct_30d)
|
||||
lines.append(
|
||||
f" 5日:{sign5}{pct_5d:.1f}% 月:{sign30}{pct_30d:.1f}% {trend}"
|
||||
)
|
||||
lines.append("")
|
||||
|
||||
# 市场情绪
|
||||
changes_1d = [
|
||||
d["pct_1d"]
|
||||
for cat in quotes.values()
|
||||
for d in cat.values()
|
||||
if "pct_1d" in d
|
||||
]
|
||||
if changes_1d:
|
||||
avg = sum(changes_1d) / len(changes_1d)
|
||||
pos = sum(1 for x in changes_1d if x > 0)
|
||||
neg = sum(1 for x in changes_1d if x < 0)
|
||||
if avg > 1.5:
|
||||
mood = "🌞 全面上涨,市场情绪亢奋,注意追高风险"
|
||||
elif avg > 0.3:
|
||||
mood = "🌤 整体偏多,风险资产强劲,可适量布局"
|
||||
elif avg > -0.3:
|
||||
mood = "😐 市场分化,观望为主,等待方向选择"
|
||||
elif avg > -1.5:
|
||||
mood = "☁️ 整体偏弱,短线谨慎,关注支撑位"
|
||||
else:
|
||||
mood = "⛈ 普跌行情,控制仓位,避免左侧接刀"
|
||||
|
||||
lines.append(f"*市场情绪:* {mood}")
|
||||
lines.append(f"今日 {pos} 涨 / {neg} 跌(共 {len(changes_1d)} 标的)")
|
||||
lines.append("")
|
||||
|
||||
lines.append("_数据来源:Yahoo Finance · 趋势仅供参考,非投资建议_")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
SUBS_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "data", "investment-subscribers.json")
|
||||
|
||||
|
||||
def load_subscribers() -> list:
|
||||
"""返回所有活跃订阅者的 chat_id 列表"""
|
||||
try:
|
||||
with open(SUBS_FILE) as f:
|
||||
data = json.load(f)
|
||||
return [uid for uid, s in data["subscribers"].items() if s.get("active")]
|
||||
except Exception:
|
||||
return [TELEGRAM_CHAT_ID] # fallback
|
||||
|
||||
|
||||
def save_broadcast_stat(sent: int, total: int):
|
||||
try:
|
||||
with open(SUBS_FILE) as f:
|
||||
data = json.load(f)
|
||||
stats = data.setdefault("stats", {})
|
||||
stats["total_broadcasts"] = stats.get("total_broadcasts", 0) + 1
|
||||
stats["last_broadcast"] = datetime.now(TORONTO_TZ).strftime("%Y-%m-%d %H:%M")
|
||||
history = stats.setdefault("broadcast_history", [])
|
||||
history.append({
|
||||
"date": stats["last_broadcast"],
|
||||
"sent": sent,
|
||||
"total": total,
|
||||
})
|
||||
if len(history) > 30:
|
||||
stats["broadcast_history"] = history[-30:]
|
||||
with open(SUBS_FILE, "w") as f:
|
||||
json.dump(data, f, ensure_ascii=False, indent=2)
|
||||
except Exception as e:
|
||||
print(f"Stat save failed: {e}")
|
||||
|
||||
|
||||
def broadcast(message: str):
|
||||
"""广播给所有活跃订阅者"""
|
||||
url = f"https://api.telegram.org/bot{TELEGRAM_TOKEN}/sendMessage"
|
||||
subscribers = load_subscribers()
|
||||
sent = 0
|
||||
for chat_id in subscribers:
|
||||
try:
|
||||
resp = requests.post(url, json={
|
||||
"chat_id": chat_id,
|
||||
"text": message,
|
||||
"parse_mode": "Markdown",
|
||||
}, timeout=15)
|
||||
if resp.ok:
|
||||
sent += 1
|
||||
else:
|
||||
print(f"Failed {chat_id}: {resp.status_code}")
|
||||
except Exception as e:
|
||||
print(f"Error {chat_id}: {e}")
|
||||
print(f"✅ Broadcast: {sent}/{len(subscribers)} delivered")
|
||||
save_broadcast_stat(sent, len(subscribers))
|
||||
|
||||
|
||||
def main():
|
||||
print(f"Fetching quotes at {datetime.now(TORONTO_TZ).isoformat()}...")
|
||||
quotes = fetch_quotes()
|
||||
message = build_message(quotes)
|
||||
print(message)
|
||||
print("\n--- Broadcasting ---")
|
||||
broadcast(message)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,178 @@
|
||||
#!/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")
|
||||
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()
|
||||
Reference in New Issue
Block a user