File size: 9,620 Bytes
e0c3dab
5790013
e0c3dab
 
2a5e1c5
 
e0c3dab
 
2a5e1c5
e0c3dab
 
 
2a5e1c5
e0c3dab
 
 
2a5e1c5
 
 
 
 
 
 
e0c3dab
 
 
 
 
 
2a5e1c5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e0c3dab
 
 
2a5e1c5
e0c3dab
2a5e1c5
 
 
 
 
 
 
e0c3dab
 
 
2a5e1c5
 
 
 
 
e0c3dab
2a5e1c5
 
 
 
 
 
 
 
 
e0c3dab
2a5e1c5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e0c3dab
2a5e1c5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e0c3dab
2a5e1c5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e0c3dab
2a5e1c5
 
 
 
 
 
e0c3dab
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2a5e1c5
e0c3dab
 
2a5e1c5
e0c3dab
 
 
 
 
 
 
 
 
 
 
 
 
2a5e1c5
 
e0c3dab
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
# 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())