agus1111 commited on
Commit
8b84fd2
·
verified ·
1 Parent(s): 19477b0

Update autotrack.py

Browse files
Files changed (1) hide show
  1. autotrack.py +88 -207
autotrack.py CHANGED
@@ -1,9 +1,4 @@
1
- # autotrack.py
2
- # Auto-track milestones (e.g., 2×) for any Contract Address (CA) mentioned in NEW messages
3
- # from YOUR group only: https://t.me/MidasTouchsignalll
4
- #
5
- # Data: Dexscreener (free) + fallback Jupiter (Solana)
6
- # Deps: telethon, aiohttp
7
 
8
  import os
9
  import re
@@ -15,46 +10,27 @@ from dataclasses import dataclass, field
15
  from typing import Optional, Dict, List, Tuple
16
  from datetime import datetime, timezone, timedelta
17
 
18
- from telethon import TelegramClient, events
19
- from telethon.sessions import StringSession, MemorySession
20
- from telethon.errors.rpcerrorlist import FloodWaitError
21
 
22
  # =========================
23
  # ENV / CONFIG
24
  # =========================
25
- API_ID = int(os.environ.get("API_ID", "0"))
26
- API_HASH = os.environ.get("API_HASH", "")
27
- STRING_SESSION = os.environ.get("STRING_SESSION", "")
28
-
29
- # Announce ke grup kamu (dipakai juga sebagai sumber pesan)
30
  TARGET_CHAT = os.environ.get("TARGET_CHAT", "https://t.me/MidasTouchsignalll")
31
-
32
- # Polling interval harga
33
  TRACK_POLL_SECS = int(os.environ.get("TRACK_POLL_SECS", "20"))
34
 
35
- # Milestones "x" sebagai string koma, contoh: "1.5,2,3"
36
- _ms_env = os.environ.get("MILESTONES", "2")
37
- try:
38
- DEFAULT_MILESTONES = sorted({float(x.strip()) for x in _ms_env.split(",") if x.strip()})
39
- except Exception:
40
- DEFAULT_MILESTONES = [2.0]
41
-
42
- STOP_WHEN_HIT = True # stop setelah milestone terbesar tercapai
43
 
44
- # Abaikan pesan lebih tua dari (startup - buffer)
45
  BACKFILL_BUFFER_MINUTES = int(os.environ.get("BACKFILL_BUFFER_MINUTES", "3"))
 
46
 
47
- # Marker antirecursive: ditambahkan ke semua pengumuman bot
48
- BOT_MARKER = "【MT-AUTOTRACK】"
49
-
50
- # =========================
51
  # HTTP endpoints
52
- # =========================
53
  DEXSCREENER_TOKEN_URL = "https://api.dexscreener.com/latest/dex/tokens/"
54
  JUPITER_URL = "https://price.jup.ag/v6/price?ids=" # free (Solana only)
55
 
56
  # =========================
57
- # Helpers
58
  # =========================
59
  def _fmt_money(x: Optional[float]) -> str:
60
  if x is None:
@@ -79,15 +55,12 @@ def _fmt_big(x: Optional[float]) -> str:
79
  # =========================
80
  CA_SOL_RE = re.compile(r"\b[1-9A-HJ-NP-Za-km-z]{32,48}\b") # Solana base58 (approx)
81
  CA_EVM_RE = re.compile(r"\b0x[a-fA-F0-9]{40}\b") # EVM 0x...
82
-
83
  CA_LABEL_RE = re.compile(r"\bCA\s*[:=]\s*(\S+)", re.IGNORECASE)
84
 
85
  def extract_ca(text: str) -> Optional[str]:
86
  """Return first found CA (EVM or Solana). Prefer 'CA: <addr>' label if present."""
87
  if not text:
88
  return None
89
-
90
- # Prefer explicit "CA: ..."
91
  m = CA_LABEL_RE.search(text)
92
  if m:
93
  cand = m.group(1)
