agus1111 commited on
Commit
46a11dd
·
verified ·
1 Parent(s): 4218965

Update bot_core.py

Browse files
Files changed (1) hide show
  1. bot_core.py +45 -118
bot_core.py CHANGED
@@ -4,7 +4,6 @@ from telethon import TelegramClient, events
4
  from telethon.sessions import StringSession
5
  from telethon.errors.rpcerrorlist import FloodWaitError
6
 
7
-
8
  # ========= LOGGING =========
9
  LOG_LEVEL = os.environ.get("LOG_LEVEL", "INFO").upper()
10
  logger = logging.getLogger("midastouch-bot")
@@ -24,7 +23,6 @@ API_ID = int(os.environ.get("API_ID", "0"))
24
  API_HASH = os.environ.get("API_HASH", "")
25
  STRING_SESSION = os.environ.get("STRING_SESSION", "")
26
 
27
- # Comma-separated; each item may be a full t.me URL or @username or bare username
28
  SOURCE_CHATS_ENV = os.environ.get("SOURCE_CHATS", "@KOLscopeAlertsBot")
29
  SOURCE_CHATS = [s.strip() for s in SOURCE_CHATS_ENV.split(",") if s.strip()]
30
  TARGET_CHAT = os.environ.get("TARGET_CHAT", "@solana_trojanbot").strip()
