agus1111 commited on
Commit
2d70f5d
·
verified ·
1 Parent(s): 871dafe

Update bot_core.py

Browse files
Files changed (1) hide show
  1. bot_core.py +68 -39
bot_core.py CHANGED
@@ -1,10 +1,26 @@
1
  # bot_core.py
2
- import os, re, time, asyncio, nest_asyncio
3
  from typing import Optional, Tuple
4
  from telethon import TelegramClient, events
5
  from telethon.sessions import StringSession
6
  from telethon.errors.rpcerrorlist import FloodWaitError
7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8
  # ======== ENV & KONFIG ========
9
  API_ID = int(os.environ.get("API_ID", "0"))
10
  API_HASH = os.environ.get("API_HASH", "")
@@ -21,6 +37,8 @@ RETRY_MAX_ATTEMPTS = int(os.environ.get("RETRY_MAX_ATTEMPTS", "12"))
21
  RETRY_SLEEP_SECONDS = float(os.environ.get("RETRY_SLEEP_SECONDS", "1.2"))
22
  RETRY_OVERALL_TIMEOUT = float(os.environ.get("RETRY_OVERALL_TIMEOUT", "60"))
23
  RECENT_CA_TTL_MINUTES = int(os.environ.get("RECENT_CA_TTL_MINUTES", "60"))
 
 
24
  TIER_MIN = os.environ.get("TIER_MIN", "medium").strip().lower()
25
 
26
  if not (API_ID and API_HASH and STRING_SESSION):
@@ -36,6 +54,7 @@ BUY_PAT = re.compile(r"(?i)buy")
36
  REFRESH_PAT = re.compile(r"(?i)refresh")
37
  RETRY_PAT = re.compile(r"(?i)(" + "|".join(map(re.escape, RETRY_LABELS)) + r")") if RETRY_LABELS else re.compile(r"$^")
38
 
 
39
  TIER_PAT = re.compile(r"\[\s*(low|medium|strong|high|fomo)\b.*?\]", re.IGNORECASE | re.UNICODE)
40
  _TIER_ORDER = {"low": 1, "medium": 2, "strong": 3, "high": 3, "fomo": 4}
41
  _MIN_RANK = _TIER_ORDER.get(TIER_MIN, 2)
@@ -60,11 +79,14 @@ def extract_tier(text: str) -> Optional[str]:
60
  def has_valid_tier(text: str) -> bool:
61
  t = extract_tier(text)
62
  if not t:
63
- print("[SKIP] Pesan tanpa tier → dilewati")
64
  return False
65
  rank = _TIER_ORDER.get(t, 0)
66
- print(f"[INFO] Tier terdeteksi: {t} (rank={rank}), minimum={_MIN_RANK}")
67
- return rank >= _MIN_RANK
 
 
 
68
 
69
  def find_first_ca(text: str) -> Optional[Tuple[str, str]]:
70
  if not text:
@@ -81,7 +103,7 @@ def find_first_ca(text: str) -> Optional[Tuple[str, str]]:
81
  if not cand:
82
  return None
83
  chain, _, addr = sorted(cand, key=lambda x: x[1])[0]
84
- print(f"[INFO] CA ditemukan: {addr} (chain={chain})")
85
  return chain, addr
86
 
87
  # ======== STATE ========
@@ -107,10 +129,10 @@ async def wait_new_keyboard(chat, *, min_id: int, timeout=25, interval=0.8):
107
  msgs = await client.get_messages(chat, limit=8)
108
  for m in msgs:
109
  if m.id > min_id and m.buttons:
110
- print(f"[DEBUG] Keyboard baru ditemukan (msg.id={m.id})")
111
  return m
112
  await asyncio.sleep(interval)
113
- print("[WARN] Tidak menemukan keyboard baru dalam timeout")
114
  return None
115
 
116
  async def click_first_match(msg, pat) -> bool:
@@ -123,7 +145,7 @@ async def click_first_match(msg, pat) -> bool:
123
  for j, btn in enumerate(row):