@@ -95,8 +68,6 @@ def extract_ca(text: str) -> Optional[str]:
95
  return cand.lower()
96
  if CA_SOL_RE.fullmatch(cand):
97
  return cand
98
-
99
- # Else, scan for EVM then Sol
100
  m2 = CA_EVM_RE.search(text)
101
  if m2:
102
  return m2.group(0).lower()
@@ -106,7 +77,7 @@ def extract_ca(text: str) -> Optional[str]:
106
  return None
107
 
108
  # =========================
109
- # Tracker core
110
  # =========================
111
  @dataclass
112
  class TrackItem:
@@ -116,17 +87,16 @@ class TrackItem:
116
  entry_mcap: Optional[float] = None
117
  symbol_hint: Optional[str] = None
118
  source_link: Optional[str] = None
119
- milestones: List[float] = field(default_factory=lambda: [2.0])
120
- hit: Dict[float, bool] = field(default_factory=dict)
121
- poll_secs: int = 20
122
- stop_when_hit: bool = True
123
  started_at: float = field(default_factory=time.time)
124
 
125
  class PriceTracker:
126
  """
127
- Usage:
128
- tracker = PriceTracker(client, announce_chat=TARGET_CHAT)
129
- await tracker.start(ca=..., basis="mcap", milestones=[2], poll_secs=20, source_link=..., symbol_hint=...)
130
  """
131
  def __init__(self, client: TelegramClient, announce_chat):
132
  self.client = client
@@ -134,7 +104,7 @@ class PriceTracker:
134
  self._tasks: Dict[str, asyncio.Task] = {}
135
  self._items: Dict[str, TrackItem] = {}
136
 
137
- # ------------- HTTP helpers -------------
138
  async def _get(self, url: str, headers: Optional[Dict[str, str]] = None, timeout: int = 8):
139
  tout = aiohttp.ClientTimeout(total=timeout)
140
  async with aiohttp.ClientSession(timeout=tout, headers=headers) as sess:
@@ -146,7 +116,6 @@ class PriceTracker:
146
  async def _dexscreener_price(self, ca: str) -> Optional[Tuple[Optional[float], Optional[str], Optional[float]]]:
147
  """
148
  Return (priceUsd, symbol, marketCapUsd/FDV) from 'best pair' by highest USD liquidity.
149
- price or mcap may be None if unavailable.
150
  """
151
  data = await self._get(DEXSCREENER_TOKEN_URL + ca)
152
  if not data:
@@ -195,7 +164,7 @@ class PriceTracker:
195
  return {"price": jp, "symbol_hint": None, "mcap": None}
196
  return None
197
 
198
- # ------------- Announce helpers (brand EN) -------------
199
  def _name(self, item: TrackItem) -> str:
200
  s = item.symbol_hint or ""
201
  return f"{s} ({item.ca[:4]}…{item.ca[-4:]})" if s else item.ca
@@ -213,19 +182,19 @@ class PriceTracker:
213
  "New contract detected:\n\n"
214
  f"CA: `{item.ca}`\n\n"
215
  f"Tracking Basis: **{basis_label}**\n"
216
- f"Entry: {entry_disp} • Milestones: {', '.join([f'{m}×' for m in item.milestones])}\n"
217
  + (f"🔗 {item.source_link}\n" if item.source_link else "")
218
  + "\nRemember: This is a signal, not financial advice.\n"
219
  "Stay safe, stay golden ✨"
220
  )
221
 
222
- def _milestone_text(self, item: TrackItem, m: float, ratio: float, cur_price: Optional[float], cur_mcap: Optional[float]) -> str:
223
  if item.basis == "mcap":
224
  change = f"{_fmt_big(item.entry_mcap)} → {_fmt_big(cur_mcap)} (~{ratio:.2f}×)"
225
- milestone_line = f"Milestone reached: **{m}× Market Cap**"
226
  else:
227
  change = f"{_fmt_money(item.entry_price)} → {_fmt_money(cur_price)} (~{ratio:.2f}×)"
