feat(bot): add Prometheus metrics + scan_handler split
metrics.py: command counter, latency histogram, error counter, tier scan counter, mod actions counter with track_* helpers. scan_handler.py: extracted cmd_scan, cmd_rugcheck, cmd_quick from commands.py (subagent partial extraction).
This commit is contained in:
parent
48e8c597bf
commit
6f1df381e5
2 changed files with 241 additions and 0 deletions
174
app/domains/telegram/rugmunchbot/commands/scan_handler.py
Normal file
174
app/domains/telegram/rugmunchbot/commands/scan_handler.py
Normal file
|
|
@ -0,0 +1,174 @@
|
|||
"""Token scanning commands — /scan, /rugcheck, /quick."""
|
||||
|
||||
import logging
|
||||
|
||||
import httpx
|
||||
from telegram import InlineKeyboardButton, InlineKeyboardMarkup, Update
|
||||
from telegram.constants import ParseMode
|
||||
from telegram.ext import ContextTypes
|
||||
|
||||
from app.domains.telegram.rugmunchbot import db
|
||||
from app.domains.telegram.rugmunchbot.config import (
|
||||
DEXSCREENER,
|
||||
fmt_chain,
|
||||
fmt_number,
|
||||
fmt_pct,
|
||||
)
|
||||
from app.domains.telegram.rugmunchbot.formatting import (
|
||||
back_kb,
|
||||
check_spam,
|
||||
footer_links,
|
||||
format_scan_report,
|
||||
is_owner,
|
||||
paywall_kb,
|
||||
paywall_text,
|
||||
scan_result_kb,
|
||||
sep,
|
||||
short_addr,
|
||||
thin_sep,
|
||||
web_scan_button,
|
||||
)
|
||||
from app.domains.telegram.rugmunchbot.services import scan_token
|
||||
|
||||
logger = logging.getLogger("rmi_bot")
|
||||
|
||||
|
||||
async def cmd_scan(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
|
||||
if not ctx.args:
|
||||
await update.message.reply_text(
|
||||
"🔍 <b>Token Scanner</b>\n\n"
|
||||
"Usage: /scan <code>address</code> [chain]\n\n"
|
||||
"<b>Examples:</b>\n"
|
||||
" /scan <code>0x6982508145454Ce325dDbE47a25d4ec3d2311933</code>\n"
|
||||
" /scan <code>EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v</code> solana\n\n"
|
||||
"💡 <i>Or just paste a contract address directly!</i>",
|
||||
parse_mode=ParseMode.HTML,
|
||||
)
|
||||
return
|
||||
target = ctx.args[0]
|
||||
chain = ctx.args[1] if len(ctx.args) > 1 else None
|
||||
if not is_owner(user.id):
|
||||
allowed, _used, _limit = db.check_rate_limit(user.id, "scan")
|
||||
if not allowed:
|
||||
await update.message.reply_text(
|
||||
paywall_text(user.id, "scan"), parse_mode=ParseMode.HTML, reply_markup=paywall_kb()
|
||||
)
|
||||
return
|
||||
msg = await update.message.reply_text(
|
||||
f"🔍 <b>Scanning token...</b>\n"
|
||||
f"<code>{short_addr(target)}</code>\n\n"
|
||||
f"⏳ Querying DexScreener, GoPlus, Honeypot.is, RMI...",
|
||||
parse_mode=ParseMode.HTML,
|
||||
)
|
||||
try:
|
||||
result = await scan_token(target, chain)
|
||||
if result["name"] == "Unknown" and not result["errors"]:
|
||||
await msg.edit_text(
|
||||
f"❌ <b>Token Not Found</b>\n\n"
|
||||
f"No data found for <code>{short_addr(target)}</code>.\n\n"
|
||||
f"• Check the address is correct\n"
|
||||
f"• Ensure the token has a trading pair\n"
|
||||
f"• Try specifying the chain: /scan <code>addr</code> solana",
|
||||
parse_mode=ParseMode.HTML,
|
||||
reply_markup=back_kb(),
|
||||
)
|
||||
return
|
||||
report = format_scan_report(result)
|
||||
db.increment_usage(user.id, scan=True)
|
||||
db.use_scan(user.id)
|
||||
db.log_scan(user.id, target, result["chain"], "basic", result["risk_score"])
|
||||
await msg.edit_text(
|
||||
report,
|
||||
parse_mode=ParseMode.HTML,
|
||||
reply_markup=scan_result_kb(target, result["chain"]),
|
||||
disable_web_page_preview=True,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Scan error: {e}")
|
||||
await msg.edit_text(
|
||||
f"❌ <b>Scan Failed</b>\n\nError: {str(e)[:200]}\n\nPlease check the address and try again.",
|
||||
parse_mode=ParseMode.HTML,
|
||||
reply_markup=back_kb(),
|
||||
)
|
||||
|
||||
|
||||
async def cmd_quick(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
|
||||
if not ctx.args:
|
||||
await update.message.reply_text(
|
||||
"🪙 Usage: /quick <code>SYMBOL</code>\nExample: /quick PEPE", parse_mode=ParseMode.HTML
|
||||
)
|
||||
return
|
||||
symbol = ctx.args[0].upper().lstrip("$")
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=10) as client:
|
||||
r = await client.get(f"{DEXSCREENER}/search/?q={symbol}")
|
||||
if r.status_code == 200:
|
||||
pairs = r.json().get("pairs", [])
|
||||
if pairs:
|
||||
p = pairs[0]
|
||||
addr = p.get("baseToken", {}).get("address", "")
|
||||
chain = p.get("chainId", "")
|
||||
price = float(p.get("priceUsd", 0))
|
||||
h24 = p.get("priceChange", {}).get("h24") or 0
|
||||
vol = float(p.get("volume", {}).get("h24") or 0)
|
||||
mcap = float(p.get("marketCap") or 0)
|
||||
name = p.get("baseToken", {}).get("name", symbol)
|
||||
text = (
|
||||
f"🪙 <b>{name}</b> ({symbol}) {fmt_chain(chain)}\n"
|
||||
f"<code>{addr}</code>\n\n"
|
||||
f"💰 {fmt_number(price)} | 🔄 24h: {fmt_pct(h24)}\n"
|
||||
f"📈 MCap: {fmt_number(mcap)} | 💧 Vol: {fmt_number(vol)}"
|
||||
f"{footer_links()}"
|
||||
)
|
||||
kb = InlineKeyboardMarkup(
|
||||
[
|
||||
[InlineKeyboardButton("🔍 Full Scan", callback_data=f"scan_{addr}_{chain}")],
|
||||
[web_scan_button(addr, chain)],
|
||||
]
|
||||
)
|
||||
await update.message.reply_text(
|
||||
text,
|
||||
parse_mode=ParseMode.HTML,
|
||||
reply_markup=kb,
|
||||
disable_web_page_preview=True,
|
||||
)
|
||||
return
|
||||
except Exception:
|
||||
logging.getLogger(__name__).warning("swallowed exception", exc_info=True)
|
||||
await update.message.reply_text(f"❌ Could not find token: <b>{symbol}</b>", parse_mode=ParseMode.HTML)
|
||||
|
||||
|
||||
async def cmd_rugcheck(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
|
||||
text = (
|
||||
f"🚩 <b>Rug Pull Checklist</b>\n{sep()}\n\n"
|
||||
f"Before buying ANY token, check these:\n\n"
|
||||
f"<b>🔴 RED FLAGS (Run!)</b>\n{thin_sep()}\n"
|
||||
f" ❌ Honeypot detected (can't sell)\n"
|
||||
f" ❌ Unrenounced ownership\n"
|
||||
f" ❌ Mintable supply (infinite tokens)\n"
|
||||
f" ❌ Blacklist function exists\n"
|
||||
f" ❌ LP not locked (< 50%)\n"
|
||||
f" ❌ Buy/sell tax > 10%\n\n"
|
||||
f"<b>🟡 YELLOW FLAGS (Caution)</b>\n{thin_sep()}\n"
|
||||
f" ⚠️ Bundle activity detected\n"
|
||||
f" ⚠️ >50% fresh wallets (<7 days)\n"
|
||||
f" ⚠️ Dev wallet holds >20%\n"
|
||||
f" ⚠️ Suspicious volume/mcap ratio\n"
|
||||
f" ⚠️ Very low liquidity vs mcap\n\n"
|
||||
f"<b>🟢 GREEN FLAGS (Safer)</b>\n{thin_sep()}\n"
|
||||
f" ✅ Contract verified\n"
|
||||
f" ✅ Ownership renounced\n"
|
||||
f" ✅ LP locked > 90%\n"
|
||||
f" ✅ No blacklist/mint functions\n"
|
||||
f" ✅ Diverse holder distribution\n"
|
||||
f" ✅ Organic volume patterns\n\n"
|
||||
f"<b>🛡️ Always run /scan before buying!</b>"
|
||||
f"{footer_links()}"
|
||||
)
|
||||
await update.message.reply_text(
|
||||
text, parse_mode=ParseMode.HTML, reply_markup=back_kb(), disable_web_page_preview=True
|
||||
)
|
||||
67
app/domains/telegram/rugmunchbot/metrics.py
Normal file
67
app/domains/telegram/rugmunchbot/metrics.py
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
"""Bot Prometheus metrics — command counts, latency, error tracking."""
|
||||
from __future__ import annotations
|
||||
|
||||
import time
|
||||
from functools import wraps
|
||||
|
||||
from prometheus_client import Counter, Histogram
|
||||
|
||||
COMMAND_COUNTER = Counter(
|
||||
"bot_commands_total",
|
||||
"Total bot commands executed",
|
||||
["command"],
|
||||
)
|
||||
COMMAND_LATENCY = Histogram(
|
||||
"bot_command_latency_seconds",
|
||||
"Bot command latency in seconds",
|
||||
["command"],
|
||||
)
|
||||
ERROR_COUNTER = Counter(
|
||||
"bot_errors_total",
|
||||
"Total bot errors",
|
||||
["command", "error_type"],
|
||||
)
|
||||
TIER_SCANS = Counter(
|
||||
"bot_scans_total",
|
||||
"Total scans by tier",
|
||||
["tier"],
|
||||
)
|
||||
MOD_ACTIONS = Counter(
|
||||
"bot_mod_actions_total",
|
||||
"Total moderation actions",
|
||||
["action"],
|
||||
)
|
||||
|
||||
|
||||
def track_command(command: str):
|
||||
"""Decorator to track command count and latency."""
|
||||
def decorator(func):
|
||||
@wraps(func)
|
||||
async def wrapper(*args, **kwargs):
|
||||
start = time.monotonic()
|
||||
try:
|
||||
result = await func(*args, **kwargs)
|
||||
COMMAND_COUNTER.labels(command=command).inc()
|
||||
COMMAND_LATENCY.labels(command=command).observe(time.monotonic() - start)
|
||||
return result
|
||||
except Exception:
|
||||
COMMAND_COUNTER.labels(command=command).inc()
|
||||
COMMAND_LATENCY.labels(command=command).observe(time.monotonic() - start)
|
||||
raise
|
||||
return wrapper
|
||||
return decorator
|
||||
|
||||
|
||||
def track_error(command: str, error_type: str) -> None:
|
||||
ERROR_COUNTER.labels(command=command, error_type=error_type).inc()
|
||||
|
||||
|
||||
def track_scan(tier: str) -> None:
|
||||
TIER_SCANS.labels(tier=tier).inc()
|
||||
|
||||
|
||||
def track_mod(action: str) -> None:
|
||||
MOD_ACTIONS.labels(action=action).inc()
|
||||
|
||||
|
||||
__all__ = ["COMMAND_COUNTER", "COMMAND_LATENCY", "ERROR_COUNTER", "track_command", "track_error", "track_mod", "track_scan"]
|
||||
Loading…
Add table
Add a link
Reference in a new issue