124
  label = (getattr(btn, "text", "") or "").strip()
125
  if label and pat.search(label):
126
- print(f"[ACTION] Klik tombol: {label}")
127
  await msg.click(i, j)
128
  return True
129
  return False
@@ -134,6 +156,7 @@ async def search_latest_with_button(chat, pat, *, min_id: int, limit=10):
134
  if m.id >= min_id and m.buttons:
135
  m2 = await client.get_messages(chat, ids=m.id)
136
  if m2 and await click_first_match(m2, pat):
 
137
  return m2
138
  return None
139
 
@@ -141,11 +164,11 @@ async def click_retry_until_gone(anchor_msg, *, baseline_id: int) -> bool:
141
  attempts = 0
142
  started = time.monotonic()
143
  msg_kb = anchor_msg
144
- while attempts < RETRY_MAX_ATTEMPTS and (time.monotonic() - started) < RETRY_OVERALL_TIMEOUT:
145
  msg_kb = await refresh_message(msg_kb)
146
  if await click_first_match(msg_kb, RETRY_PAT):
147
  attempts += 1
148
- print(f"[RETRY] Klik Retry ke-{attempts}")
149
  await asyncio.sleep(RETRY_SLEEP_SECONDS)
150
  continue
151
  msgs = await client.get_messages(TARGET_CHAT, limit=8)
@@ -155,69 +178,73 @@ async def click_retry_until_gone(anchor_msg, *, baseline_id: int) -> bool:
155
  if m2 and await click_first_match(m2, RETRY_PAT):
156
  attempts += 1
157
  baseline_id = max(baseline_id, m2.id)
158
- print(f"[RETRY] Klik Retry di pesan baru id={m.id}")
159
  await asyncio.sleep(RETRY_SLEEP_SECONDS)
160
  break
161
  else:
162
  break
163
- print("[INFO] Retry selesai / tidak ada tombol lagi")
164
  return True
165
 
166
  async def buy_flow_with_ca(addr: str):
167
- print(f"[FLOW] Mulai buy_flow dengan CA: {addr}")
168
  last = await client.get_messages(TARGET_CHAT, limit=1)
169
  baseline_id = last[0].id if last else 0
170
 
171
  await client.send_message(TARGET_CHAT, addr)
172
- print("[SEND] CA terkirim ke target bot")
173
 
174
  msg_kb = await wait_new_keyboard(TARGET_CHAT, min_id=baseline_id, timeout=25)
175
  if not msg_kb:
176
- print("[WARN] Keyboard tidak muncul, fallback ke /start")
177
  await client.send_message(TARGET_CHAT, "/start")
178
  await asyncio.sleep(1.0)
179
  await client.send_message(TARGET_CHAT, addr)
180
  msg_kb = await wait_new_keyboard(TARGET_CHAT, min_id=baseline_id, timeout=25)
181
  if not msg_kb:
182
- print("[ERROR] Gagal mendapatkan keyboard setelah fallback")
183
  return
184
 
185
  if AMOUNT_LABEL:
186
- if await click_first_match(msg_kb, re.compile(re.escape(AMOUNT_LABEL), re.IGNORECASE)):
 
 
187
  await asyncio.sleep(1.0)
188
  msg2 = await wait_new_keyboard(TARGET_CHAT, min_id=baseline_id, timeout=12)
189
  msg_kb = msg2 or await refresh_message(msg_kb)
190
  baseline_id = max(baseline_id, getattr(msg_kb, "id", baseline_id))
191
 
192
- # === Tambahan: tunggu + Refresh 2x sebelum BUY ===
193
- print("[WAIT] Tunggu 15 detik sebelum klik Refresh pertama")
194
  await asyncio.sleep(15)
195
- await click_first_match(msg_kb, REFRESH_PAT)
 
196
 
197
- print("[WAIT] Tunggu 15 detik sebelum klik Refresh kedua")
198
  await asyncio.sleep(15)
199
  msg_kb = await refresh_message(msg_kb)