228
- milestone_line = f"Milestone reached: **{m}× Price**"
229
 
230
  elapsed = int(time.time() - item.started_at)
231
  mm, ss = divmod(elapsed, 60)
@@ -241,7 +210,7 @@ class PriceTracker:
241
  "Stay safe, stay golden ✨"
242
  )
243
 
244
- # ------------- Loop -------------
245
  async def _loop(self, item: TrackItem):
246
  # init snapshot
247
  snap = await self._get_snapshot(item.ca)
@@ -256,9 +225,13 @@ class PriceTracker:
256
  if not item.symbol_hint and snap.get("symbol_hint"):
257
  item.symbol_hint = snap.get("symbol_hint")
258
 
 
259
  if item.basis == "mcap" and not item.entry_mcap:
260
  item.basis = "price"
261
 
 
 
 
262
  basis_label = "Market Cap" if item.basis == "mcap" else "Price"
263
  await self._say(self._start_text(item, basis_label))
264
 
@@ -285,180 +258,88 @@ class PriceTracker:
285
  continue
286
  ratio = (cur_price / item.entry_price) if item.entry_price > 0 else 0.0
287
 
288
- # milestones
289
- for m in item.milestones:
290
- if item.hit.get(m):
291
- continue
292
- if ratio >= m:
293
- item.hit[m] = True
294
- await self._say(self._milestone_text(item, m, ratio, cur_price, cur_mcap))
295
- if item.stop_when_hit and m == max(item.milestones):
296
- await self._say(f"🛑 Tracking stopped for {self._name(item)} (final milestone reached).")
297
- return
 
 
 
 
 
 
 
 
298
 
299
  await asyncio.sleep(item.poll_secs)
300
 
301
- # ------------- Public -------------
302
  def is_tracking(self, ca: str) -> bool:
303
- t = self._tasks.get(ca)
304
- return bool(t) and not t.done()
305
-
306
- async def start(
307
- self,
308
- ca: str,
309
- *,
310
- basis: str = "mcap",
311
- entry_price: Optional[float] = None,
312
- entry_mcap: Optional[float] = None,
313
- milestones: List[float] = None,
314
- poll_secs: int = 20,
315
- source_link: Optional[str] = None,
316
- symbol_hint: Optional[str] = None,
317
- stop_when_hit: bool = True,
318
- ):
319
- ca = ca.strip()
320
  if self.is_tracking(ca):
321
  return
322
- if milestones is None or not milestones:
323
- milestones = DEFAULT_MILESTONES
324
-
325
- item = TrackItem(
326
- ca=ca,
327
- basis=basis.lower(),
328
- entry_price=entry_price,
329
- entry_mcap=entry_mcap,
330
- milestones=sorted(set(milestones)),
331
- poll_secs=poll_secs,
332
- source_link=source_link,
333
- symbol_hint=symbol_hint,
334
- stop_when_hit=stop_when_hit,
335
- )
336
- self._tasks[ca] = asyncio.create_task(self._loop(item))
337
-
338
-
339
 
340
  # =========================
341
- # Setup: attach to existing client from botsignal
342
  # =========================
343
- def setup_autotrack(shared_client: TelegramClient, announce_chat: str | None = None):
344
  """
345
- Attach autotrack to an existing Telethon client.
346
- - Registers the new-message handler for TARGET_CHAT
347
- - Initializes global tracker with the shared client
 
 
348
  """
