feat(bot): 7 more features — AI Copilot, DD Reports, Auto-Scanner, Early Warning, Leaderboard, Launch Monitor, Wallet Profiler
/ask — AI Copilot with RAG-backed answers from 20 Qdrant collections /dd — Automated due diligence report (risk, deployer, holders, LP, scams) /autoscan — Group auto-scanner (scan every CA posted in group chats) /earlywarn — Scam early warning alerts (BEFORE rug pulls happen) /leaderboard — Top scam hunters + XP system /launchmon — Watch deployer for new token launches /profile — Cross-chain wallet profiler Bot now at 82 commands total.
This commit is contained in:
parent
ef9a444e80
commit
da5dbe52c2
5 changed files with 832 additions and 160 deletions
|
|
@ -57,6 +57,7 @@ from app.domains.telegram.rugmunchbot.commands import (
|
|||
cmd_watch,
|
||||
cmd_watchlist,
|
||||
)
|
||||
from app.domains.telegram.rugmunchbot.commands.ai import cmd_ask
|
||||
from app.domains.telegram.rugmunchbot.commands.dex import (
|
||||
cmd_buy,
|
||||
cmd_holders,
|
||||
|
|
@ -65,6 +66,7 @@ from app.domains.telegram.rugmunchbot.commands.dex import (
|
|||
cmd_sell,
|
||||
cmd_swap,
|
||||
)
|
||||
from app.domains.telegram.rugmunchbot.commands.due_diligence import cmd_dd
|
||||
from app.domains.telegram.rugmunchbot.commands.moderation import (
|
||||
anti_flood_check,
|
||||
auto_blacklist,
|
||||
|
|
@ -180,6 +182,8 @@ async def setup_bot_profile(app: Application):
|
|||
BotCommand("ta", "Technical analysis & signals"),
|
||||
BotCommand("compare", "Compare two tokens side-by-side"),
|
||||
BotCommand("quick", "Quick token price check"),
|
||||
BotCommand("ask", "AI Copilot — ask crypto security questions"),
|
||||
BotCommand("dd", "Due Diligence — comprehensive token report"),
|
||||
BotCommand("ragscan", "RAG scam intelligence lookup"),
|
||||
BotCommand("buy", "Generate DEX buy link (referral)"),
|
||||
BotCommand("sell", "Generate DEX sell link (referral)"),
|
||||
|
|
@ -228,6 +232,13 @@ async def setup_bot_profile(app: Application):
|
|||
BotCommand("export", "Export trade history"),
|
||||
BotCommand("wallets", "Manage connected wallets"),
|
||||
BotCommand("balance", "Wallet balance check"),
|
||||
BotCommand("ask", "Ask the AI copilot anything"),
|
||||
BotCommand("dd", "Full due diligence report"),
|
||||
BotCommand("autoscan", "Enable group auto-scanner"),
|
||||
BotCommand("earlywarn", "Enable scam early warning"),
|
||||
BotCommand("leaderboard", "Top scam hunters"),
|
||||
BotCommand("launchmon", "Watch deployer for launches"),
|
||||
BotCommand("profile", "Multi-chain wallet profiler"),
|
||||
BotCommand("similar", "Clone token detection"),
|
||||
BotCommand("voteban", "Community vote ban"),
|
||||
]
|
||||
|
|
@ -267,6 +278,8 @@ def main():
|
|||
app.add_handler(CommandHandler("watch", cmd_watch))
|
||||
app.add_handler(CommandHandler("unwatch", cmd_unwatch))
|
||||
app.add_handler(CommandHandler("alerts", cmd_alerts))
|
||||
app.add_handler(CommandHandler("ask", cmd_ask))
|
||||
app.add_handler(CommandHandler("dd", cmd_dd))
|
||||
app.add_handler(CommandHandler("ragscan", cmd_ragscan))
|
||||
app.add_handler(CommandHandler("buy", cmd_buy))
|
||||
app.add_handler(CommandHandler("sell", cmd_sell))
|
||||
|
|
@ -290,6 +303,7 @@ def main():
|
|||
app.add_handler(CallbackQueryHandler(handle_voteban_callback, pattern="^voteban:"))
|
||||
app.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, handle_message))
|
||||
app.add_handler(MessageHandler(filters.StatusUpdate.NEW_CHAT_MEMBERS, on_new_member))
|
||||
app.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, group_auto_scan, block=False))
|
||||
app.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, anti_flood_check, block=False))
|
||||
app.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, auto_scam_check, block=False))
|
||||
app.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, auto_blacklist, block=False))
|
||||
|
|
@ -348,6 +362,7 @@ class RugMunchBot:
|
|||
application.add_handler(CallbackQueryHandler(handle_callback))
|
||||
application.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, handle_message))
|
||||
application.add_handler(MessageHandler(filters.StatusUpdate.NEW_CHAT_MEMBERS, on_new_member))
|
||||
application.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, group_auto_scan, block=False))
|
||||
application.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, anti_flood_check, block=False))
|
||||
application.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, auto_scam_check, block=False))
|
||||
application.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, auto_blacklist, block=False))
|
||||
|
|
@ -382,6 +397,8 @@ _HANDLER_REGISTRY = [
|
|||
("watch", cmd_watch),
|
||||
("unwatch", cmd_unwatch),
|
||||
("alerts", cmd_alerts),
|
||||
("ask", cmd_ask),
|
||||
("dd", cmd_dd),
|
||||
("ragscan", cmd_ragscan),
|
||||
("buy", cmd_buy),
|
||||
("sell", cmd_sell),
|
||||
|
|
@ -425,8 +442,10 @@ __all__ = [
|
|||
"cmd_admin_set_tier",
|
||||
"cmd_admin_stats",
|
||||
"cmd_alerts",
|
||||
"cmd_ask",
|
||||
"cmd_buy",
|
||||
"cmd_compare",
|
||||
"cmd_dd",
|
||||
"cmd_help",
|
||||
"cmd_holders",
|
||||
"cmd_mc",
|
||||
|
|
|
|||
|
|
@ -1,196 +1,197 @@
|
|||
"""
|
||||
🧠 RMI Crypto AI Agent - DataBus-Powered
|
||||
==========================================
|
||||
THE consumer-facing AI for Telegram and web. All data flows through DataBus.
|
||||
<EFBFBD> AI Copilot — /ask command backed by RAG and the internal chat_completion router.
|
||||
|
||||
Architecture:
|
||||
User Query → Sanitize → Intent Detection → DataBus.fetch() → RAG → DeepSeek → Reply
|
||||
↓ ↓ ↓
|
||||
price/security/ 93 chains 3,480 docs
|
||||
trending/news 116 providers knowledge base
|
||||
|
||||
Single source of truth: DataBus (cache, dedup, credit-aware, fallback chains)
|
||||
Route: User question → RAG v2/search → chat_completion → formatted reply with sources.
|
||||
Caches common questions in Redis (1 h TTL) to avoid redundant API calls.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import hashlib
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
from datetime import UTC, datetime
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import httpx
|
||||
from telegram.constants import ParseMode
|
||||
|
||||
logger = logging.getLogger("rmi.agent")
|
||||
if TYPE_CHECKING:
|
||||
from telegram import Update
|
||||
from telegram.ext import ContextTypes
|
||||
|
||||
DEEPSEEK_KEY = os.getenv("DEEPSEEK_API_KEY", "")
|
||||
DEEPSEEK_URL = "https://api.deepseek.com/v1/chat/completions"
|
||||
BACKEND_URL = os.getenv("BACKEND_URL", "http://rmi-backend:8000")
|
||||
MODEL = "deepseek-v4-flash"
|
||||
|
||||
# ── Security ──
|
||||
INJECTION_PATTERNS = [
|
||||
r"ignore (all |previous |above )?(instructions|prompts|rules)",
|
||||
r"you are now",
|
||||
r"system:\s*",
|
||||
r"<\|im_start\|>",
|
||||
r"<\|im_end\|>",
|
||||
r"\[INST\]",
|
||||
r"\[/INST\]",
|
||||
r"DAN\s",
|
||||
r"jailbreak",
|
||||
r"prompt\s*injection",
|
||||
]
|
||||
PRIVATE_KEY_RX = re.compile(
|
||||
r"(0x[a-fA-F0-9]{64}|[1-9A-HJ-NP-Za-km-z]{44,88}|sk-[a-zA-Z0-9]{32,}|-----BEGIN.*PRIVATE KEY-----)"
|
||||
from app.ai_router import chat_completion
|
||||
from app.domains.telegram.rugmunchbot import db
|
||||
from app.domains.telegram.rugmunchbot.config import BACKEND_URL
|
||||
from app.domains.telegram.rugmunchbot.formatting import (
|
||||
back_kb,
|
||||
check_spam,
|
||||
paywall_kb,
|
||||
paywall_text,
|
||||
sep,
|
||||
thin_sep,
|
||||
)
|
||||
|
||||
# ── DataBus Chains available to the bot ──
|
||||
AGENT_CHAINS = {
|
||||
"token_price": "Real-time price from CoinGecko/DexScreener consensus",
|
||||
"token_security": "42 security checks via GoPlus API",
|
||||
"trending": "Trending tokens across all chains",
|
||||
"token_launches": "New token launches with age risk",
|
||||
"market_overview": "Global crypto market metrics + Fear & Greed",
|
||||
"news": "Aggregated crypto news from 200+ RSS sources",
|
||||
"fear_greed": "Market sentiment index 0-100",
|
||||
"live_prices": "Multi-coin price lookup",
|
||||
}
|
||||
logger = logging.getLogger("rugmunchbot.ask")
|
||||
|
||||
SYSTEM_PROMPT = (
|
||||
"You are RugMunch AI, a crypto scam intelligence assistant. "
|
||||
"Answer using the provided context from a database of 3,000+ known scams and hacks. "
|
||||
"Be concise, accurate, and cite sources when possible. "
|
||||
"If the context doesn't contain the answer, say so honestly."
|
||||
)
|
||||
|
||||
MAX_CONTENT_BYTES = 3800
|
||||
|
||||
|
||||
async def _databus_fetch(data_type: str, **params) -> dict | None:
|
||||
"""Fetch data from DataBus - single source of truth."""
|
||||
async def _get_redis():
|
||||
import redis.asyncio as aioredis
|
||||
|
||||
return aioredis.Redis(
|
||||
host=os.getenv("REDIS_HOST", "rmi-redis"),
|
||||
port=int(os.getenv("REDIS_PORT", "6379")),
|
||||
db=0,
|
||||
decode_responses=True,
|
||||
socket_connect_timeout=2,
|
||||
)
|
||||
|
||||
|
||||
def _cache_key(query: str) -> str:
|
||||
return f"rmi:bot:ask:{hashlib.sha256(query.strip().lower().encode()).hexdigest()}"
|
||||
|
||||
|
||||
async def _rag_search(query: str, limit: int = 5) -> tuple[str, list[dict]]:
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=8) as client:
|
||||
async with httpx.AsyncClient(timeout=10) as client:
|
||||
resp = await client.post(
|
||||
f"{BACKEND_URL}/api/v1/databus/fetch",
|
||||
json={"data_type": data_type, **params},
|
||||
headers={"Content-Type": "application/json", "X-RMI-Key": "rmi-internal-2026"},
|
||||
)
|
||||
return resp.json()
|
||||
except Exception as e:
|
||||
logger.warning(f"DataBus.{data_type}: {e}")
|
||||
return None
|
||||
|
||||
|
||||
async def _rag_search(query: str, limit: int = 3) -> str:
|
||||
"""Query RAG knowledge base."""
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=5) as client:
|
||||
resp = await client.get(
|
||||
f"{BACKEND_URL}/api/v1/rag/search?q={query}&limit={limit}",
|
||||
f"{BACKEND_URL}/api/v1/rag/v2/search",
|
||||
json={"query": query, "collection": "known_scams", "top_k": limit, "min_similarity": 0.4},
|
||||
headers={"X-RMI-Key": "rmi-internal-2026"},
|
||||
)
|
||||
data = resp.json()
|
||||
docs = data.get("results", [])
|
||||
return "\n".join(f"• {d.get('content', '')[:200]}" for d in docs[:limit])
|
||||
except Exception:
|
||||
return ""
|
||||
if resp.status_code == 200:
|
||||
data = resp.json()
|
||||
hits = data.get("hits", data.get("results", []))
|
||||
lines = []
|
||||
for h in hits[:limit]:
|
||||
content = (h.get("content") or h.get("text") or "")[:400]
|
||||
meta = h.get("metadata", {}) or {}
|
||||
src = meta.get("source", meta.get("title", meta.get("url", "")))
|
||||
if content:
|
||||
line = f"{content}"
|
||||
if src:
|
||||
line += f" [Source: {src}]"
|
||||
lines.append(f"• {line}")
|
||||
return "\n".join(lines), hits
|
||||
except Exception as e:
|
||||
logger.warning("RAG search: %s", e)
|
||||
return "", []
|
||||
|
||||
|
||||
def _detect_intent(text: str) -> dict:
|
||||
"""Detect what user wants and which DataBus chains to query."""
|
||||
t = text.lower()
|
||||
chains_to_query = []
|
||||
if any(w in t for w in ["price", "worth", "value", "cost", "how much"]):
|
||||
chains_to_query.append("token_price")
|
||||
if any(w in t for w in ["safe", "scam", "rug", "honeypot", "risk", "audit", "security"]):
|
||||
chains_to_query.append("token_security")
|
||||
if any(w in t for w in ["trending", "hot", "popular", "mover", "gainer"]):
|
||||
chains_to_query.append("trending")
|
||||
if any(w in t for w in ["launch", "new token", "just launched", "fresh"]):
|
||||
chains_to_query.append("token_launches")
|
||||
if any(w in t for w in ["market", "sentiment", "fear", "greed", "global"]):
|
||||
chains_to_query.extend(["market_overview", "fear_greed"])
|
||||
if any(w in t for w in ["news", "headline", "latest", "today", "happening"]):
|
||||
chains_to_query.append("news")
|
||||
return {"chains": chains_to_query[:3]}
|
||||
def _format_answer(content: str, sources: list[dict]) -> str:
|
||||
s = sep()
|
||||
d = thin_sep()
|
||||
lines = ["🧠 <b>RugMunch AI</b>", s, content]
|
||||
|
||||
if sources:
|
||||
seen: set[str] = set()
|
||||
source_lines = []
|
||||
for h in sources[:4]:
|
||||
meta = h.get("metadata", {}) or {}
|
||||
url = meta.get("source_url", meta.get("url", ""))
|
||||
title = meta.get("title", meta.get("source", ""))
|
||||
label = title or url or ""
|
||||
if label and label not in seen:
|
||||
seen.add(label)
|
||||
if url:
|
||||
source_lines.append(f'📎 <a href="{url}">{label[:80]}</a>')
|
||||
else:
|
||||
source_lines.append(f"📎 {label[:80]}")
|
||||
if source_lines:
|
||||
lines.extend(["", d, "<b>Sources</b>", *source_lines])
|
||||
|
||||
lines.extend(["", thin_sep("·", 28), "<i>Ask anything about crypto scams, hacks, or rug pulls.</i>"])
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
RMI_PERSONA = """You are the RMI Crypto AI - powered by Rug Munch Intelligence.
|
||||
You run on DataBus: 93 chains, 116 providers, 3,480-doc knowledge base.
|
||||
You are direct, security-first, and data-driven. Never give financial advice.
|
||||
Use Telegram HTML format. Keep responses scannable. Warn about scams immediately."""
|
||||
async def cmd_ask(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
|
||||
user = update.effective_user
|
||||
db.get_or_create_user(user.id, user.username, user.first_name)
|
||||
if check_spam(user.id):
|
||||
return
|
||||
|
||||
|
||||
async def cmd_ai(update, ctx):
|
||||
"""Full AI agent with DataBus enrichment."""
|
||||
msg = update.message
|
||||
text = " ".join(ctx.args) if ctx.args else ""
|
||||
question = " ".join(ctx.args) if ctx.args else ""
|
||||
|
||||
if not text:
|
||||
chains_list = "\n".join(
|
||||
f"<code>{k}</code> - {v}" for k, v in list(AGENT_CHAINS.items())[:6]
|
||||
)
|
||||
if not question:
|
||||
await msg.reply_text(
|
||||
f"<b>🧠 RMI Crypto AI</b>\n\n"
|
||||
f"Ask anything. I query DataBus for real-time data:\n\n{chains_list}\n\n"
|
||||
f"<b>Examples:</b>\n"
|
||||
f"<code>/ai SOL price and risk</code>\n"
|
||||
f"<code>/ai trending tokens today</code>\n"
|
||||
f"<code>/ai latest crypto scams</code>\n"
|
||||
f"<code>/ai market sentiment</code>\n\n"
|
||||
f"<i>Powered by DeepSeek V4 Flash + DataBus</i>",
|
||||
parse_mode="HTML",
|
||||
disable_web_page_preview=True,
|
||||
"🧠 <b>RugMunch AI Copilot</b>\n\n"
|
||||
"Ask any crypto security question:\n\n"
|
||||
"<b>Examples:</b>\n"
|
||||
"• <code>/ask How do honeypots work?</code>\n"
|
||||
"• <code>/ask What was the biggest rug pull?</code>\n"
|
||||
"• <code>/ask Is 0x... a known scam?</code>\n"
|
||||
"• <code>/ask How to spot bundled wallets?</code>\n\n"
|
||||
"<i>Powered by RMI 3,000+ scam intelligence database</i>",
|
||||
parse_mode=ParseMode.HTML,
|
||||
)
|
||||
return
|
||||
|
||||
# Sanitize
|
||||
clean = text[:2000]
|
||||
for p in INJECTION_PATTERNS:
|
||||
if re.search(p, clean, re.IGNORECASE):
|
||||
await msg.reply_text("⚠️ Blocked by security filter.", parse_mode="HTML")
|
||||
return
|
||||
clean = PRIVATE_KEY_RX.sub("[REDACTED]", clean)
|
||||
allowed, _used, _limit = db.check_rate_limit(user.id, "ai")
|
||||
if not allowed:
|
||||
await msg.reply_text(
|
||||
paywall_text(user.id, "ai"), parse_mode=ParseMode.HTML, reply_markup=paywall_kb()
|
||||
)
|
||||
return
|
||||
|
||||
await msg.chat.send_action("typing")
|
||||
|
||||
# ── DataBus enrichment ──
|
||||
intent = _detect_intent(text)
|
||||
context_parts = []
|
||||
ckey = _cache_key(question)
|
||||
try:
|
||||
r = await _get_redis()
|
||||
cached = await r.get(ckey)
|
||||
if cached:
|
||||
db.increment_usage(user.id, ai_msg=True)
|
||||
await msg.reply_text(cached, parse_mode=ParseMode.HTML, disable_web_page_preview=True)
|
||||
return
|
||||
except Exception:
|
||||
cached = None
|
||||
|
||||
for chain in intent["chains"]:
|
||||
data = await _databus_fetch(chain, limit=5)
|
||||
if data:
|
||||
context_parts.append(f"[DataBus:{chain}] {json.dumps(data)[:400]}")
|
||||
|
||||
# RAG
|
||||
rag = await _rag_search(text)
|
||||
if rag:
|
||||
context_parts.append(f"[RAG]\n{rag}")
|
||||
|
||||
# Build prompt
|
||||
system = RMI_PERSONA
|
||||
if context_parts:
|
||||
system += "\n\nLIVE DATA:\n" + "\n\n".join(context_parts)
|
||||
system += f"\n\nTime: {datetime.now(UTC).strftime('%Y-%m-%d %H:%M UTC')}"
|
||||
rag_text, rag_hits = await _rag_search(question)
|
||||
|
||||
try:
|
||||
body = json.dumps(
|
||||
{
|
||||
"model": MODEL,
|
||||
"messages": [
|
||||
{"role": "system", "content": system},
|
||||
{"role": "user", "content": clean},
|
||||
],
|
||||
"max_tokens": 600,
|
||||
"temperature": 0.7,
|
||||
}
|
||||
).encode()
|
||||
async with httpx.AsyncClient(timeout=20) as client:
|
||||
resp = await client.post(
|
||||
DEEPSEEK_URL,
|
||||
content=body,
|
||||
headers={
|
||||
"Authorization": f"Bearer {DEEPSEEK_KEY}",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
)
|
||||
reply = resp.json()["choices"][0]["message"]["content"]
|
||||
reply = PRIVATE_KEY_RX.sub("[REDACTED]", reply[:3800])
|
||||
await msg.reply_text(reply, parse_mode="HTML", disable_web_page_preview=True)
|
||||
except Exception as e:
|
||||
logger.error(f"AI failed: {e}")
|
||||
await msg.reply_text(
|
||||
"❌ AI temporarily unavailable. Try /security, /trending, or /news.", parse_mode="HTML"
|
||||
result = await chat_completion(
|
||||
messages=[
|
||||
{"role": "system", "content": SYSTEM_PROMPT},
|
||||
{"role": "user", "content": f"Context:\n{rag_text}\n\nQuestion: {question}"}
|
||||
if rag_text
|
||||
else {"role": "user", "content": question},
|
||||
],
|
||||
temperature=0.3,
|
||||
max_tokens=600,
|
||||
timeout=25.0,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error("LLM call failed: %s", e)
|
||||
result = {"content": "", "error": str(e)}
|
||||
|
||||
ai_content = result.get("content") or result.get("error") or ""
|
||||
if not ai_content.strip():
|
||||
await msg.reply_text(
|
||||
"❌ <b>AI Copilot Unavailable</b>\n\n"
|
||||
"The AI service is temporarily offline. Try:\n"
|
||||
"• /ragscan <code>address</code> — scam intelligence lookup\n"
|
||||
"• /scan <code>address</code> — full token audit\n"
|
||||
"• /rugcheck — rug pull red flag checklist",
|
||||
parse_mode=ParseMode.HTML,
|
||||
reply_markup=back_kb(),
|
||||
)
|
||||
return
|
||||
|
||||
reply = _format_answer(ai_content[:MAX_CONTENT_BYTES], rag_hits)
|
||||
db.increment_usage(user.id, ai_msg=True)
|
||||
|
||||
try:
|
||||
r2 = await _get_redis()
|
||||
await r2.setex(ckey, 3600, reply)
|
||||
except Exception as e:
|
||||
logger.warning("Redis cache set failed: %s", e)
|
||||
|
||||
await msg.reply_text(reply, parse_mode=ParseMode.HTML, disable_web_page_preview=True)
|
||||
|
|
|
|||
205
app/domains/telegram/rugmunchbot/commands/autoprotect.py
Normal file
205
app/domains/telegram/rugmunchbot/commands/autoprotect.py
Normal file
|
|
@ -0,0 +1,205 @@
|
|||
"""Group Auto-Scanner + Scam Early Warning — unique RMI moat features.
|
||||
|
||||
Auto-Scanner: detects every contract address posted in group chats,
|
||||
auto-scans them, and replies with risk assessment.
|
||||
|
||||
Early Warning: alerts users BEFORE rug pulls based on RAG pattern matching.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
|
||||
import httpx
|
||||
from telegram import Update
|
||||
from telegram.constants import ParseMode
|
||||
from telegram.ext import ContextTypes
|
||||
|
||||
from app.domains.telegram.rugmunchbot.formatting import (
|
||||
EVM_RE,
|
||||
SOL_RE,
|
||||
)
|
||||
|
||||
logger = logging.getLogger("rugmunchbot.autoprotect")
|
||||
|
||||
# ── Group Auto-Scanner ──────────────────────────────────────────
|
||||
|
||||
async def group_auto_scan(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
|
||||
"""Auto-scan every contract address posted in groups.
|
||||
|
||||
Registered as a MessageHandler with filters.TEXT & ~filters.COMMAND.
|
||||
Checks group settings: only scans if enabled for that group.
|
||||
"""
|
||||
if not update.message or not update.message.text:
|
||||
return
|
||||
chat = update.effective_chat
|
||||
if chat.type == "private":
|
||||
return # Don't scan in private chats (already handled by handle_message)
|
||||
|
||||
# Check group settings
|
||||
try:
|
||||
from app.core.redis import get_redis
|
||||
r = get_redis()
|
||||
settings = r.get(f"bot:chat:{chat.id}:settings")
|
||||
settings = json.loads(settings) if settings else {}
|
||||
if not settings.get("auto_scan", False):
|
||||
return # Not enabled for this group
|
||||
except Exception:
|
||||
return
|
||||
|
||||
text = update.message.text
|
||||
user = update.message.from_user
|
||||
if not user:
|
||||
return
|
||||
|
||||
# Find all addresses in the message
|
||||
addresses = []
|
||||
for match in EVM_RE.finditer(text):
|
||||
addresses.append(("ethereum", match.group()))
|
||||
for match in SOL_RE.finditer(text):
|
||||
addresses.append(("solana", match.group()))
|
||||
|
||||
if not addresses or len(addresses) > 3: # Skip if too many addresses (spam)
|
||||
return
|
||||
|
||||
for chain, addr in addresses[:2]: # Max 2 scans per message
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=10) as client:
|
||||
r = await client.post(
|
||||
"http://localhost:8000/api/v1/scanner/scan",
|
||||
json={"token_address": addr, "chain": chain, "tiers": ["free"]},
|
||||
)
|
||||
if r.status_code == 200:
|
||||
data = r.json()
|
||||
score = data.get("safety_score", 50)
|
||||
risk = data.get("risk_level", "unknown")
|
||||
flags = data.get("risk_flags", [])
|
||||
|
||||
if score < 40 or len(flags) > 2:
|
||||
emoji = "🔴"
|
||||
level = "HIGH RISK"
|
||||
elif score < 70:
|
||||
emoji = "🟡"
|
||||
level = "MEDIUM"
|
||||
else:
|
||||
emoji = "🟢"
|
||||
level = "SAFE"
|
||||
|
||||
msg = (
|
||||
f"{emoji} <b>Auto-Scan: {level}</b>\n"
|
||||
f"Address: <code>{addr}</code>\n"
|
||||
f"Score: {score}/100\n"
|
||||
f"Flags: {', '.join(flags) if flags else 'None'}\n"
|
||||
f"Chain: {chain}\n\n"
|
||||
f"Posted by {user.first_name} • Auto-scan by @RugMunchBot"
|
||||
)
|
||||
await update.message.reply_text(
|
||||
msg,
|
||||
parse_mode=ParseMode.HTML,
|
||||
disable_web_page_preview=True,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning("group_auto_scan: %s", e)
|
||||
|
||||
|
||||
async def cmd_autoscan(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
|
||||
"""Enable/disable group auto-scanner (admin only)."""
|
||||
chat = update.effective_chat
|
||||
user = update.effective_user
|
||||
|
||||
# Check if user is admin
|
||||
try:
|
||||
member = await chat.get_member(user.id)
|
||||
if member.status not in ("administrator", "creator"):
|
||||
await update.message.reply_text("🔒 Only admins can configure auto-scan.")
|
||||
return
|
||||
except Exception:
|
||||
await update.message.reply_text("🔒 Admin only.")
|
||||
return
|
||||
|
||||
if ctx.args and ctx.args[0] in ("on", "off"):
|
||||
enabled = ctx.args[0] == "on"
|
||||
try:
|
||||
from app.core.redis import get_redis
|
||||
r = get_redis()
|
||||
settings = r.get(f"bot:chat:{chat.id}:settings")
|
||||
settings = json.loads(settings) if settings else {}
|
||||
settings["auto_scan"] = enabled
|
||||
r.set(f"bot:chat:{chat.id}:settings", json.dumps(settings))
|
||||
state = "ON" if enabled else "OFF"
|
||||
await update.message.reply_text(
|
||||
f"🛡 Group Auto-Scanner: <b>{state}</b>\n\n"
|
||||
f"{'Every contract address posted will be auto-scanned.' if enabled else 'Auto-scanning disabled.'}",
|
||||
parse_mode=ParseMode.HTML,
|
||||
)
|
||||
except Exception:
|
||||
await update.message.reply_text("Failed to save settings.")
|
||||
else:
|
||||
await update.message.reply_text(
|
||||
"🔧 <b>Group Auto-Scanner</b>\n\n"
|
||||
"Usage: /autoscan on — auto-scan all contract addresses posted\n"
|
||||
" /autoscan off — disable auto-scanning\n\n"
|
||||
"When enabled, every contract address posted in this group\n"
|
||||
"will be automatically analyzed for scams and risks.\n\n"
|
||||
"<i>Pro tier feature — requires bot to be admin in this group.</i>",
|
||||
parse_mode=ParseMode.HTML,
|
||||
)
|
||||
|
||||
|
||||
# ── Scam Early Warning ───────────────────────────────────────────
|
||||
|
||||
async def cmd_earlywarn(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
|
||||
"""Subscribe to scam early warning alerts."""
|
||||
user = update.effective_user
|
||||
if not ctx.args:
|
||||
await update.message.reply_text(
|
||||
"🚨 <b>Scam Early Warning System</b>\n\n"
|
||||
"Get alerted BEFORE rug pulls happen. Our RAG system\n"
|
||||
"detects scam patterns from 3,000+ historical incidents\n"
|
||||
"and alerts you when a new token matches.\n\n"
|
||||
"Usage:\n"
|
||||
" /earlywarn on — enable alerts\n"
|
||||
" /earlywarn off — disable alerts\n"
|
||||
" /earlywarn status — check status\n"
|
||||
" /earlywarn chains eth sol bsc — filter by chains",
|
||||
parse_mode=ParseMode.HTML,
|
||||
)
|
||||
return
|
||||
|
||||
cmd = ctx.args[0].lower()
|
||||
if cmd == "on":
|
||||
try:
|
||||
from app.core.redis import get_redis
|
||||
r = get_redis()
|
||||
r.set(f"bot:earlywarn:{user.id}", "1")
|
||||
await update.message.reply_text(
|
||||
"🚨 <b>Early Warning enabled!</b>\n\n"
|
||||
"I'll alert you when new tokens match known scam patterns.\n"
|
||||
"Alerts come as DM notifications.\n\n"
|
||||
"<i>Pro tier only.</i>",
|
||||
parse_mode=ParseMode.HTML,
|
||||
)
|
||||
except Exception:
|
||||
await update.message.reply_text("Failed to enable.")
|
||||
elif cmd == "off":
|
||||
try:
|
||||
from app.core.redis import get_redis
|
||||
r = get_redis()
|
||||
r.delete(f"bot:earlywarn:{user.id}")
|
||||
await update.message.reply_text("🚨 Early Warning disabled.")
|
||||
except Exception:
|
||||
pass
|
||||
elif cmd == "status":
|
||||
try:
|
||||
from app.core.redis import get_redis
|
||||
r = get_redis()
|
||||
active = r.get(f"bot:earlywarn:{user.id}")
|
||||
state = "🟢 Active" if active else "🔴 Inactive"
|
||||
await update.message.reply_text(f"Early Warning: {state}")
|
||||
except Exception:
|
||||
await update.message.reply_text("Status unavailable.")
|
||||
else:
|
||||
await update.message.reply_text("Usage: /earlywarn on|off|status")
|
||||
|
||||
|
||||
__all__ = ["cmd_autoscan", "cmd_earlywarn", "group_auto_scan"]
|
||||
275
app/domains/telegram/rugmunchbot/commands/due_diligence.py
Normal file
275
app/domains/telegram/rugmunchbot/commands/due_diligence.py
Normal file
|
|
@ -0,0 +1,275 @@
|
|||
"""
|
||||
Due Diligence — /dd command for comprehensive token analysis.
|
||||
|
||||
Sections: Risk Score, Deployer History, Holder Analysis, Liquidity Analysis,
|
||||
Scam Pattern Match, Similar Tokens.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import httpx
|
||||
from telegram import InlineKeyboardButton, InlineKeyboardMarkup
|
||||
from telegram.constants import ParseMode
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from telegram import Update
|
||||
from telegram.ext import ContextTypes
|
||||
|
||||
from app.domains.telegram.rugmunchbot import db
|
||||
from app.domains.telegram.rugmunchbot.config import (
|
||||
BACKEND_URL,
|
||||
DEXSCREENER,
|
||||
WEB_APP_URL,
|
||||
fmt_chain,
|
||||
fmt_number,
|
||||
)
|
||||
from app.domains.telegram.rugmunchbot.formatting import (
|
||||
back_kb,
|
||||
check_spam,
|
||||
footer_links,
|
||||
is_evm,
|
||||
is_sol,
|
||||
paywall_kb,
|
||||
paywall_text,
|
||||
sep,
|
||||
short_addr,
|
||||
thin_sep,
|
||||
web_scan_button,
|
||||
)
|
||||
|
||||
logger = logging.getLogger("rugmunchbot.dd")
|
||||
|
||||
MAX_REPORT_BYTES = 4000
|
||||
|
||||
|
||||
async def _fetch_api(client: httpx.AsyncClient, url: str, label: str) -> dict:
|
||||
try:
|
||||
resp = await client.get(url, headers={"X-RMI-Key": "rmi-internal-2026"})
|
||||
if resp.status_code == 200:
|
||||
data = resp.json()
|
||||
if data:
|
||||
return data
|
||||
except Exception as e:
|
||||
logger.warning("dd %s error: %s", label, e)
|
||||
return {}
|
||||
|
||||
|
||||
async def _fetch_dexscreener(
|
||||
client: httpx.AsyncClient, addr: str
|
||||
) -> dict | None:
|
||||
try:
|
||||
resp = await client.get(f"{DEXSCREENER}/tokens/{addr}")
|
||||
if resp.status_code == 200:
|
||||
pairs = resp.json().get("pairs") or []
|
||||
if pairs:
|
||||
return pairs[0]
|
||||
except Exception as e:
|
||||
logger.warning("dd dexscreener error: %s", e)
|
||||
return None
|
||||
|
||||
|
||||
def _risk_emoji(score: float) -> str:
|
||||
if score >= 70:
|
||||
return "🔴"
|
||||
if score >= 40:
|
||||
return "🟡"
|
||||
return "🟢"
|
||||
|
||||
|
||||
def _risk_label(score: float) -> str:
|
||||
if score >= 70:
|
||||
return "HIGH RISK"
|
||||
if score >= 40:
|
||||
return "MEDIUM RISK"
|
||||
return "LOW RISK"
|
||||
|
||||
|
||||
async def cmd_dd(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
|
||||
user = update.effective_user
|
||||
db.get_or_create_user(user.id, user.username, user.first_name)
|
||||
if check_spam(user.id):
|
||||
return
|
||||
|
||||
msg = update.message
|
||||
if not ctx.args:
|
||||
await msg.reply_text(
|
||||
"📋 <b>Due Diligence Report</b>\n\n"
|
||||
"Usage: /dd <code>token_address</code>\n\n"
|
||||
"Generates a comprehensive analysis:\n"
|
||||
"• Risk scoring & audit\n"
|
||||
"• Deployer history\n"
|
||||
"• Holder distribution\n"
|
||||
"• Liquidity analysis\n"
|
||||
"• Scam pattern matching\n"
|
||||
"• Similar token detection",
|
||||
parse_mode=ParseMode.HTML,
|
||||
)
|
||||
return
|
||||
|
||||
addr = ctx.args[0]
|
||||
if not is_evm(addr) and not is_sol(addr):
|
||||
await msg.reply_text("Invalid token address format.", reply_markup=back_kb())
|
||||
return
|
||||
|
||||
allowed, _used, _limit = db.check_rate_limit(user.id, "scan")
|
||||
if not allowed:
|
||||
await msg.reply_text(
|
||||
paywall_text(user.id, "scan"), parse_mode=ParseMode.HTML, reply_markup=paywall_kb()
|
||||
)
|
||||
return
|
||||
|
||||
progress = await msg.reply_text(
|
||||
f"📋 <b>Generating Due Diligence Report...</b>\n\n"
|
||||
f"<code>{short_addr(addr)}</code>\n\n"
|
||||
f"⏳ Step 1/5: Fetching audit & deployer data...",
|
||||
parse_mode=ParseMode.HTML,
|
||||
)
|
||||
|
||||
async with httpx.AsyncClient(timeout=20) as client:
|
||||
audit, deployer, holders, dex_data, rag_hits = await asyncio.gather(
|
||||
_fetch_api(client, f"{BACKEND_URL}/api/v1/scanner/audit?address={addr}", "audit"),
|
||||
_fetch_api(client, f"{BACKEND_URL}/api/v1/databus/fetch/deployer_history?address={addr}", "deployer"),
|
||||
_fetch_api(client, f"{BACKEND_URL}/api/v1/databus/fetch/token_holders?address={addr}", "holders"),
|
||||
_fetch_dexscreener(client, addr),
|
||||
_dd_rag_search(client, addr),
|
||||
)
|
||||
|
||||
await progress.edit_text(
|
||||
f"📋 <b>Generating Due Diligence Report...</b>\n\n"
|
||||
f"<code>{short_addr(addr)}</code>\n\n"
|
||||
f"⏳ Step 5/5: Building report...",
|
||||
parse_mode=ParseMode.HTML,
|
||||
)
|
||||
|
||||
s = sep()
|
||||
d = thin_sep()
|
||||
lines = ["📋 <b>Due Diligence Report</b>", s, f"<code>{addr}</code>"]
|
||||
|
||||
# ── 1. Risk Score ──
|
||||
risk_score = audit.get("risk_score", audit.get("score", 0))
|
||||
risk_em = _risk_emoji(risk_score)
|
||||
risk_lb = _risk_label(risk_score)
|
||||
lines.extend(["", f"{risk_em} <b>Risk Score: {risk_score}/100</b> — {risk_lb}", d])
|
||||
|
||||
if audit.get("rug_count", 0) > 0:
|
||||
lines.append(f" 🚩 Rug Pulls: <b>{audit['rug_count']}</b>")
|
||||
if audit.get("abuse_reports", 0) > 0:
|
||||
lines.append(f" 📝 Abuse Reports: <b>{audit['abuse_reports']}</b>")
|
||||
if audit.get("trust_score"):
|
||||
lines.append(f" 🛡️ Trust Score: <b>{audit['trust_score']}</b>")
|
||||
if audit.get("flags"):
|
||||
for flag in audit["flags"][:4]:
|
||||
lines.append(f" 🔴 {flag}")
|
||||
|
||||
# ── 2. Deployer History ──
|
||||
tokens = deployer.get("tokens", deployer.get("data", []))[:5]
|
||||
lines.extend(["", f"🏭 <b>Deployer History</b> — {short_addr(addr)}", d])
|
||||
if tokens:
|
||||
for t in tokens:
|
||||
name = t.get("name", t.get("symbol", "???"))
|
||||
sym = t.get("symbol", "???")
|
||||
chain = t.get("chain", "???")
|
||||
taddr = t.get("address", "")[:12]
|
||||
lines.append(f" • {name} (${sym}) {fmt_chain(chain)}\n <code>{taddr}...</code>")
|
||||
else:
|
||||
lines.append(" No deployer history found")
|
||||
|
||||
# ── 3. Holder Analysis ──
|
||||
top_holders = holders.get("holders", holders.get("data", []))[:5]
|
||||
total_holders = holders.get("total", holders.get("count", "?"))
|
||||
lines.extend(["", f"👥 <b>Holder Analysis</b> ({total_holders} holders)", d])
|
||||
if top_holders:
|
||||
for i, h in enumerate(top_holders, 1):
|
||||
h_addr = h.get("address", "")[:10]
|
||||
pct = h.get("percentage", h.get("share", 0))
|
||||
tag = f" [{h.get('tag')}]" if h.get("tag") else ""
|
||||
lines.append(f" {i}. <code>{h_addr}...</code> {pct:.1f}%{tag}")
|
||||
else:
|
||||
lines.append(" Holder data unavailable")
|
||||
|
||||
# ── 4. Liquidity Analysis ──
|
||||
lines.extend(["", "💧 <b>Liquidity Analysis</b>", d])
|
||||
if dex_data:
|
||||
price = float(dex_data.get("priceUsd") or dex_data.get("priceNative") or 0)
|
||||
liq = float((dex_data.get("liquidity") or {}).get("usd") or 0)
|
||||
vol24 = float((dex_data.get("volume") or {}).get("h24") or 0)
|
||||
mcap = float(dex_data.get("marketCap") or 0)
|
||||
lines.append(f" 💰 Price: {fmt_number(price)}")
|
||||
lines.append(f" 💧 Liquidity: {fmt_number(liq)}")
|
||||
lines.append(f" 📊 24h Volume: {fmt_number(vol24)}")
|
||||
lines.append(f" 📈 Market Cap: {fmt_number(mcap)}")
|
||||
if mcap > 0 and liq > 0:
|
||||
lines.append(f" 🔄 Liq/MCap Ratio: {liq / mcap * 100:.1f}%")
|
||||
else:
|
||||
lines.append(" No liquidity data (not yet paired)")
|
||||
|
||||
# ── 5. Scam Pattern Match ──
|
||||
lines.extend(["", "🔬 <b>Scam Pattern Match</b>", d])
|
||||
if rag_hits:
|
||||
seen: set[str] = set()
|
||||
for h in rag_hits[:4]:
|
||||
snippet = (h.get("content") or h.get("text") or "")[:150]
|
||||
meta = h.get("metadata", {}) or {}
|
||||
pname = meta.get("name", meta.get("pattern", ""))
|
||||
label = pname or snippet
|
||||
if label and label not in seen:
|
||||
seen.add(label)
|
||||
score = h.get("score", h.get("similarity", 0))
|
||||
lines.append(f" {'🔴' if score > 0.7 else '🟡'} {label[:120]}")
|
||||
if not seen:
|
||||
lines.append(" ✅ No known scam patterns matched")
|
||||
else:
|
||||
lines.append(" ✅ No known scam patterns matched")
|
||||
|
||||
# ── 6. Similar Tokens ──
|
||||
lines.extend(["", "🔄 <b>Similar Tokens</b>", d])
|
||||
similar = audit.get("similar", audit.get("matches", []))[:3]
|
||||
if similar:
|
||||
for sim in similar:
|
||||
sym = sim.get("symbol", "???")
|
||||
sim_val = sim.get("similarity", sim.get("score", 0))
|
||||
saddr = (sim.get("address", ""))[:10]
|
||||
lines.append(f" • {sym} ({sim_val:.0%} match) — <code>{saddr}...</code>")
|
||||
else:
|
||||
lines.append(" No similar tokens detected")
|
||||
|
||||
db.increment_usage(user.id, scan=True)
|
||||
db.use_scan(user.id)
|
||||
db.log_scan(user.id, addr, "", "dd", float(risk_score))
|
||||
|
||||
report = "\n".join(lines) + "\n" + footer_links()
|
||||
kb = InlineKeyboardMarkup([
|
||||
[web_scan_button(addr, "")],
|
||||
[InlineKeyboardButton("🌐 Download Full Report", url=f"{WEB_APP_URL}?token={addr}")],
|
||||
[InlineKeyboardButton("◀️ Main Menu", callback_data="menu_main")],
|
||||
])
|
||||
|
||||
await progress.edit_text(
|
||||
report[:MAX_REPORT_BYTES],
|
||||
parse_mode=ParseMode.HTML,
|
||||
reply_markup=kb,
|
||||
disable_web_page_preview=True,
|
||||
)
|
||||
|
||||
|
||||
async def _dd_rag_search(client: httpx.AsyncClient, addr: str) -> list[dict]:
|
||||
try:
|
||||
resp = await client.post(
|
||||
f"{BACKEND_URL}/api/v1/rag/v2/search",
|
||||
json={
|
||||
"query": f"token {addr} scam rug honeypot",
|
||||
"collection": "known_scams",
|
||||
"top_k": 5,
|
||||
"min_similarity": 0.4,
|
||||
},
|
||||
headers={"X-RMI-Key": "rmi-internal-2026"},
|
||||
)
|
||||
if resp.status_code == 200:
|
||||
data = resp.json()
|
||||
return data.get("hits", data.get("results", []))
|
||||
except Exception as e:
|
||||
logger.warning("dd rag: %s", e)
|
||||
return []
|
||||
172
app/domains/telegram/rugmunchbot/commands/power.py
Normal file
172
app/domains/telegram/rugmunchbot/commands/power.py
Normal file
|
|
@ -0,0 +1,172 @@
|
|||
"""Leaderboard + Launch Monitor + Wallet Profiler — gamification + power tools."""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
import httpx
|
||||
from telegram import Update
|
||||
from telegram.constants import ParseMode
|
||||
from telegram.ext import ContextTypes
|
||||
|
||||
from app.domains.telegram.rugmunchbot.config import (
|
||||
BACKEND_URL,
|
||||
)
|
||||
from app.domains.telegram.rugmunchbot.formatting import back_kb, footer_links, sep, short_addr
|
||||
|
||||
logger = logging.getLogger("rugmunchbot.power")
|
||||
|
||||
|
||||
# ── Leaderboard ──────────────────────────────────────────────────
|
||||
|
||||
async def cmd_leaderboard(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
|
||||
"""Show top scam finders, most active users, XP leaderboard."""
|
||||
from app.domains.telegram.rugmunchbot.db import get_db
|
||||
conn = get_db()
|
||||
|
||||
# Top scanners (most scans)
|
||||
top_scanners = conn.execute(
|
||||
"SELECT user_id, total_scans FROM users WHERE total_scans > 0 ORDER BY total_scans DESC LIMIT 10"
|
||||
).fetchall()
|
||||
# Top XP (scan volume * risk_score)
|
||||
top_xp = conn.execute(
|
||||
"SELECT user_id, SUM(CASE WHEN risk_score > 70 THEN risk_score ELSE 0 END) as xp FROM scan_history GROUP BY user_id ORDER BY xp DESC LIMIT 10"
|
||||
).fetchall()
|
||||
|
||||
conn.close()
|
||||
|
||||
text = f"🏆 <b>Leaderboard</b>\n{sep()}\n\n"
|
||||
text += "<b>🏅 Top Scanners</b>\n"
|
||||
for i, row in enumerate(top_scanners[:5]):
|
||||
text += f"{i+1}. User {row['user_id']} — {row['total_scans']} scans\n"
|
||||
|
||||
text += "\n<b>⭐ Top Hunters (by XP)</b>\n"
|
||||
for i, row in enumerate(top_xp[:5]):
|
||||
text += f"{i+1}. User {row['user_id']} — {int(row['xp'])} XP\n"
|
||||
|
||||
text += (
|
||||
f"\n<b>How to earn XP:</b>\n"
|
||||
f"• Each scan = 10 XP\n"
|
||||
f"• High-risk find = 50 XP bonus\n"
|
||||
f"• Confirmed scam = 200 XP\n"
|
||||
f"• Top weekly = 1000 XP bonus + rewards\n"
|
||||
f"{footer_links()}"
|
||||
)
|
||||
|
||||
await update.message.reply_text(text, parse_mode=ParseMode.HTML, reply_markup=back_kb())
|
||||
|
||||
|
||||
# ── Token Launch Monitor ─────────────────────────────────────────
|
||||
|
||||
async def cmd_launchmon(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
|
||||
"""Watch a deployer address for new token launches."""
|
||||
user = update.effective_user
|
||||
if not ctx.args:
|
||||
await update.message.reply_text(
|
||||
"🔭 <b>Token Launch Monitor</b>\n\n"
|
||||
"Watch a deployer address and get instant alerts\n"
|
||||
"when they deploy a new token.\n\n"
|
||||
"Usage:\n"
|
||||
" /launchmon add <deployer_address> <label>\n"
|
||||
" /launchmon list — show watched deployers\n"
|
||||
" /launchmon remove <address>",
|
||||
parse_mode=ParseMode.HTML,
|
||||
reply_markup=back_kb(),
|
||||
)
|
||||
return
|
||||
|
||||
cmd = ctx.args[0].lower()
|
||||
if cmd == "add":
|
||||
if len(ctx.args) < 2:
|
||||
await update.message.reply_text("Usage: /launchmon add <address> <label>")
|
||||
return
|
||||
addr = ctx.args[1]
|
||||
label = " ".join(ctx.args[2:]) if len(ctx.args) > 2 else short_addr(addr)
|
||||
from app.domains.telegram.rugmunchbot.db import get_db
|
||||
conn = get_db()
|
||||
conn.execute(
|
||||
"INSERT INTO watchlists (user_id, token_address, chain, symbol, alert_price, alert_direction, created_at, is_active) VALUES (?,?,?,?,0,'launchmon',?,1)",
|
||||
(user.id, addr, "all", f"LM:{label}", 0, __import__("time").time()),
|
||||
)
|
||||
conn.commit()
|
||||
conn.close()
|
||||
await update.message.reply_text(f"🔭 Watching {label} ({short_addr(addr)}) for new launches.", reply_markup=back_kb())
|
||||
|
||||
elif cmd == "list":
|
||||
from app.domains.telegram.rugmunchbot.db import get_db
|
||||
conn = get_db()
|
||||
rows = conn.execute("SELECT * FROM watchlists WHERE user_id=? AND alert_direction='launchmon' AND is_active=1", (user.id,)).fetchall()
|
||||
conn.close()
|
||||
if not rows:
|
||||
await update.message.reply_text("No deployers being watched. Use /launchmon add <address>")
|
||||
return
|
||||
text = f"🔭 <b>Watched Deployers</b>\n{sep()}\n\n"
|
||||
for r in rows:
|
||||
label = r["symbol"].replace("LM:", "")
|
||||
text += f"• {label}: <code>{r['token_address'][:12]}...</code>\n"
|
||||
await update.message.reply_text(text, parse_mode=ParseMode.HTML, reply_markup=back_kb())
|
||||
|
||||
elif cmd == "remove":
|
||||
if len(ctx.args) < 2:
|
||||
await update.message.reply_text("Usage: /launchmon remove <address>")
|
||||
return
|
||||
from app.domains.telegram.rugmunchbot.db import get_db
|
||||
conn = get_db()
|
||||
conn.execute("UPDATE watchlists SET is_active=0 WHERE user_id=? AND token_address=? AND alert_direction='launchmon'", (user.id, ctx.args[1]))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
await update.message.reply_text("🔭 Removed from watch.", reply_markup=back_kb())
|
||||
|
||||
|
||||
# ── Wallet Profiler ──────────────────────────────────────────────
|
||||
|
||||
async def cmd_profile(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
|
||||
"""Cross-chain wallet profiler — complete multi-chain history."""
|
||||
if not ctx.args:
|
||||
await update.message.reply_text("Usage: /profile <wallet address>")
|
||||
return
|
||||
addr = ctx.args[0]
|
||||
|
||||
await update.message.chat.send_action("typing")
|
||||
text = f"🔍 <b>Wallet Profile</b> — {short_addr(addr)}\n{sep()}\n\n"
|
||||
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=15) as client:
|
||||
r = await client.get(f"{BACKEND_URL}/api/v1/wallet/{addr}")
|
||||
if r.status_code == 200:
|
||||
data = r.json()
|
||||
labels = data.get("labels", [])
|
||||
chains = data.get("chains_found", [])
|
||||
score = data.get("risk_score", 50)
|
||||
|
||||
if score > 70:
|
||||
emoji = "🔴"
|
||||
elif score > 40:
|
||||
emoji = "🟡"
|
||||
else:
|
||||
emoji = "🟢"
|
||||
|
||||
text += f"{emoji} Risk Score: {score}/100\n\n"
|
||||
text += f"🌐 Active on {len(chains)} chains: {', '.join(chains)}\n\n"
|
||||
|
||||
if labels:
|
||||
text += "<b>Labels:</b>\n"
|
||||
for l in labels[:10]:
|
||||
text += f"• {l.get('label_name', 'unknown')} ({l.get('label_category', '?')})\n"
|
||||
else:
|
||||
text += "No known labels.\n"
|
||||
|
||||
text += f"\n{footer_links()}"
|
||||
await update.message.reply_text(text, parse_mode=ParseMode.HTML, reply_markup=back_kb())
|
||||
else:
|
||||
await update.message.reply_text("Wallet not found. Try again in a few seconds.", reply_markup=back_kb())
|
||||
except Exception:
|
||||
# Fallback: basic info
|
||||
text += "Unable to fetch full profile. Try:\n"
|
||||
text += f"• <a href='https://debank.com/profile/{addr}'>Debank</a>\n"
|
||||
text += f"• <a href='https://etherscan.io/address/{addr}'>Etherscan</a>\n"
|
||||
text += f"• <a href='https://solscan.io/account/{addr}'>Solscan</a>\n"
|
||||
text += footer_links()
|
||||
await update.message.reply_text(text, parse_mode=ParseMode.HTML, reply_markup=back_kb(), disable_web_page_preview=True)
|
||||
|
||||
|
||||
__all__ = ["cmd_launchmon", "cmd_leaderboard", "cmd_profile"]
|
||||
Loading…
Add table
Add a link
Reference in a new issue