200
- await click_first_match(msg_kb, REFRESH_PAT)
201
- # ================================================
 
202
 
203
- print("[STEP] Klik tombol BUY")
204
  ok_buy = await click_first_match(msg_kb, BUY_PAT)
205
  if not ok_buy:
206
  hit = await search_latest_with_button(TARGET_CHAT, BUY_PAT, min_id=baseline_id, limit=10)
207
  if hit:
208
  msg_kb = hit
209
  ok_buy = True
210
-
211
  if not ok_buy and msg_kb.buttons and msg_kb.buttons[-1]:
212
  try:
213
  await msg_kb.click(len(msg_kb.buttons) - 1, 0)
214
- print("[FALLBACK] Klik tombol terakhir sebagai BUY")
215
  ok_buy = True
216
- except:
217
- pass
 
218
 
219
  if not ok_buy:
220
- print("[WARN] Tidak menemukan tombol BUY, ulangi dengan /start")
221
  await client.send_message(TARGET_CHAT, "/start")
222
  await asyncio.sleep(1.0)
223
  await client.send_message(TARGET_CHAT, addr)
@@ -226,20 +253,21 @@ async def buy_flow_with_ca(addr: str):
226
  if not await click_first_match(msg_kb, BUY_PAT):
227
  hit2 = await search_latest_with_button(TARGET_CHAT, BUY_PAT, min_id=baseline_id, limit=10)
228
  if not hit2:
229
- print("[ERROR] BUY tetap gagal setelah ulang")
230
  return
231
  msg_kb = hit2
 
232
 
233
  await asyncio.sleep(1.0)
234
  await click_retry_until_gone(msg_kb, baseline_id=baseline_id)
235
- print("[SUCCESS] Buy flow selesai")
236
 
237
  # ======== ACCEPT / QUEUE ========
238
  def _accept_ca(addr: str) -> bool:
239
  if not addr:
240
  return False
241
  if _is_recent(addr):
242
- print(f"[SKIP] CA {addr} sudah diproses baru-baru ini")
243
  return False
244
  _mark_ca(addr)
245
  return True
@@ -248,19 +276,19 @@ def _accept_ca(addr: str) -> bool:
248
  @client.on(events.NewMessage(chats=_normalize_chat(SOURCE_CHAT)))
249
  async def on_message_from_source(event: events.NewMessage.Event):
250
  txt = (event.raw_text or "").strip()
251
- print(f"[EVENT] Pesan baru dari source (len={len(txt)})")
252
 
253
  if not has_valid_tier(txt):
254
  return
255
 
256
  found = find_first_ca(txt)
257
  if not found:
258
- print("[INFO] Tidak ada CA ditemukan di pesan ini")
259
  return
260
  _, addr = found
261
  if _accept_ca(addr):
262
- print(f"[QUEUE] Tambah CA {addr} ke antrean")
263
  await ca_queue.put(addr)
 
264
 
265
  # ======== LIFECYCLE ========
266
  async def _worker():
@@ -269,18 +297,19 @@ async def _worker():
269
  try:
270
  await buy_flow_with_ca(addr)
271
  except FloodWaitError as fw:
272
- print(f"[ERROR] FloodWait, tidur {fw.seconds} detik")
273
  await asyncio.sleep(fw.seconds + 2)
274
  except Exception as e:
275
- print(f"[ERROR] Exception worker: {e}")
276
  finally:
277
  ca_queue.task_done()
278
 
279
  async def start_bot_background():
280
- print("[START] Menjalankan bot...")
 
281
  await client.start()
282
  asyncio.create_task(_worker())
283
 
284
  async def stop_bot():
285
- print("[STOP] Memutus koneksi bot...")
286
  await client.disconnect()
 
1
  # bot_core.py
2
+ import os, re, time, sys, asyncio, nest_asyncio, logging
3
  from typing import Optional, Tuple
4
  from telethon import TelegramClient, events
5
  from telethon.sessions import StringSession
6
  from telethon.errors.rpcerrorlist import FloodWaitError