349
- global client, tracker, startup_time_utc
350
- client = shared_client
351
- if announce_chat:
352
- ac = announce_chat
353
- else:
354
- ac = TARGET_CHAT
355
- # initialize tracker with shared client
356
- tracker = PriceTracker(client, announce_chat=ac)
357
- # startup timestamp for anti-backfill
358
- startup_time_utc = datetime.now(timezone.utc)
359
- # register handler
360
- client.add_event_handler(on_new_message, events.NewMessage(chats=(TARGET_CHAT,)))
361
- print("[AUTOTRACK] attached to shared client; listening on", TARGET_CHAT)
362
-
363
- # =========================
364
- # Client bootstrap
365
- # =========================
366
- def build_client() -> TelegramClient:
367
- if STRING_SESSION:
368
- print(">> Using StringSession (persistent).")
369
- return TelegramClient(StringSession(STRING_SESSION), API_ID, API_HASH)
370
- print(">> Using MemorySession (login each run).")
371
- return TelegramClient(MemorySession(), API_ID, API_HASH)
372
-
373
- client: TelegramClient | None = None
374
- startup_time_utc = None
375
- me_user_id: Optional[int] = None # filled at start
376
-
377
- tracker: PriceTracker | None = None
378
 
379
- # =========================
380
- # Event handler: ONLY your group
381
- # =========================
382
- CUTOFF_BUFFER = timedelta(minutes=BACKFILL_BUFFER_MINUTES)
383
-
384
- def _is_old_message(msg_dt: Optional[datetime]) -> bool:
385
- if not isinstance(msg_dt, datetime):
386
- return False
387
- # Telethon new-message handler biasanya sudah hanya message baru.
388
- # Buffer ini sebagai pengaman tambahan.
389
- return msg_dt.replace(tzinfo=timezone.utc) < (startup_time_utc - CUTOFF_BUFFER)
390
-
391
- def _is_bot_own_message(event) -> bool:
392
- # Jangan proses pesan yang dikirim oleh diri sendiri (script ini)
393
- if getattr(event, "out", False):
394
- return True
395
- txt = (event.raw_text or "") if hasattr(event, "raw_text") else ""
396
- return BOT_MARKER in (txt or "")
397
-
398
- async def on_new_message(event):
399
- try:
400
- # anti-loop: jangan proses pesan sendiri/marker
401
- if _is_bot_own_message(event):
402
  return
403
 
404
- msg = event.message
405
- if _is_old_message(getattr(msg, "date", None)):
406
- # ignore any old message near startup (extra safety)
407
- return
 
 
 
 
408
 
409
- text = msg.message or (getattr(msg, "raw_text", None) or "")
410
  ca = extract_ca(text)
411
  if not ca:
412
- return # no CA found
413
 
414
- # Optional: link back to source message
415
- source_link = None
416
  try:
417
- # If chat has a public username, build t.me link
418
- chat = await event.get_chat()
419
- uname = getattr(chat, "username", None)
420
- mid = getattr(msg, "id", None)
421
- if uname and mid:
422
- source_link = f"https://t.me/{uname}/{mid}"
423
  except:
424
- pass
425
-
426
- # Start tracking
427
- await tracker.start(
428
- ca=ca,
429
- basis="mcap",
430
- milestones=DEFAULT_MILESTONES,
431
- poll_secs=TRACK_POLL_SECS,
432
- source_link=source_link,
433
- stop_when_hit=STOP_WHEN_HIT,
434
- )
435
- # (Optional) send small ack? Usually start_text already covers announce.
436
- # Here we rely on _start_text inside tracker._loop
437
- except Exception as e:
438
- print(f"[AUTOTRACK] error: {e}")
439
 
440
- # =========================
441
- # Entrypoint
442
- # =========================
 
443
 
444
- async def main():
445
- global client, tracker, startup_time_utc
446
- if client is None:
447
- client = build_client()
448
- startup_time_utc = datetime.now(timezone.utc)
449
- tracker = PriceTracker(client, announce_chat=TARGET_CHAT)
450
- await client.start()
451
- try:
452
- me = await client.get_me()
453
- globals()["me_user_id"] = int(getattr(me, "id", 0))
454
- print(f">> Logged in as: {getattr(me, 'username', None) or me_user_id}")
455
- except Exception as e:
456
- print(f"Warning: cannot resolve self id: {e}")
457
-
458
- print("AutoTrack running. Listening ONLY your group for NEW messages...")
459
- # ensure handler is attached in standalone mode
460
- client.add_event_handler(on_new_message, events.NewMessage(chats=(TARGET_CHAT,)))
461
- await client.run_until_disconnected()
462
-
463
- if __name__ == "__main__":
464
- asyncio.run(main())
 