@@ -37,8 +35,8 @@ RETRY_LABELS = [x.strip() for x in os.environ.get(
37
  RETRY_MAX_ATTEMPTS = int(os.environ.get("RETRY_MAX_ATTEMPTS", "12"))
38
  RETRY_SLEEP_SECONDS = float(os.environ.get("RETRY_SLEEP_SECONDS", "1.2"))
39
  RETRY_OVERALL_TIMEOUT = float(os.environ.get("RETRY_OVERALL_TIMEOUT", "60"))
40
-
41
- DEDUP_TTL_MINUTES = int(os.environ.get("DEDUP_TTL_MINUTES", "600")) # 10 jam default
42
 
43
  if not (API_ID and API_HASH and STRING_SESSION):
44
  raise RuntimeError("API_ID/API_HASH/STRING_SESSION belum di-set di Secrets.")
@@ -49,8 +47,8 @@ nest_asyncio.apply()
49
  CA_SOL_RE = re.compile(r"\b([1-9A-HJ-NP-Za-km-z]{32,44})(?:pump)?\b")
50
  CA_EVM_RE = re.compile(r"\b0x[a-fA-F0-9]{40}\b")
51
 
52
- BUY_PAT = re.compile(r"(?i)buy")
53
- RETRY_PAT = re.compile(r"(?i)(" + "|".join(map(re.escape, RETRY_LABELS)) + r")") if RETRY_LABELS else re.compile(r"$^")
54
 
55
  # ========= UTIL =========
56
  def _normalize_chat(s: str) -> str:
@@ -91,10 +89,36 @@ def find_first_ca(text: str) -> Optional[Tuple[str, str]]:
91
  logger.info("CA | ditemukan chain=%s addr=%s", chain, addr)
92
  return chain, addr
93
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
94
  # ========= STATE =========
95
  client = TelegramClient(StringSession(STRING_SESSION), API_ID, API_HASH)
96
  ca_queue: "asyncio.Queue[str]" = asyncio.Queue()
97
- # addr -> (timestamp, first_source_label)
98
  recent_ca_info: dict[str, tuple[float, str]] = {}
99
 
100
  def _is_recent(addr: str) -> bool:
@@ -104,7 +128,7 @@ def _is_recent(addr: str) -> bool:
104
  def _mark_ca(addr: str, source_label: str):
105
  recent_ca_info[addr] = (time.time(), source_label)
106
 
107
- # ========= BOT FLOW HELPERS =========
108
  async def refresh_message(msg):
109
  return await client.get_messages(msg.chat_id, ids=msg.id)
110
 
@@ -114,10 +138,8 @@ async def wait_new_keyboard(chat, *, min_id: int, timeout=25, interval=0.8):
114
  msgs = await client.get_messages(chat, limit=8)
115
  for m in msgs:
116
  if m.id > min_id and m.buttons:
117
- logger.debug("KB | keyboard baru id=%s", m.id)
118
  return m
119
  await asyncio.sleep(interval)
120
- logger.warning("KB | tidak menemukan keyboard (timeout=%ss)", timeout)
121
  return None
122
 
123
  async def click_first_match(msg, pat) -> bool:
@@ -130,134 +152,44 @@ async def click_first_match(msg, pat) -> bool:
130
  for j, btn in enumerate(row):
131
  label = (getattr(btn, "text", "") or "").strip()
132
  if label and pat.search(label):
133
- logger.info("UI | klik tombol: %s (i=%d, j=%d)", label, i, j)
134
  await msg.click(i, j)
135
  return True
136
  return False
137
 
138
- async def search_latest_with_button(chat, pat, *, min_id: int, limit=10):
139
- msgs = await client.get_messages(chat, limit=limit)
140
- for m in msgs:
141
- if m.id >= min_id and m.buttons:
142
- m2 = await client.get_messages(chat, ids=m.id)
143
- if m2 and await click_first_match(m2, pat):
144
- logger.debug("UI | tombol ditemukan di pesan id=%s", m.id)
145
- return m2
146
- return None
147
-
148
- async def click_retry_until_gone(anchor_msg, *, baseline_id: int) -> bool:
149
- attempts = 0
150
- started = time.monotonic()
151
- msg_kb = anchor_msg
152
- while attempts < RETRY_MAX_ATTEMPTS and (time.monotonic() - started) < (RETRY_OVERALL_TIMEOUT):
153
- msg_kb = await refresh_message(msg_kb)
154
- if await click_first_match(msg_kb, RETRY_PAT):
155
- attempts += 1
156
- logger.info("RET | klik Retry ke-%d", attempts)
157
- await asyncio.sleep(RETRY_SLEEP_SECONDS)
158
- continue
159
-
160
- msgs = await client.get_messages(TARGET_CHAT, limit=8)
161
- for m in msgs:
162
- if m.id >= baseline_id and m.buttons:
163
- m2 = await client.get_messages(TARGET_CHAT, ids=m.id)
164
- if m2 and await click_first_match(m2, RETRY_PAT):
165
- attempts += 1
166
- baseline_id = max(baseline_id, m2.id)
167
- logger.info("RET | klik Retry di msg.id=%s (total=%d)", m.id, attempts)
168
- await asyncio.sleep(RETRY_SLEEP_SECONDS)
169
- break
170
- else:
171
- break
172
- logger.info("RET | selesai (attempts=%d)", attempts)
173
- return True
174
-
175
  async def buy_flow_with_ca(addr: str):
176
  logger.info("FLOW| start buy_flow CA=%s", addr)
177
  last = await client.get_messages(TARGET_CHAT, limit=1)
178
  baseline_id = last[0].id if last else 0
179
-
180
  await client.send_message(TARGET_CHAT, addr)
181
- logger.info("SEND| CA terkirim ke target bot")
182
-
183
  msg_kb = await wait_new_keyboard(TARGET_CHAT, min_id=baseline_id, timeout=25)
184
  if not msg_kb:
185
- logger.warning("KB | no keyboard → fallback /start")
186
- await client.send_message(TARGET_CHAT, "/start")
187
- await asyncio.sleep(1.0)
188
- await client.send_message(TARGET_CHAT, addr)
189
- msg_kb = await wait_new_keyboard(TARGET_CHAT, min_id=baseline_id, timeout=25)
190
- if not msg_kb:
191
- logger.error("KB | gagal dapat keyboard setelah fallback")
192
- return
193
-
194
- if AMOUNT_LABEL:
195
- ok_amt = await click_first_match(msg_kb, re.compile(re.escape(AMOUNT_LABEL), re.IGNORECASE))
196
- logger.info("UI | pilih amount '%s' → %s", AMOUNT_LABEL, "OK" if ok_amt else "MISS")
197
- if ok_amt:
198
- await asyncio.sleep(1.0)
199
- msg2 = await wait_new_keyboard(TARGET_CHAT, min_id=baseline_id, timeout=12)
200
- msg_kb = msg2 or await refresh_message(msg_kb)
201
- baseline_id = max(baseline_id, getattr(msg_kb, "id", baseline_id))
202
-
203
- logger.info("STEP| klik 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
- logger.info("UI | BUY ditemukan di pesan lain")
211
- if not ok_buy and getattr(msg_kb, "buttons", None) and msg_kb.buttons[-1]:
212
- try:
213
- await msg_kb.click(len(msg_kb.buttons) - 1, 0)
214
- ok_buy = True
215
- logger.info("FALL| klik tombol terakhir sebagai BUY")
216
- except Exception as e:
217
- logger.debug("FALL| gagal klik tombol terakhir: %s", e)
218
-
219
- if not ok_buy:
220
- logger.warning("BUY | tombol tidak ditemukan → ulangi /start")
221
- await client.send_message(TARGET_CHAT, "/start")
222
- await asyncio.sleep(1.0)
223
- await client.send_message(TARGET_CHAT, addr)
224
- msg_kb2 = await wait_new_keyboard(TARGET_CHAT, min_id=baseline_id, timeout=25)
225
- msg_kb = msg_kb2 or msg_kb
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
- logger.error("BUY | tetap gagal setelah ulang")
230
- return
231
- msg_kb = hit2
232
- logger.info("UI | BUY ditemukan setelah ulang")
233
-
234
- await asyncio.sleep(1.0)
235
- await click_retry_until_gone(msg_kb, baseline_id=baseline_id)
236
  logger.info("DONE| buy_flow selesai untuk CA=%s", addr)
237
 
238
  # ========= ACCEPT / QUEUE =========
239
-
240
  def _accept_ca(addr: str, current_source: str) -> bool:
241
  if not addr:
242
  return False
243
  if _is_recent(addr):
244
- ts, first_src = recent_ca_info.get(addr, (0.0, "?"))
245
- age = time.time() - ts
246
- logger.info(
247
- "SKIP| CA %s duplikat (age=%.1fs < %ds) pertama dari [%s], sekarang dari [%s]",
248
- addr, age, DEDUP_TTL_MINUTES * 60, first_src, current_source
249
- )
250
  return False
251
  _mark_ca(addr, current_source)
252
  return True
253
 
254
  # ========= EVENT HANDLER =========
255
- # Attach handler directly to the client (fix: decorator must bind to client)
256
- @client.on(events.NewMessage(chats=[_normalize_chat(s) for s in SOURCE_CHATS]))
257
  async def on_message_from_sources(event: events.NewMessage.Event):
258
  src_label = _label_chat_from_event(event)
259
  txt = (event.raw_text or "").strip()
260
- logger.info("EVNT| pesan baru dari %s (len=%d)", src_label, len(txt))
 
 
 
 
 
 
 
 
261
 
262
  found = find_first_ca(txt)
263
  if not found:
@@ -283,18 +215,13 @@ async def _worker():
283
  ca_queue.task_done()
284
 
285
  async def start_bot_background():
286
- logger.info(
287
- "BOOT| starting bot (sources=%s, target=%s, dedup=%d min, log=%s)",
288
- ",".join([_normalize_chat(s) for s in SOURCE_CHATS]), TARGET_CHAT, DEDUP_TTL_MINUTES, LOG_LEVEL
289
- )
290
  await client.start()
291
  asyncio.create_task(_worker())
292
 
293
  async def stop_bot():
294
- logger.info("BOOT| stopping bot…")
295
  await client.disconnect()
296
 
297
- # Optional: quick runner for manual testing
298
  if __name__ == "__main__":
299
  async def _main():
300
  await start_bot_background()
 
4
  from telethon.sessions import StringSession
5
  from telethon.errors.rpcerrorlist import FloodWaitError
6
 
 
7
  # ========= LOGGING =========
8
  LOG_LEVEL = os.environ.get("LOG_LEVEL", "INFO").upper()
9
  logger = logging.getLogger("midastouch-bot")
 
23
  API_HASH = os.environ.get("API_HASH", "")
24
  STRING_SESSION = os.environ.get("STRING_SESSION", "")
25
 
 
26
  SOURCE_CHATS_ENV = os.environ.get("SOURCE_CHATS", "@KOLscopeAlertsBot")
27
  SOURCE_CHATS = [s.strip() for s in SOURCE_CHATS_ENV.split(",") if s.strip()]
28
  TARGET_CHAT = os.environ.get("TARGET_CHAT", "@solana_trojanbot").strip()
 
35
  RETRY_MAX_ATTEMPTS = int(os.environ.get("RETRY_MAX_ATTEMPTS", "12"))
36
  RETRY_SLEEP_SECONDS = float(os.environ.get("RETRY_SLEEP_SECONDS", "1.2"))
37
  RETRY_OVERALL_TIMEOUT = float(os.environ.get("RETRY_OVERALL_TIMEOUT", "60"))
38
+ DEDUP_TTL_MINUTES = int(os.environ.get("DEDUP_TTL_MINUTES", "600")) # 10 jam
39
+ MIN_MCAP_USD = float(os.environ.get("MIN_MCAP_USD", "70000")) # batas minimal MC
40
 
41
  if not (API_ID and API_HASH and STRING_SESSION):
42
  raise RuntimeError("API_ID/API_HASH/STRING_SESSION belum di-set di Secrets.")
 
47
  CA_SOL_RE = re.compile(r"\b([1-9A-HJ-NP-Za-km-z]{32,44})(?:pump)?\b")
48
  CA_EVM_RE = re.compile(r"\b0x[a-fA-F0-9]{40}\b")
49
 
50
+ BUY_PAT = re.compile(r"(?i)buy")
51
+ RETRY_PAT = re.compile(r"(?i)(" + "|".join(map(re.escape, RETRY_LABELS)) + r")") if RETRY_LABELS else re.compile(r"$^")
52
 
53
  # ========= UTIL =========
54
  def _normalize_chat(s: str) -> str:
 
89
  logger.info("CA | ditemukan chain=%s addr=%s", chain, addr)
90
  return chain, addr
91
 
92
+ # ========= MCAP PARSER =========
93
+ _MC_UNITS = {"K": 1e3, "M": 1e6, "B": 1e9}
94
+ _MC_PAT_NUM_THEN_TAG = re.compile(r"(?i)\b\$?\s*([\d][\d.,]*)\s*([KMB])?\s*(?:mcap|mc|market\s*cap)\b")
95
+ _MC_PAT_TAG_THEN_NUM = re.compile(r"(?i)\b(?:mcap|mc|market\s*cap)\s*[:@]?\s*\$?\s*([\d][\d.,]*)\s*([KMB])?\b")
96
+
97
+ def _to_float_num(num_str: str) -> float:
98
+ s = num_str.strip().replace(" ", "").replace("$", "")
99
+ has_dot, has_comma = "." in s, "," in s
100
+ if has_dot and has_comma:
101
+ s = s.replace(",", "")
102
+ elif has_comma and not has_dot:
103
+ s = s.replace(",", ".")
104
+ return float(s)
105
+
106
+ def parse_mcap_usd(text: str) -> Optional[float]:
107
+ if not text:
108
+ return None
109
+ for pat in (_MC_PAT_NUM_THEN_TAG, _MC_PAT_TAG_THEN_NUM):
110
+ m = pat.search(text)
111
+ if m:
112
+ raw_num = m.group(1)
113
+ unit = (m.group(2) or "").upper()
114
+ base = _to_float_num(raw_num)
115
+ mult = _MC_UNITS.get(unit, 1.0)
116
+ return base * mult
117
+ return None
118
+
119
  # ========= STATE =========
120
  client = TelegramClient(StringSession(STRING_SESSION), API_ID, API_HASH)
121
  ca_queue: "asyncio.Queue[str]" = asyncio.Queue()
 
122
  recent_ca_info: dict[str, tuple[float, str]] = {}
123
 
124
  def _is_recent(addr: str) -> bool:
 
128
  def _mark_ca(addr: str, source_label: str):
129
  recent_ca_info[addr] = (time.time(), source_label)
130
 
131
+ # ========= FLOW =========
132
  async def refresh_message(msg):
133
  return await client.get_messages(msg.chat_id, ids=msg.id)
134
 
 
138
  msgs = await client.get_messages(chat, limit=8)
139
  for m in msgs:
140
  if m.id > min_id and m.buttons:
 
141
  return m
142
  await asyncio.sleep(interval)
 
143
  return None
144
 
145
  async def click_first_match(msg, pat) -> bool:
 
152
  for j, btn in enumerate(row):
153
  label = (getattr(btn, "text", "") or "").strip()
154
  if label and pat.search(label):
 
155
  await msg.click(i, j)
156
  return True
157
  return False
158
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
159
  async def buy_flow_with_ca(addr: str):
160
  logger.info("FLOW| start buy_flow CA=%s", addr)
161
  last = await client.get_messages(TARGET_CHAT, limit=1)
162
  baseline_id = last[0].id if last else 0
 
163
  await client.send_message(TARGET_CHAT, addr)
 
 
164
  msg_kb = await wait_new_keyboard(TARGET_CHAT, min_id=baseline_id, timeout=25)
165
  if not msg_kb:
166
+ return
167
+ await click_first_match(msg_kb, BUY_PAT)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
168
  logger.info("DONE| buy_flow selesai untuk CA=%s", addr)
169
 
170
  # ========= ACCEPT / QUEUE =========
 
171
  def _accept_ca(addr: str, current_source: str) -> bool:
172
  if not addr:
173
  return False
174
  if _is_recent(addr):
 
 
 
 
 
 
175
  return False
176
  _mark_ca(addr, current_source)
177
  return True
178
 
179
  # ========= EVENT HANDLER =========
180
+ @events.register(events.NewMessage(chats=[_normalize_chat(s) for s in SOURCE_CHATS]))
 
181
  async def on_message_from_sources(event: events.NewMessage.Event):
182
  src_label = _label_chat_from_event(event)
183
  txt = (event.raw_text or "").strip()
184
+ logger.info("EVNT| pesan baru dari %s", src_label)
185
+
186
+ mc_usd = parse_mcap_usd(txt)
187
+ if mc_usd is not None:
188
+ if mc_usd < MIN_MCAP_USD:
189
+ logger.info("SKIP| MC=%.2f < %.2f → abaikan", mc_usd, MIN_MCAP_USD)
190
+ return
191
+ else:
192
+ logger.info("PASS| MC=%.2f ≥ %.2f → lanjutkan", mc_usd, MIN_MCAP_USD)
193
 
194
  found = find_first_ca(txt)
195
  if not found:
 
215
  ca_queue.task_done()
216
 
217
  async def start_bot_background():
218
+ logger.info("BOOT| starting bot (sources=%s, target=%s)", ",".join(SOURCE_CHATS), TARGET_CHAT)
 
 
 
219
  await client.start()
220
  asyncio.create_task(_worker())
221
 
222
  async def stop_bot():
 
223
  await client.disconnect()
224
 
 
225
  if __name__ == "__main__":
226
  async def _main():
227
  await start_bot_background()