7
 
8
+ # ========= LOGGING =========
9
+ LOG_LEVEL = os.environ.get("LOG_LEVEL", "INFO").upper()
10
+ logger = logging.getLogger("midastouch-bot")
11
+ if not logger.handlers:
12
+ handler = logging.StreamHandler(sys.stdout)
13
+ fmt = logging.Formatter(
14
+ fmt="%(asctime)s | %(levelname)-7s | %(message)s",
15
+ datefmt="%Y-%m-%d %H:%M:%S",
16
+ )
17
+ handler.setFormatter(fmt)
18
+ logger.addHandler(handler)
19
+ logger.setLevel(LOG_LEVEL)
20
+
21
+ # Biar log Telethon ikut muncul saat DEBUG
22
+ logging.getLogger("telethon").setLevel(logging.WARNING if LOG_LEVEL != "DEBUG" else logging.INFO)
23
+
24
  # ======== ENV & KONFIG ========
25
  API_ID = int(os.environ.get("API_ID", "0"))
26
  API_HASH = os.environ.get("API_HASH", "")
 
37
  RETRY_SLEEP_SECONDS = float(os.environ.get("RETRY_SLEEP_SECONDS", "1.2"))
38
  RETRY_OVERALL_TIMEOUT = float(os.environ.get("RETRY_OVERALL_TIMEOUT", "60"))
39
  RECENT_CA_TTL_MINUTES = int(os.environ.get("RECENT_CA_TTL_MINUTES", "60"))
40
+
41
+ # Ambang tier minimum (low < medium < strong/high < fomo)
42
  TIER_MIN = os.environ.get("TIER_MIN", "medium").strip().lower()
43
 
44
  if not (API_ID and API_HASH and STRING_SESSION):
 
54
  REFRESH_PAT = re.compile(r"(?i)refresh")
55
  RETRY_PAT = re.compile(r"(?i)(" + "|".join(map(re.escape, RETRY_LABELS)) + r")") if RETRY_LABELS else re.compile(r"$^")
56
 
57
+ # [Low 🌱], [Medium ⚡️], [Strong 💪], [High], [FOMO🔥]
58
  TIER_PAT = re.compile(r"\[\s*(low|medium|strong|high|fomo)\b.*?\]", re.IGNORECASE | re.UNICODE)
59
  _TIER_ORDER = {"low": 1, "medium": 2, "strong": 3, "high": 3, "fomo": 4}
60
  _MIN_RANK = _TIER_ORDER.get(TIER_MIN, 2)
 
79
  def has_valid_tier(text: str) -> bool:
80
  t = extract_tier(text)
81
  if not t:
82
+ logger.info("SKIP | pesan TANPA tier → dilewati")
83
  return False
84
  rank = _TIER_ORDER.get(t, 0)
85
+ if rank >= _MIN_RANK:
86
+ logger.info("PASS | tier=%s (rank=%d) >= min=%d", t, rank, _MIN_RANK)
87
+ return True
88
+ logger.info("SKIP | tier=%s (rank=%d) < min=%d", t, rank, _MIN_RANK)
89
+ return False
90
 
91
  def find_first_ca(text: str) -> Optional[Tuple[str, str]]:
92
  if not text:
 
103
  if not cand:
104
  return None
105
  chain, _, addr = sorted(cand, key=lambda x: x[1])[0]
106
+ logger.info("CA | ditemukan chain=%s addr=%s", chain, addr)
107
  return chain, addr
108
 
109
  # ======== STATE ========
 
129
  msgs = await client.get_messages(chat, limit=8)
130
  for m in msgs:
131
  if m.id > min_id and m.buttons:
132
+ logger.debug("KB | keyboard baru id=%s", m.id)
133
  return m
134
  await asyncio.sleep(interval)
135
+ logger.warning("KB | tidak menemukan keyboard (timeout=%ss)", timeout)
136
  return None
137
 
138
  async def click_first_match(msg, pat) -> bool:
 