1
+
 
 
 
 
 
2
 
3
  import os
4
  import re
 
10
  from typing import Optional, Dict, List, Tuple
11
  from datetime import datetime, timezone, timedelta
12
 
13
+ from telethon import events, TelegramClient
 
 
14
 
15
  # =========================
16
  # ENV / CONFIG
17
  # =========================
 
 
 
 
 
18
  TARGET_CHAT = os.environ.get("TARGET_CHAT", "https://t.me/MidasTouchsignalll")
 
 
19
  TRACK_POLL_SECS = int(os.environ.get("TRACK_POLL_SECS", "20"))
20
 
21
+ MIN_START_MULT = float(os.environ.get("MIN_START_MULT", "1.5")) # start dari x1.5
22
+ STEP_MULT = float(os.environ.get("STEP_MULT", "0.5")) # naik 0.5 tiap hit
23
+ USE_MULTIPLICATIVE_STEP = os.environ.get("USE_MULTIPLICATIVE_STEP", "0") == "1"
 
 
 
 
 
24
 
 
25
  BACKFILL_BUFFER_MINUTES = int(os.environ.get("BACKFILL_BUFFER_MINUTES", "3"))
26
+ BOT_MARKER = os.environ.get("BOT_MARKER", "【MT-AUTOTRACK】")
27
 
 
 
 
 
28
  # HTTP endpoints
 
29
  DEXSCREENER_TOKEN_URL = "https://api.dexscreener.com/latest/dex/tokens/"
30
  JUPITER_URL = "https://price.jup.ag/v6/price?ids=" # free (Solana only)
31
 
32
  # =========================
33
+ # Helpers (formatting)
34
  # =========================
35
  def _fmt_money(x: Optional[float]) -> str:
36
  if x is None:
 
55
  # =========================
56
  CA_SOL_RE = re.compile(r"\b[1-9A-HJ-NP-Za-km-z]{32,48}\b") # Solana base58 (approx)
57
  CA_EVM_RE = re.compile(r"\b0x[a-fA-F0-9]{40}\b") # EVM 0x...
 
58
  CA_LABEL_RE = re.compile(r"\bCA\s*[:=]\s*(\S+)", re.IGNORECASE)
59
 
60
  def extract_ca(text: str) -> Optional[str]:
61
  """Return first found CA (EVM or Solana). Prefer 'CA: <addr>' label if present."""
62
  if not text:
63
  return None
 
 
64
  m = CA_LABEL_RE.search(text)
65
  if m:
66
  cand = m.group(1)
 
68
  return cand.lower()
69
  if CA_SOL_RE.fullmatch(cand):
70
  return cand
 
 
71
  m2 = CA_EVM_RE.search(text)
72
  if m2:
73
  return m2.group(0).lower()
 
77
  return None
78
 
79
  # =========================
80
+ # Tracker core (generator milestones)
81
  # =========================
82
  @dataclass
83
  class TrackItem:
 
87
  entry_mcap: Optional[float] = None
88
  symbol_hint: Optional[str] = None
89
  source_link: Optional[str] = None
90
+ poll_secs: int = TRACK_POLL_SECS
91
+ # generator state
92
+ next_target: float = field(default_factory=lambda: max(1.5, MIN_START_MULT))
 
93
  started_at: float = field(default_factory=time.time)
94
 
95
  class PriceTracker:
96
  """
97
+ Milestone tanpa batas.
98
+ - next_target dimulai dari MIN_START_MULT (default 1.5).
99
+ - Saat ratio >= next_target → announce, lalu next_target += STEP_MULT (atau *= (1+STEP) jika USE_MULTIPLICATIVE_STEP=1).
100
  """
