bot-signal-telegram / summary_daily.py
agus1111's picture
Update summary_daily.py
e0c3dab verified
# summary_daily.py — Daily summary + guard loop ala /lb (HF Spaces friendly)
import os
import time
import asyncio
import sqlite3
from collections import defaultdict
from datetime import datetime, timezone, timedelta, date
from zoneinfo import ZoneInfo
# =========================
# ENV / DB
# =========================
DB_PATH = os.environ.get("BOTSIGNAL_DB", "/tmp/botsignal.db")
OUT_DIR = os.environ.get("SUMMARY_OUT_DIR", "summaries") # optional, not used by default
JAKARTA_TZ = ZoneInfo("Asia/Jakarta")
SUMMARY_CHAT_ID_ENV = os.environ.get("SUMMARY_CHAT_ID") # opsional; fallback ke TARGET_CHANNEL
def _db():
conn = sqlite3.connect(DB_PATH)
conn.execute("PRAGMA journal_mode=WAL;")
return conn
def _ensure_tables():
"""
Tabel summary_*:
- summary_calls: log post awal (diisi oleh botsignal saat pertama kali posting CA)
- summary_milestones: log milestone reply (diisi oleh autotrack)
- summary_outcomes: penanda lose (delete)
"""
conn = _db()
conn.executescript("""
CREATE TABLE IF NOT EXISTS summary_calls (
keyword TEXT PRIMARY KEY,
posted_at INTEGER NOT NULL,
tier TEXT NOT NULL,
msg_id INTEGER NOT NULL
);
CREATE TABLE IF NOT EXISTS summary_milestones (
keyword TEXT NOT NULL,
reply_msg_id INTEGER,
multiple REAL,
replied_at INTEGER NOT NULL
);
CREATE TABLE IF NOT EXISTS summary_outcomes (
keyword TEXT PRIMARY KEY,
is_deleted INTEGER DEFAULT 0
);
""")
conn.commit()
conn.close()
# =========================
# Helpers
# =========================
def _day_bounds_utc(d: date):
# [00:00, 23:59:59] UTC untuk tanggal d
sod = datetime(d.year, d.month, d.day, tzinfo=timezone.utc)
eod = datetime(d.year, d.month, d.day, 23, 59, 59, tzinfo=timezone.utc)
return int(sod.timestamp()), int(eod.timestamp())
def _fmt(num):
return f"{num:,}".replace(",", ".")
# =========================
# Core summary compute
# =========================
def compute_summary_for_date(d: date):
_ensure_tables()
sod, eod = _day_bounds_utc(d)
conn = _db(); cur = conn.cursor()
# Semua post awal selama hari tsb (UTC)
cur.execute("""
SELECT keyword, tier, posted_at, msg_id
FROM summary_calls
WHERE posted_at BETWEEN ? AND ?
""", (sod, eod))
rows = cur.fetchall()
total_calls = len(rows)
calls = {r[0]: {"tier": r[1], "posted_at": r[2], "msg_id": r[3]} for r in rows}
# Max multiple per keyword (hanya reply di window hari tsb)
max_mult_per_kw = {}
have_reply = set()
if calls:
q = ",".join("?" for _ in calls)
cur.execute(f"""
SELECT keyword, MAX(multiple)
FROM summary_milestones
WHERE keyword IN ({q})
AND replied_at BETWEEN ? AND ?
GROUP BY keyword
""", (*calls.keys(), sod, eod))
for kw, mx in cur.fetchall():
if mx is not None:
have_reply.add(kw)
max_mult_per_kw[kw] = mx
# Delete mark (lose)
deleted = set()
if calls:
q = ",".join("?" for _ in calls)
cur.execute(f"""
SELECT keyword
FROM summary_outcomes
WHERE keyword IN ({q})
AND is_deleted = 1
""", (*calls.keys(),))
for (kw,) in cur.fetchall():
deleted.add(kw)
conn.close()
wins = have_reply
loses = (deleted | (set(calls.keys()) - have_reply))
calls_per_tier = defaultdict(int)
for _, meta in calls.items():
calls_per_tier[meta["tier"]] += 1
win_per_tier = defaultdict(int)
lose_per_tier = defaultdict(int)
for kw, meta in calls.items():
if kw in wins:
win_per_tier[meta["tier"]] += 1
if kw in loses:
lose_per_tier[meta["tier"]] += 1
top_kw, top_mult = None, None
for kw, mult in max_mult_per_kw.items():
if top_mult is None or mult > top_mult:
top_kw, top_mult = kw, mult
return {
"total_calls": total_calls,
"win_rate": (len(wins) / total_calls) * 100 if total_calls else 0.0,
"calls_per_tier": dict(sorted(calls_per_tier.items())),
"total_win": len(wins),
"total_lose": len(loses),
"win_per_tier": dict(sorted(win_per_tier.items())),
"lose_per_tier": dict(sorted(lose_per_tier.items())),
"top_call": {"keyword": top_kw, "max_multiple": top_mult} if top_kw else None,
}
def render_telegram_html(summary: dict, label_date: str) -> str:
# HTML (pakai parse_mode='html')
lines = []
lines.append(f"<b>Daily Summary — {label_date}</b>")
lines.append("")
lines.append(f"<b>Total Call:</b> {_fmt(summary['total_calls'])}")
lines.append(f"<b>Win Rate:</b> {summary['win_rate']:.2f}%")
lines.append("")
lines.append("<b>Total Call per Tier</b>")
if summary["calls_per_tier"]:
for tier, cnt in summary["calls_per_tier"].items():
lines.append(f"• {tier}: {_fmt(cnt)}")
else:
lines.append("• (kosong)")
lines.append("")
lines.append("<b>Total Win & Lose</b>")
lines.append(f"• Win: {_fmt(summary['total_win'])}")
lines.append(f"• Lose: {_fmt(summary['total_lose'])}")
lines.append("")
lines.append("<b>Detail per Tier</b>")
lines.append("<u>Win</u>")
if summary["win_per_tier"]:
for tier, cnt in summary["win_per_tier"].items():
lines.append(f"• {tier}: {_fmt(cnt)}")
else:
lines.append("• (kosong)")
lines.append("<u>Lose</u>")
if summary["lose_per_tier"]:
for tier, cnt in summary["lose_per_tier"].items():
lines.append(f"• {tier}: {_fmt(cnt)}")
else:
lines.append("• (kosong)")
lines.append("")
lines.append("<b>Top Call (Max Reply Multiple)</b>")
if summary["top_call"] and summary["top_call"]["keyword"]:
lines.append(f"• CA: <code>{summary['top_call']['keyword']}</code>")
lines.append(f"• Max: {summary['top_call']['max_multiple']}×")
else:
lines.append("• (belum ada reply)")
return "\n".join(lines)
# =========================
# Guard loop ala /lb (idempotent)
# =========================
def _sent_db():
conn = sqlite3.connect(DB_PATH)
conn.execute("PRAGMA journal_mode=WAL;")
return conn
def _ensure_sent_table():
conn = _sent_db()
conn.executescript("""
CREATE TABLE IF NOT EXISTS summary_sent (
day TEXT PRIMARY KEY, -- YYYY-MM-DD (lokal WIB)
sent_at INTEGER NOT NULL -- epoch UTC
);
""")
conn.commit(); conn.close()
def _is_sent(day_iso: str) -> bool:
try:
conn = _sent_db(); cur = conn.cursor()
cur.execute("SELECT 1 FROM summary_sent WHERE day = ?", (day_iso,))
ok = cur.fetchone() is not None
conn.close(); return ok
except Exception:
return False
def _mark_sent(day_iso: str):
conn = _sent_db()
conn.execute(
"INSERT OR REPLACE INTO summary_sent(day, sent_at) VALUES(?, ?)",
(day_iso, int(time.time()))
)
conn.commit(); conn.close()
def _yesterday_local():
now_local = datetime.now(JAKARTA_TZ)
return (now_local.date() - timedelta(days=1)), now_local
async def _maybe_send_daily_summary_once(client, fallback_chat: str):
"""
Kirim rekap H-1 sekali/hari setelah >= 06:00 WIB (lokal).
Idempotent via tabel summary_sent.
"""
day, now_local = _yesterday_local()
if now_local.hour < 6:
return # belum waktunya
day_iso = day.isoformat()
if _is_sent(day_iso):
return # sudah terkirim
# Tujuan kirim
target_chat = SUMMARY_CHAT_ID_ENV or os.environ.get("TARGET_CHANNEL", fallback_chat)
# Hitung & kirim
data = compute_summary_for_date(day)
html_text = render_telegram_html(data, day_iso)
await client.send_message(target_chat, html_text, parse_mode="html")
_mark_sent(day_iso)
print(f"[SUMMARY] sent for {day_iso}{target_chat}")
async def start_summary_guard(client, *, interval_sec: int = 900, fallback_chat: str = "@MidasTouchsignalll"):
"""
Panggil sekali saat startup app (mis. dari servr.py).
Loop ringan cek tiap 15 menit—mirip /lb periodic.
Cocok untuk HF Spaces yang sleep: saat bangun setelah 06:00, dia kirim H-1 sekali.
"""
_ensure_sent_table()
while True:
try:
await _maybe_send_daily_summary_once(client, fallback_chat)
except Exception as e:
print(f"[SUMMARY] guard error: {e}")
await asyncio.sleep(interval_sec)
# =========================
# Optional: CLI sekali jalan (kirim 'hari ini')
# =========================
if __name__ == "__main__":
# Kirim summary untuk 'hari ini' (UTC) ke TARGET_CHANNEL — hanya untuk tes manual
import asyncio as _a
from telethon import TelegramClient
from telethon.sessions import StringSession
API_ID = int(os.environ.get("API_ID", "0"))
API_HASH = os.environ.get("API_HASH", "")
STRING_SESSION = os.environ.get("STRING_SESSION", "")
TARGET_CHANNEL = os.environ.get("TARGET_CHANNEL", "@MidasTouchsignalll") # fallback
_summary = compute_summary_for_date(date.today())
_html = render_telegram_html(_summary, date.today().strftime("%Y-%m-%d"))
async def _main():
async with TelegramClient(StringSession(STRING_SESSION), API_ID, API_HASH) as c:
await c.send_message(SUMMARY_CHAT_ID_ENV or TARGET_CHANNEL, _html, parse_mode="html")
print("[SUMMARY] posted to Telegram")
_a.run(_main())