145
  for j, btn in enumerate(row):
146
  label = (getattr(btn, "text", "") or "").strip()
147
  if label and pat.search(label):
148
+ logger.info("UI | klik tombol: %s (i=%d, j=%d)", label, i, j)
149
  await msg.click(i, j)
150
  return True
151
  return False
 
156
  if m.id >= min_id and m.buttons:
157
  m2 = await client.get_messages(chat, ids=m.id)
158
  if m2 and await click_first_match(m2, pat):
159
+ logger.debug("UI | tombol ditemukan di pesan id=%s", m.id)
160
  return m2
161
  return None
162
 
 
164
  attempts = 0
165
  started = time.monotonic()
166
  msg_kb = anchor_msg
167
+ while attempts < RETRY_MAX_ATTEMPTS and (time.monotonic() - started) < (RETRY_OVERALL_TIMEOUT):
168
  msg_kb = await refresh_message(msg_kb)
169
  if await click_first_match(msg_kb, RETRY_PAT):
170
  attempts += 1
171
+ logger.info("RET | klik Retry ke-%d", attempts)
172
  await asyncio.sleep(RETRY_SLEEP_SECONDS)
173
  continue
174
  msgs = await client.get_messages(TARGET_CHAT, limit=8)
 
178
  if m2 and await click_first_match(m2, RETRY_PAT):
179
  attempts += 1
180
  baseline_id = max(baseline_id, m2.id)
181
+ logger.info("RET | klik Retry di msg.id=%s (total=%d)", m.id, attempts)
182
  await asyncio.sleep(RETRY_SLEEP_SECONDS)
183
  break
184
  else:
185
  break
186
+ logger.info("RET | selesai (attempts=%d)", attempts)
187
  return True
188
 
189
  async def buy_flow_with_ca(addr: str):
190
+ logger.info("FLOW| start buy_flow CA=%s", addr)
191
  last = await client.get_messages(TARGET_CHAT, limit=1)
192
  baseline_id = last[0].id if last else 0
193
 
194
  await client.send_message(TARGET_CHAT, addr)
195
+ logger.info("SEND| CA terkirim ke target bot")
196
 
197
  msg_kb = await wait_new_keyboard(TARGET_CHAT, min_id=baseline_id, timeout=25)
198
  if not msg_kb:
199
+ logger.warning("KB | no keyboard fallback /start")
200
  await client.send_message(TARGET_CHAT, "/start")
201
  await asyncio.sleep(1.0)
202
  await client.send_message(TARGET_CHAT, addr)
203
  msg_kb = await wait_new_keyboard(TARGET_CHAT, min_id=baseline_id, timeout=25)
204
  if not msg_kb:
205
+ logger.error("KB | gagal dapat keyboard setelah fallback")
206
  return
207
 
208
  if AMOUNT_LABEL:
209
+ ok_amt = await click_first_match(msg_kb, re.compile(re.escape(AMOUNT_LABEL), re.IGNORECASE))
210
+ logger.info("UI | pilih amount '%s' → %s", AMOUNT_LABEL, "OK" if ok_amt else "MISS")
211
+ if ok_amt:
212
  await asyncio.sleep(1.0)
213
  msg2 = await wait_new_keyboard(TARGET_CHAT, min_id=baseline_id, timeout=12)
214
  msg_kb = msg2 or await refresh_message(msg_kb)
215
  baseline_id = max(baseline_id, getattr(msg_kb, "id", baseline_id))
216
 
217
+ # === Tunggu + Refresh 2x sebelum BUY ===
218
+ logger.info("WAIT| 15s sebelum Refresh #1")
219
  await asyncio.sleep(15)
220
+ ok_r1 = await click_first_match(msg_kb, REFRESH_PAT)
221
+ logger.info("UI | Refresh #1 → %s", "OK" if ok_r1 else "MISS")
222
 
223
+ logger.info("WAIT| 15s sebelum Refresh #2")
224
  await asyncio.sleep(15)
225
  msg_kb = await refresh_message(msg_kb)