101
  def __init__(self, client: TelegramClient, announce_chat):
102
  self.client = client
 
104
  self._tasks: Dict[str, asyncio.Task] = {}
105
  self._items: Dict[str, TrackItem] = {}
106
 
107
+ # ---------- HTTP ----------
108
  async def _get(self, url: str, headers: Optional[Dict[str, str]] = None, timeout: int = 8):
109
  tout = aiohttp.ClientTimeout(total=timeout)
110
  async with aiohttp.ClientSession(timeout=tout, headers=headers) as sess:
 
116
  async def _dexscreener_price(self, ca: str) -> Optional[Tuple[Optional[float], Optional[str], Optional[float]]]:
117
  """
118
  Return (priceUsd, symbol, marketCapUsd/FDV) from 'best pair' by highest USD liquidity.
 
119
  """
120
  data = await self._get(DEXSCREENER_TOKEN_URL + ca)
121
  if not data:
 
164
  return {"price": jp, "symbol_hint": None, "mcap": None}
165
  return None
166
 
167
+ # ---------- Announce ----------
168
  def _name(self, item: TrackItem) -> str:
169
  s = item.symbol_hint or ""
170
  return f"{s} ({item.ca[:4]}…{item.ca[-4:]})" if s else item.ca
 
182
  "New contract detected:\n\n"
183
  f"CA: `{item.ca}`\n\n"
184
  f"Tracking Basis: **{basis_label}**\n"
185
+ f"Entry: {entry_disp} • Next milestone: {item.next_target:.2f}×\n"
186
  + (f"🔗 {item.source_link}\n" if item.source_link else "")
187
  + "\nRemember: This is a signal, not financial advice.\n"
188
  "Stay safe, stay golden ✨"
189
  )
190
 
191
+ def _milestone_text(self, item: TrackItem, hit_level: float, ratio: float, cur_price: Optional[float], cur_mcap: Optional[float]) -> str:
192
  if item.basis == "mcap":
193
  change = f"{_fmt_big(item.entry_mcap)} → {_fmt_big(cur_mcap)} (~{ratio:.2f}×)"
194
+ milestone_line = f"Milestone reached: **{hit_level:.2f}× Market Cap**"
195
  else:
196
  change = f"{_fmt_money(item.entry_price)} → {_fmt_money(cur_price)} (~{ratio:.2f}×)"
197
+ milestone_line = f"Milestone reached: **{hit_level:.2f}× Price**"
198
 
199
  elapsed = int(time.time() - item.started_at)
200
  mm, ss = divmod(elapsed, 60)
 
210
  "Stay safe, stay golden ✨"
211
  )
212
 
213
+ # ---------- Loop ----------
214
  async def _loop(self, item: TrackItem):
215
  # init snapshot
216
  snap = await self._get_snapshot(item.ca)
 
225
  if not item.symbol_hint and snap.get("symbol_hint"):
226
  item.symbol_hint = snap.get("symbol_hint")
227
 
228
+ # Jika basis mcap tapi tidak ada mcap → fallback ke price
229
  if item.basis == "mcap" and not item.entry_mcap:
230
  item.basis = "price"
231
 
232
+ # Pastikan next_target minimal MIN_START_MULT dan > 1.0
233
+ item.next_target = max(MIN_START_MULT, 1.0001)
234
+
235
  basis_label = "Market Cap" if item.basis == "mcap" else "Price"
236
  await self._say(self._start_text(item, basis_label))
237
 
 
258
  continue
259
  ratio = (cur_price / item.entry_price) if item.entry_price > 0 else 0.0
260
 