226
+ ok_r2 = await click_first_match(msg_kb, REFRESH_PAT)
227
+ logger.info("UI | Refresh #2 → %s", "OK" if ok_r2 else "MISS")
228
+ # ================================
229
 
230
+ logger.info("STEP| klik BUY")
231
  ok_buy = await click_first_match(msg_kb, BUY_PAT)
232
  if not ok_buy:
233
  hit = await search_latest_with_button(TARGET_CHAT, BUY_PAT, min_id=baseline_id, limit=10)
234
  if hit:
235
  msg_kb = hit
236
  ok_buy = True
237
+ logger.info("UI | BUY ditemukan di pesan lain")
238
  if not ok_buy and msg_kb.buttons and msg_kb.buttons[-1]:
239
  try:
240
  await msg_kb.click(len(msg_kb.buttons) - 1, 0)
 
241
  ok_buy = True
242
+ logger.info("FALL| klik tombol terakhir sebagai BUY")
243
+ except Exception as e:
244
+ logger.debug("FALL| gagal klik tombol terakhir: %s", e)
245
 
246
  if not ok_buy:
247
+ logger.warning("BUY | tombol tidak ditemukan ulangi /start")
248
  await client.send_message(TARGET_CHAT, "/start")
249
  await asyncio.sleep(1.0)
250
  await client.send_message(TARGET_CHAT, addr)
 
253
  if not await click_first_match(msg_kb, BUY_PAT):
254
  hit2 = await search_latest_with_button(TARGET_CHAT, BUY_PAT, min_id=baseline_id, limit=10)
255
  if not hit2:
256
+ logger.error("BUY | tetap gagal setelah ulang")
257
  return
258
  msg_kb = hit2
259
+ logger.info("UI | BUY ditemukan setelah ulang")
260
 
261
  await asyncio.sleep(1.0)
262
  await click_retry_until_gone(msg_kb, baseline_id=baseline_id)
263
+ logger.info("DONE| buy_flow selesai untuk CA=%s", addr)
264
 
265
  # ======== ACCEPT / QUEUE ========
266
  def _accept_ca(addr: str) -> bool:
267
  if not addr:
268
  return False
269
  if _is_recent(addr):
270
+ logger.info("SKIP| CA %s sudah diproses baru-baru ini", addr)
271
  return False
272
  _mark_ca(addr)
273
  return True
 
276
  @client.on(events.NewMessage(chats=_normalize_chat(SOURCE_CHAT)))
277
  async def on_message_from_source(event: events.NewMessage.Event):
278
  txt = (event.raw_text or "").strip()
279
+ logger.info("EVNT| pesan baru (len=%d)", len(txt))
280
 
281
  if not has_valid_tier(txt):
282
  return
283
 
284
  found = find_first_ca(txt)
285
  if not found:
286
+ logger.info("INFO| tidak ada CA di pesan ini")
287
  return
288
  _, addr = found
289
  if _accept_ca(addr):
 
290
  await ca_queue.put(addr)
291
+ logger.info("QUEUE| enqueue CA %s", addr)
292
 
293
  # ======== LIFECYCLE ========
294
  async def _worker():
 
297
  try:
298
  await buy_flow_with_ca(addr)
299
  except FloodWaitError as fw:
300
+ logger.error("ERR | FloodWait %ss → tidur", fw.seconds)
301
  await asyncio.sleep(fw.seconds + 2)
302
  except Exception as e:
303
+ logger.exception("ERR | Exception di worker: %s", e)
304
  finally:
305
  ca_queue.task_done()
306
 
307
  async def start_bot_background():
308
+ logger.info("BOOT| starting bot (source=%s, target=%s, tier_min=%s, log=%s)",
309
+ SOURCE_CHAT, TARGET_CHAT, TIER_MIN, LOG_LEVEL)
310
  await client.start()
311
  asyncio.create_task(_worker())
312
 
313
  async def stop_bot():
314
+ logger.info("BOOT| stopping bot")
315
  await client.disconnect()