261
+ # --- GENERATOR TANPA BATAS ---
262
+ # Kirim semua milestone yang terlewati (kalau harga "loncat")
263
+ # contoh: next_target 2.0 tapi ratio 2.7 → kirim 2.0 dan 2.5
264
+ hits_this_poll = 0
265
+ while ratio >= item.next_target:
266
+ # Anti spam: batasi hit per poll (optional)
267
+ if hits_this_poll >= 4:
268
+ break
269
+ hit_level = item.next_target
270
+ await self._say(self._milestone_text(item, hit_level, ratio, cur_price, cur_mcap))
271
+ # next target
272
+ if USE_MULTIPLICATIVE_STEP:
273
+ # multiplicative: misal STEP_MULT=0.5 => next *= 1.5
274
+ item.next_target = round(item.next_target * (1.0 + STEP_MULT), 6)
275
+ else:
276
+ # additive: next += 0.5
277
+ item.next_target = round(item.next_target + STEP_MULT, 6)
278
+ hits_this_poll += 1
279
 
280
  await asyncio.sleep(item.poll_secs)
281
 
282
+ # ---------- Public ----------
283
  def is_tracking(self, ca: str) -> bool:
284
+ return ca in self._tasks and not self._tasks[ca].done()
285
+
286
+ async def start(self, ca: str, basis: str = "mcap", source_link: Optional[str] = None, symbol_hint: Optional[str] = None, poll_secs: Optional[int] = None):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
287
  if self.is_tracking(ca):
288
  return
289
+ item = TrackItem(ca=ca, basis=basis, source_link=source_link, symbol_hint=symbol_hint)
290
+ if poll_secs:
291
+ item.poll_secs = poll_secs
292
+ task = asyncio.create_task(self._loop(item))
293
+ self._tasks[ca] = task
 
 
 
 
 
 
 
 
 
 
 
 
294
 
295
  # =========================
296
+ # Wiring ke Telethon: setup_autotrack(client)
297
  # =========================
298
+ def setup_autotrack(client: TelegramClient):
299
  """
300
+ Pasang handler yang:
301
+ - Mendengar pesan BARU dari TARGET_CHAT kamu.
302
+ - Ekstrak CA dari teks.
303
+ - Mulai tracking otomatis bila belum berjalan.
304
+ - Skip pesan yang berasal dari bot sendiri (ada BOT_MARKER).
305
  """
306
+ tracker = PriceTracker(client, announce_chat=TARGET_CHAT)
307
+ started_at = datetime.now(timezone.utc)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
308
 
309
+ @client.on(events.NewMessage(chats=TARGET_CHAT))
310
+ async def _on_new_message(ev: events.NewMessage.Event):
311
+ msg = ev.message
312
+ # Hindari recursive: jika pesan berisi marker bot, abaikan
313
+ if msg.raw_text and BOT_MARKER in msg.raw_text:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
314
  return
315
 
316
+ # Hindari backfill pesan lama di awal start
317
+ if msg.date:
318
+ if msg.date.tzinfo is None:
319
+ msg_dt = msg.date.replace(tzinfo=timezone.utc)
320
+ else:
321
+ msg_dt = msg.date.astimezone(timezone.utc)
322
+ if msg_dt < (started_at - timedelta(minutes=BACKFILL_BUFFER_MINUTES)):
323
+ return
324
 
325
+ text = (msg.raw_text or "").strip()
326
  ca = extract_ca(text)
327
  if not ca:
328
+ return
329
 
330
+ # Link sumber (opsional)
331
+ src_link = None
332
  try:
333
+ src_link = await client.get_messages(TARGET_CHAT, ids=msg.id)
334
+ if getattr(src_link, "id", None) is not None:
335
+ # gunakan t.me link standar (public group disarankan)
336
+ src_link = f"https://t.me/{ev.chat.username}/{msg.id}" if getattr(ev.chat, "username", None) else None
 
 
337
  except:
338
+ src_link = None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
339
 
340
+ # Start tracking bila belum
341
+ if not tracker.is_tracking(ca):
342
+ # default basis mcap; fallback ke price terjadi di _loop jika mcap tidak tersedia
343
+ await tracker.start(ca=ca, basis="mcap", source_link=src_link)
344
 
345
+ return tracker # optional: bisa dipakai kalau mau manual query status