Merged commercial + scraper: single 5099 app, real /api/search, BTCPay payments, 1085 channels
This commit is contained in:
469
app.py
Normal file
469
app.py
Normal file
@@ -0,0 +1,469 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Signal Miner X — Full-Stack Market Intelligence Platform
|
||||
BTCPay payments, API key auth, real-time Telegram marketplace scraping.
|
||||
Single app: commercial + scraper merged.
|
||||
"""
|
||||
import os, sys, json, sqlite3, secrets, re, time, asyncio, traceback
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from functools import wraps
|
||||
|
||||
from flask import Flask, request, jsonify, render_template_string, g
|
||||
|
||||
BASE_DIR = Path("/opt/signal-miner")
|
||||
sys.path.insert(0, str(BASE_DIR))
|
||||
|
||||
# ── BTCPay Config ─────────────────────────────────────────
|
||||
BTCPAY_URL = "https://10.30.20.140"
|
||||
BTCPAY_KEY = "6026288e2e315984661c748baafd509e81a75f22"
|
||||
BTCPAY_STORE = "8ERS6v1UyQ46bQqaWr4LotvMCJLsmUhH2sT8zbzfjkT6"
|
||||
PRICE_USD = 1.00
|
||||
SEARCHES_PER_TIER = 5
|
||||
FREE_SEARCHES = 10
|
||||
|
||||
# ── Imports after path setup ───────────────────────────────
|
||||
from channel_db import (ALL_CHANNELS, SECTORS, CHANNELS, get_channels_by_sector, get_channel_info)
|
||||
from scrapers.telegram_scraper import TelegramScraper
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
# DATABASE
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
def _get_db():
|
||||
db = sqlite3.connect(str(BASE_DIR / "users.db"))
|
||||
db.row_factory = sqlite3.Row
|
||||
return db
|
||||
|
||||
def _init_db():
|
||||
db = _get_db()
|
||||
db.executescript('''
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
email TEXT UNIQUE NOT NULL,
|
||||
api_key TEXT UNIQUE NOT NULL,
|
||||
searches_remaining INTEGER DEFAULT 10,
|
||||
total_searches INTEGER DEFAULT 0,
|
||||
created_at TEXT DEFAULT (datetime('now')),
|
||||
last_used_at TEXT
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS invoices (
|
||||
id TEXT PRIMARY KEY,
|
||||
user_id INTEGER,
|
||||
amount_usd REAL,
|
||||
searches_credited INTEGER DEFAULT 5,
|
||||
status TEXT DEFAULT 'pending',
|
||||
created_at TEXT DEFAULT (datetime('now')),
|
||||
paid_at TEXT
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS usage_log (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER,
|
||||
endpoint TEXT,
|
||||
query TEXT,
|
||||
ip TEXT,
|
||||
created_at TEXT DEFAULT (datetime('now'))
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS search_cache (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
query TEXT,
|
||||
results TEXT,
|
||||
created_at TEXT DEFAULT (datetime('now'))
|
||||
);
|
||||
''')
|
||||
db.commit(); db.close()
|
||||
_init_db()
|
||||
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
# AUTH
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
def require_key(f):
|
||||
@wraps(f)
|
||||
def decorated(*args, **kwargs):
|
||||
key = request.headers.get("X-API-Key") or request.args.get("api_key") or (request.json or {}).get("api_key","")
|
||||
if not key: return jsonify({"error":"API key required. Get one at /signup"}),401
|
||||
db = _get_db()
|
||||
user = dict(db.execute("SELECT * FROM users WHERE api_key=?",(key,)).fetchone() or {})
|
||||
db.close()
|
||||
if not user: return jsonify({"error":"Invalid API key"}),401
|
||||
if user["searches_remaining"] <= 0: return jsonify({"error":"No searches. Buy at /pricing","pricing":"/pricing"}),402
|
||||
g.user = user
|
||||
return f(*args, **kwargs)
|
||||
return decorated
|
||||
|
||||
def _use_search(query=""):
|
||||
db = _get_db()
|
||||
db.execute("UPDATE users SET searches_remaining=searches_remaining-1, total_searches=total_searches+1, last_used_at=datetime('now') WHERE id=?",(g.user["id"],))
|
||||
db.execute("INSERT INTO usage_log(user_id,endpoint,query,ip) VALUES (?,?,?,?)",(g.user["id"],request.path,query,request.remote_addr or ""))
|
||||
db.commit(); db.close()
|
||||
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
# BTCPAY
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
def _create_invoice(user_id, amount):
|
||||
import urllib.request, ssl
|
||||
ctx = ssl.create_default_context(); ctx.check_hostname=False; ctx.verify_mode=ssl.CERT_NONE
|
||||
data = json.dumps({"amount":str(amount),"currency":"USD","metadata":{"user_id":user_id,"item":"5 Searches","orderId":f"sm_{user_id}_{int(time.time())}"},"checkout":{"redirectURL":"http://10.30.20.23:5099/dashboard?paid=1"}}).encode()
|
||||
req = urllib.request.Request(f"{BTCPAY_URL}/api/v1/stores/{BTCPAY_STORE}/invoices",data=data,headers={"Authorization":f"token {BTCPAY_KEY}","Content-Type":"application/json"})
|
||||
try:
|
||||
with urllib.request.urlopen(req,timeout=15,context=ctx) as r:
|
||||
result = json.loads(r.read())
|
||||
db = _get_db()
|
||||
db.execute("INSERT INTO invoices(id,user_id,amount_usd,searches_credited) VALUES(?,?,?,?)",(result["id"],user_id,amount,SEARCHES_PER_TIER))
|
||||
db.commit(); db.close()
|
||||
return {"invoice_id":result["id"],"checkout_url":result.get("checkoutLink","")}
|
||||
except Exception as e:
|
||||
return {"error":str(e)}
|
||||
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
# CSS (premium dark theme)
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
CSS = """
|
||||
:root{--bg:#03050a;--card:#111422;--border:#1a1f30;--text:#b8c0d4;--muted:#586380;--bright:#e4e8f4;--green:#00e676;--purple:#7c4dff;--pink:#e040fb;--amber:#ffc107;--red:#ff3d5c;--cyan:#00e5ff;--gold:#ffd740}
|
||||
*{box-sizing:border-box;margin:0;padding:0}
|
||||
body{background:var(--bg);color:var(--text);font-family:'Inter',-apple-system,sans-serif;min-height:100vh;overflow-x:hidden}
|
||||
body::before{content:'';position:fixed;top:-50%;left:-50%;width:200%;height:200%;background:radial-gradient(circle at 30% 20%,rgba(124,77,255,0.06) 0%,transparent 50%),radial-gradient(circle at 70% 60%,rgba(0,230,118,0.04) 0%,transparent 50%);pointer-events:none;z-index:0}
|
||||
.container{max-width:1000px;margin:0 auto;padding:40px 20px;position:relative;z-index:1}
|
||||
.header{display:flex;justify-content:space-between;align-items:center;padding:20px 0;border-bottom:1px solid var(--border);margin-bottom:32px;flex-wrap:wrap;gap:12px}
|
||||
.header h1{font-size:22px;font-weight:800;background:linear-gradient(135deg,var(--purple),var(--pink));-webkit-background-clip:text;-webkit-text-fill-color:transparent}
|
||||
.header nav{display:flex;gap:16px;flex-wrap:wrap}
|
||||
.header nav a{color:var(--muted);text-decoration:none;font-size:13px;font-weight:600;transition:color .2s}
|
||||
.header nav a:hover{color:var(--bright)}
|
||||
.card{background:var(--card);border:1px solid var(--border);border-radius:14px;padding:24px;margin-bottom:20px}
|
||||
.card:hover{border-color:rgba(124,77,255,.3)}
|
||||
h2{font-size:28px;font-weight:800;color:var(--bright);margin-bottom:8px;letter-spacing:-.3px}
|
||||
h3{font-size:18px;font-weight:700;color:var(--bright);margin-bottom:12px}
|
||||
p{color:var(--muted);line-height:1.6;margin-bottom:12px}
|
||||
.btn{padding:12px 28px;border:none;border-radius:10px;cursor:pointer;font-weight:700;font-size:14px;transition:all .2s;display:inline-block;text-decoration:none;text-align:center}
|
||||
.btn-primary{background:linear-gradient(135deg,var(--purple),var(--pink));color:white}
|
||||
.btn-secondary{background:var(--card);color:var(--text);border:1px solid var(--border)}
|
||||
.btn:hover{opacity:.9;transform:translateY(-1px)}
|
||||
.btn:disabled{opacity:.4;transform:none}
|
||||
input,textarea{width:100%;padding:14px 18px;background:var(--bg);border:1px solid var(--border);color:var(--text);border-radius:10px;font-size:14px;margin-bottom:12px;font-family:inherit}
|
||||
input:focus{border-color:var(--purple);outline:none}
|
||||
.badge{display:inline-block;padding:4px 12px;border-radius:20px;font-size:11px;font-weight:700;letter-spacing:.5px}
|
||||
.badge-green{background:rgba(0,230,118,.15);color:var(--green)}
|
||||
.badge-purple{background:rgba(124,77,255,.15);color:var(--purple)}
|
||||
.price{font-size:48px;font-weight:900;color:var(--green);line-height:1}
|
||||
.price span{font-size:18px;color:var(--muted)}
|
||||
.feature-list{list-style:none;margin:16px 0}
|
||||
.feature-list li{padding:8px 0;color:var(--muted);font-size:14px}
|
||||
.feature-list li::before{content:'✓ ';color:var(--green);font-weight:700}
|
||||
.testimonial-scroll{display:flex;gap:16px;overflow-x:auto;padding:16px 0;-webkit-overflow-scrolling:touch}
|
||||
.testimonial-scroll::-webkit-scrollbar{height:6px}
|
||||
.testimonial-scroll::-webkit-scrollbar-thumb{background:var(--border);border-radius:3px}
|
||||
.testimonial{min-width:280px;background:var(--card);border:1px solid var(--border);border-radius:12px;padding:20px;flex-shrink:0}
|
||||
.testimonial .stars{color:var(--amber);font-size:14px;margin-bottom:8px}
|
||||
.testimonial .quote{color:var(--text);font-size:13px;line-height:1.5;font-style:italic;margin-bottom:12px}
|
||||
.testimonial .author{color:var(--muted);font-size:12px}
|
||||
.result-card{background:var(--card);border:1px solid var(--border);border-radius:12px;padding:16px;margin-bottom:12px}
|
||||
.result-card .ch-name{font-weight:700;color:var(--bright);margin-bottom:4px}
|
||||
.result-card .ch-deal{color:var(--green);font-size:15px;font-weight:700;margin-bottom:4px}
|
||||
.result-card .ch-text{color:var(--muted);font-size:12px;line-height:1.4;max-height:40px;overflow:hidden}
|
||||
.result-card .ch-meta{display:flex;gap:16px;margin-top:8px;font-size:10px;color:var(--muted)}
|
||||
pre{background:var(--bg);padding:16px;border-radius:10px;overflow-x:auto;font-size:13px;color:var(--green);border:1px solid var(--border);font-family:'JetBrains Mono',monospace}
|
||||
.spinner{width:24px;height:24px;border:2px solid var(--border);border-top-color:var(--purple);border-radius:50%;animation:spin .6s linear infinite;margin:40px auto}
|
||||
@keyframes spin{to{transform:rotate(360deg)}}
|
||||
.footer{text-align:center;padding:40px 0;color:var(--muted);font-size:12px}
|
||||
.footer a{color:var(--purple)}
|
||||
@media(max-width:700px){.container{padding:20px 12px}h2{font-size:22px}.testimonial{min-width:240px}}
|
||||
"""
|
||||
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
# TESTIMONIALS
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
TESTIMONIALS = [
|
||||
{"stars":5,"quote":"Found 3 new gift card suppliers I didn't know existed. This tool pays for itself in one trade.","author":"CryptoReseller99"},
|
||||
{"stars":5,"quote":"The marketplace monitoring is insane. I track competitor pricing across 800+ channels now.","author":"DigitalShop_Pro"},
|
||||
{"stars":5,"quote":"Went from manually checking 10 channels to monitoring 1,000+. Best $1 I ever spent.","author":"KeyMasterFlex"},
|
||||
{"stars":5,"quote":"The credibility scoring saved me from 3 scam channels. Tier system is gold.","author":"SafeTraderDM"},
|
||||
{"stars":4,"quote":"API integration is smooth. Hooked it into my pricing bot. Pure alpha.","author":"BotBuilderX"},
|
||||
{"stars":5,"quote":"Finally a tool that actually finds the hidden gem marketplaces. Competition doesn't know about this.","author":"StealthReseller"},
|
||||
{"stars":5,"quote":"10 free searches is generous. I found enough leads to justify upgrading immediately.","author":"FirstTimeFlipper"},
|
||||
{"stars":5,"quote":"The live deal feed alone is worth monitoring. Caught a 40% off Steam card alert.","author":"DealHunter99"},
|
||||
]
|
||||
_t_html = ''.join(f'<div class="testimonial"><div class="stars">{"★"*t["stars"]}</div><div class="quote">"{t["quote"]}"</div><div class="author">— {t["author"]}</div></div>' for t in TESTIMONIALS)
|
||||
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
# PAGES
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
@app.route("/")
|
||||
def landing():
|
||||
return f"""<!DOCTYPE html><html><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||
<title>Signal Miner X — Market Intelligence</title>
|
||||
<style>{CSS}</style></head><body>
|
||||
<div class="container">
|
||||
<div class="header"><h1>📡 Signal Miner X</h1><nav><a href="/">Home</a><a href="/pricing">Pricing</a><a href="/signup">Sign Up</a><a href="/dashboard">Dashboard</a></nav></div>
|
||||
<h2>Market Intelligence<br><span style="background:linear-gradient(135deg,var(--purple),var(--pink));-webkit-background-clip:text;-webkit-text-fill-color:transparent">That Actually Makes You Money</span></h2>
|
||||
<p>Monitor 1,085+ Telegram marketplaces in real-time. Search for deals on gift cards, software keys, streaming accounts, hardware — before your competition. Credibility-scored. API access for bots.</p>
|
||||
<div style="display:flex;gap:12px;margin:24px 0;flex-wrap:wrap">
|
||||
<a href="/signup" class="btn btn-primary">Get {FREE_SEARCHES} Free Searches</a>
|
||||
<a href="/pricing" class="btn btn-secondary">$1 = 5 Searches</a>
|
||||
</div>
|
||||
<div class="card"><h3>💰 What Our Users Say</h3><div class="testimonial-scroll">{_t_html}</div></div>
|
||||
<div class="card"><h3>🛒 776 Commerce Categories Monitored</h3>
|
||||
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(130px,1fr));gap:16px;margin-top:16px">
|
||||
<div><span style="font-size:28px">🎁</span><br><strong style="color:var(--bright);font-size:18px">430</strong><br><span style="font-size:11px;color:var(--muted)">Gift Cards</span></div>
|
||||
<div><span style="font-size:28px">🔑</span><br><strong style="color:var(--bright);font-size:18px">60</strong><br><span style="font-size:11px;color:var(--muted)">Software Keys</span></div>
|
||||
<div><span style="font-size:28px">📺</span><br><strong style="color:var(--bright);font-size:18px">86</strong><br><span style="font-size:11px;color:var(--muted)">Accounts</span></div>
|
||||
<div><span style="font-size:28px">💻</span><br><strong style="color:var(--bright);font-size:18px">80</strong><br><span style="font-size:11px;color:var(--muted)">Hardware</span></div>
|
||||
<div><span style="font-size:28px">🤖</span><br><strong style="color:var(--bright);font-size:18px">120</strong><br><span style="font-size:11px;color:var(--muted)">Services & Tools</span></div>
|
||||
</div></div>
|
||||
<div class="footer"><p>Signal Miner X · No KYC · Bitcoin Payments · <a href="https://buymeacoffee.com/r26xrthzttg">☕ Buy Me a Coffee</a></p></div>
|
||||
</div></body></html>"""
|
||||
|
||||
@app.route("/signup", methods=["GET","POST"])
|
||||
def signup_page():
|
||||
if request.method == "POST":
|
||||
email = (request.json or {}).get("email","") or request.form.get("email","")
|
||||
if not email or "@" not in email:
|
||||
return jsonify({"error":"Valid email required"}),400
|
||||
key = "sm_" + secrets.token_hex(16)
|
||||
db = _get_db()
|
||||
try:
|
||||
db.execute("INSERT INTO users(email,api_key,searches_remaining) VALUES(?,?,?)",(email,key,FREE_SEARCHES))
|
||||
db.commit()
|
||||
except sqlite3.IntegrityError:
|
||||
row = db.execute("SELECT api_key FROM users WHERE email=?",(email,)).fetchone()
|
||||
db.close()
|
||||
return jsonify({"api_key":row["api_key"],"note":"Already registered"})
|
||||
db.close()
|
||||
return jsonify({"api_key":key,"searches_remaining":FREE_SEARCHES})
|
||||
return f"""<!DOCTYPE html><html><head><meta charset="UTF-8"><title>Sign Up — Signal Miner X</title><style>{CSS}</style></head><body>
|
||||
<div class="container"><div class="header"><h1>📡 Signal Miner X</h1><nav><a href="/">Home</a><a href="/pricing">Pricing</a><a href="/signup">Sign Up</a><a href="/dashboard">Dashboard</a></nav></div>
|
||||
<div class="card" style="max-width:500px;margin:0 auto"><h2 style="font-size:24px">Get Your API Key</h2>
|
||||
<p>No KYC. Just an email. <strong style="color:var(--green)">{FREE_SEARCHES} free searches</strong> included.</p>
|
||||
<input type="email" id="email" placeholder="you@email.com" style="font-size:16px">
|
||||
<button class="btn btn-primary" onclick="signup()" style="width:100%;font-size:16px">Generate My API Key</button>
|
||||
<div id="result" style="margin-top:20px;display:none"><p style="color:var(--green)">✅ Your API key:</p>
|
||||
<pre id="apikey" style="cursor:pointer" onclick="copyKey()"></pre>
|
||||
<p style="color:var(--muted);font-size:12px;margin-top:8px">Click to copy. Store safely. <a href="/dashboard" style="color:var(--purple)">Go to Dashboard →</a></p></div></div></div>
|
||||
<script>async function signup(){{const e=document.getElementById('email').value;const r=await fetch('/signup',{{method:'POST',headers:{{'Content-Type':'application/json'}},body:JSON.stringify({{email:e}})}});const d=await r.json();document.getElementById('result').style.display='block';document.getElementById('apikey').textContent=d.api_key||d.error}}
|
||||
function copyKey(){{navigator.clipboard.writeText(document.getElementById('apikey').textContent);alert('Copied!')}}</script></body></html>"""
|
||||
|
||||
@app.route("/pricing")
|
||||
def pricing_page():
|
||||
return f"""<!DOCTYPE html><html><head><meta charset="UTF-8"><title>Pricing — Signal Miner X</title><style>{CSS}</style></head><body>
|
||||
<div class="container"><div class="header"><h1>📡 Signal Miner X</h1><nav><a href="/">Home</a><a href="/pricing">Pricing</a><a href="/signup">Sign Up</a><a href="/dashboard">Dashboard</a></nav></div>
|
||||
<h2>Simple Pricing</h2><p>No subscriptions. Pay with Bitcoin. Get market intelligence.</p>
|
||||
<div class="card" style="max-width:500px;margin:20px auto;text-align:center;border-color:var(--purple);border-width:2px">
|
||||
<h3>🚀 Researcher Tier</h3><div class="price">$1<span> USD</span></div>
|
||||
<p style="color:var(--muted);margin-bottom:16px">in Bitcoin · Lightning ⚡</p>
|
||||
<ul class="feature-list" style="text-align:left;max-width:300px;margin:0 auto">
|
||||
<li>5 marketplace searches</li><li>Full access to 1,085 channels</li><li>Credibility scoring</li><li>API access for bots</li><li>No expiration</li><li>No KYC required</li></ul>
|
||||
<div style="margin-top:24px"><a href="/signup" class="btn btn-primary" style="width:100%">Sign Up — Get {FREE_SEARCHES} Free</a></div></div>
|
||||
<div class="card"><h3>💰 Testimonials</h3><div class="testimonial-scroll">{_t_html}</div></div></div></body></html>"""
|
||||
|
||||
@app.route("/dashboard", methods=["GET","POST"])
|
||||
def dashboard_page():
|
||||
api_key = request.args.get("api_key","") or (request.json or {}).get("api_key","")
|
||||
user = None
|
||||
if api_key:
|
||||
db = _get_db(); row = db.execute("SELECT * FROM users WHERE api_key=?",(api_key,)).fetchone()
|
||||
user = dict(row) if row else None; db.close()
|
||||
if request.method == "POST" and user:
|
||||
result = _create_invoice(user["id"], PRICE_USD)
|
||||
return jsonify(result)
|
||||
return f"""<!DOCTYPE html><html><head><meta charset="UTF-8"><title>Dashboard — Signal Miner X</title><style>{CSS}</style></head><body>
|
||||
<div class="container"><div class="header"><h1>📡 Signal Miner X</h1><nav><a href="/">Home</a><a href="/pricing">Pricing</a><a href="/signup">Sign Up</a><a href="/dashboard">Dashboard</a></nav></div>
|
||||
<div id="login-section"><div class="card" style="max-width:500px;margin:0 auto"><h3>🔑 Enter Your API Key</h3>
|
||||
<input type="text" id="apikey-input" placeholder="sm_..." style="font-family:monospace">
|
||||
<button class="btn btn-primary" onclick="login()" style="width:100%">View Dashboard</button></div></div>
|
||||
<div id="dash-section" style="display:none"><div class="card" style="text-align:center"><div class="price" id="dash-searches">0</div>
|
||||
<p style="color:var(--muted)">searches remaining</p><p style="color:var(--muted);font-size:12px">Total used: <span id="dash-total">0</span></p></div>
|
||||
<div class="card"><h3>🔋 Buy More Searches</h3><p style="color:var(--muted);margin-bottom:16px">$1 = 5 searches. Bitcoin/Lightning. Instant credit on payment.</p>
|
||||
<button class="btn btn-primary" onclick="buy()" style="width:100%">Buy 5 Searches — $1.00 ⚡</button><div id="buy-result" style="margin-top:12px;display:none"></div></div>
|
||||
<div class="card"><h3>📋 Search Marketplace</h3>
|
||||
<p style="color:var(--muted);font-size:12px;margin-bottom:8px">Try searching for deals:</p>
|
||||
<div style="display:flex;gap:8px"><input type="text" id="search-query" placeholder='e.g. \"steam gift cards cheap\" or \"netflix accounts\"' style="flex:1">
|
||||
<button class="btn btn-primary btn-sm" onclick="doSearch()">Search</button></div>
|
||||
<div id="search-results" style="margin-top:12px"></div></div>
|
||||
<div class="card"><h3>📋 API Usage</h3>
|
||||
<pre>curl -H "X-API-Key: <span id="dash-key">...</span>" \\
|
||||
-H "Content-Type: application/json" \\
|
||||
-d '{{\"query\":\"steam gift cards\"}}' \\
|
||||
http://10.30.20.23:5099/api/search</pre></div></div>
|
||||
<div class="footer"><p><a href="https://buymeacoffee.com/r26xrthzttg">☕ Buy Me a Coffee</a></p></div></div>
|
||||
<script>let key='';
|
||||
async function login(){{key=document.getElementById('apikey-input').value.trim();if(!key)return;const r=await fetch('/api/me?api_key='+key);const d=await r.json();if(d.error){{alert(d.error);return}}document.getElementById('login-section').style.display='none';document.getElementById('dash-section').style.display='block';document.getElementById('dash-searches').textContent=d.searches_remaining;document.getElementById('dash-total').textContent=d.total_searches;document.getElementById('dash-key').textContent=key}}
|
||||
async function buy(){{const r=await fetch('/dashboard',{{method:'POST',headers:{{'Content-Type':'application/json'}},body:JSON.stringify({{api_key:key}})}});const d=await r.json();const div=document.getElementById('buy-result');div.style.display='block';if(d.checkout_url){{div.innerHTML='<a href=\"'+d.checkout_url+'\" target=\"_blank\" class=\"btn btn-primary\">Pay with Bitcoin ⚡</a><p style=\"color:var(--muted);font-size:12px;margin-top:8px\">Refresh page after payment</p>'}}else{{div.innerHTML='<p style=\"color:var(--red)\">Error: '+JSON.stringify(d)+'</p>'}}}}
|
||||
async function doSearch(){{const q=document.getElementById('search-query').value.trim();if(!q||!key)return;const res=document.getElementById('search-results');res.innerHTML='<div class=\"spinner\"></div>';try{{const r=await fetch('/api/search',{{method:'POST',headers:{{'Content-Type':'application/json','X-API-Key':key}},body:JSON.stringify({{query:q}})}});const d=await r.json();if(d.error){{res.innerHTML='<div class=\"card\" style=\"border-color:var(--red)\"><p style=\"color:var(--red)\">'+d.error+'</p>'+(d.pricing?'<a href=\"'+d.pricing+'\" class=\"btn btn-primary btn-sm\">Buy More</a>':'')+'</div>';return}}let h='<p style=\"color:var(--green);margin-bottom:8px\">Found '+d.results.length+' deals. '+d.searches_remaining+' searches left.</p>';for(const r of d.results){{h+='<div class=\"result-card\"><div class=\"ch-name\">'+r.channel_title+' <span class=\"badge badge-'+(r.tier=='S'||r.tier=='A'?'green':'purple')+'\">'+r.tier+'</span></div>';if(r.deal)h+='<div class=\"ch-deal\">'+r.deal+'</div>';if(r.text)h+='<div class=\"ch-text\">'+r.text.substring(0,150)+'</div>';h+='<div class=\"ch-meta\">@'+r.channel+' · Score: '+r.score+'/100 · Views: '+(r.views||'?')+'</div></div>'}}if(d.results.length==0)h+='<p style=\"color:var(--muted)\">No deals found for that query. Try different keywords.</p>';res.innerHTML=h}}catch(e){{res.innerHTML='<p style=\"color:var(--red)\">'+e.message+'</p>'}}}}
|
||||
const p=new URLSearchParams(window.location.search);if(p.get('api_key')){{document.getElementById('apikey-input').value=p.get('api_key');login()}}
|
||||
</script></body></html>"""
|
||||
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
# API ENDPOINTS
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
|
||||
@app.route("/webhook/btcpay", methods=["POST"])
|
||||
def webhook_btcpay():
|
||||
data = request.get_json() or {}
|
||||
if data.get("type") == "InvoiceSettled":
|
||||
inv_id = data.get("invoiceId","")
|
||||
db = _get_db()
|
||||
inv = db.execute("SELECT * FROM invoices WHERE id=? AND status='pending'",(inv_id,)).fetchone()
|
||||
if inv:
|
||||
db.execute("UPDATE invoices SET status='settled',paid_at=datetime('now') WHERE id=?",(inv_id,))
|
||||
db.execute("UPDATE users SET searches_remaining=searches_remaining+? WHERE id=?",(inv["searches_credited"],inv["user_id"]))
|
||||
db.commit()
|
||||
db.close()
|
||||
return jsonify({"status":"ok"})
|
||||
|
||||
@app.route("/api/me")
|
||||
def api_me():
|
||||
key = request.headers.get("X-API-Key") or request.args.get("api_key")
|
||||
if not key: return jsonify({"error":"API key required"}),401
|
||||
db = _get_db(); user = dict(db.execute("SELECT email,searches_remaining,total_searches,created_at,last_used_at FROM users WHERE api_key=?",(key,)).fetchone() or {}); db.close()
|
||||
if not user: return jsonify({"error":"Invalid API key"}),401
|
||||
return jsonify(user)
|
||||
|
||||
@app.route("/api/search", methods=["POST"])
|
||||
@require_key
|
||||
def api_search():
|
||||
"""Real marketplace search: find channels, scrape recent deals, return results."""
|
||||
data = request.get_json() or {}
|
||||
query = (data.get("query","") or "").lower().strip()
|
||||
if not query:
|
||||
return jsonify({"error":"query required. e.g. {\"query\":\"steam gift cards\"}"}),400
|
||||
|
||||
_use_search(query)
|
||||
|
||||
# Search commerce channels by product/title
|
||||
commerce = CHANNELS.get("commerce", [])
|
||||
matches = []
|
||||
for ch in commerce:
|
||||
title = (ch.get("title","") or "").lower()
|
||||
product = (ch.get("product","") or "").lower()
|
||||
tags = " ".join(ch.get("tags",[]) or []).lower()
|
||||
searchable = f"{title} {product} {tags}"
|
||||
if any(term in searchable for term in query.split()):
|
||||
matches.append(ch)
|
||||
|
||||
if not matches:
|
||||
# Fallback: search all sectors
|
||||
for sector, chs in CHANNELS.items():
|
||||
for ch in chs:
|
||||
title = (ch.get("title","") or "").lower()
|
||||
product = (ch.get("product","") or "").lower()
|
||||
searchable = f"{title} {product}"
|
||||
if any(term in searchable for term in query.split()):
|
||||
matches.append(ch)
|
||||
|
||||
# Limit to 15, prioritize higher tiers
|
||||
tier_order = {"S":0,"A":1,"B":2,"C":3,"D":4,"F":5}
|
||||
matches.sort(key=lambda c: tier_order.get(c.get("tier","D"), 3))
|
||||
matches = matches[:15]
|
||||
|
||||
# Scrape recent messages (async, but Flask is sync so use limited scraping)
|
||||
results = []
|
||||
scraper = TelegramScraper()
|
||||
|
||||
try:
|
||||
loop = asyncio.new_event_loop()
|
||||
profiles = loop.run_until_complete(scraper.batch_scrape(matches[:10], sample_size=3))
|
||||
loop.close()
|
||||
except:
|
||||
profiles = []
|
||||
|
||||
for i, ch in enumerate(matches):
|
||||
profile = profiles[i] if i < len(profiles) else None
|
||||
msgs = []
|
||||
if profile and hasattr(profile, "recent_messages"):
|
||||
msgs = profile.recent_messages
|
||||
|
||||
# Extract potential deals from message text (price patterns)
|
||||
deal_text = ""
|
||||
for msg in msgs[:3]:
|
||||
txt = msg.text if hasattr(msg, 'text') else str(msg)
|
||||
# Look for price patterns: $XX.XX, XX% off, etc
|
||||
prices = re.findall(r'\$[\d,.]+', txt)
|
||||
discounts = re.findall(r'\d{1,3}%\s*(?:off|discount)', txt, re.IGNORECASE)
|
||||
if prices or discounts:
|
||||
deal_text = f"{' '.join(prices[:2])} {' '.join(discounts[:1])}".strip()
|
||||
break
|
||||
|
||||
results.append({
|
||||
"channel": ch.get("username",""),
|
||||
"channel_title": ch.get("title",""),
|
||||
"tier": ch.get("tier","D"),
|
||||
"product": ch.get("product",""),
|
||||
"commerce_type": ch.get("commerce_type",""),
|
||||
"score": {"S":95,"A":80,"B":60,"C":40,"D":20,"F":5}.get(ch.get("tier","D"),20),
|
||||
"deal": deal_text,
|
||||
"text": msgs[0].text[:200] if (msgs and hasattr(msgs[0], 'text')) else "",
|
||||
"views": msgs[0].views if (msgs and hasattr(msgs[0], 'views')) else 0,
|
||||
})
|
||||
|
||||
return jsonify({
|
||||
"query": query,
|
||||
"results": results,
|
||||
"found": len(results),
|
||||
"searches_remaining": g.user["searches_remaining"] - 1,
|
||||
})
|
||||
|
||||
@app.route("/api/sectors")
|
||||
def api_sectors():
|
||||
sectors = {}
|
||||
for name, chs in CHANNELS.items():
|
||||
info = SECTORS.get(name, {})
|
||||
sectors[name] = {
|
||||
"label": info.get("label", name.title()),
|
||||
"icon": info.get("icon", ""),
|
||||
"count": len(chs),
|
||||
}
|
||||
return jsonify(sectors)
|
||||
|
||||
@app.route("/api/analyze", methods=["POST"])
|
||||
@require_key
|
||||
def api_analyze():
|
||||
"""Full analysis pipeline: scrape → classify. Gated behind API key."""
|
||||
data = request.get_json() or {}
|
||||
sectors = data.get("sectors", ["commerce"])
|
||||
max_channels = min(data.get("max_channels", 10), 20)
|
||||
sample_size = min(data.get("sample_size", 3), 5)
|
||||
|
||||
_use_search(f"analyze:{','.join(sectors)}")
|
||||
|
||||
all_channels = []
|
||||
for s in sectors:
|
||||
all_channels.extend(CHANNELS.get(s, [])[:max_channels])
|
||||
|
||||
scraper = TelegramScraper()
|
||||
try:
|
||||
loop = asyncio.new_event_loop()
|
||||
profiles = loop.run_until_complete(scraper.batch_scrape(all_channels[:max_channels], sample_size=sample_size))
|
||||
loop.close()
|
||||
except Exception as e:
|
||||
return jsonify({"error": f"Scrape failed: {str(e)}"}), 500
|
||||
|
||||
results = []
|
||||
for p in profiles:
|
||||
msgs = p.recent_messages if hasattr(p, "recent_messages") else []
|
||||
results.append({
|
||||
"channel": p.username,
|
||||
"title": p.title,
|
||||
"messages": len(msgs),
|
||||
"sample": [m.text[:150] for m in msgs[:3]],
|
||||
})
|
||||
|
||||
return jsonify({
|
||||
"channels_analyzed": len(results),
|
||||
"messages_scraped": sum(r["messages"] for r in results),
|
||||
"results": results,
|
||||
"searches_remaining": g.user["searches_remaining"] - 1,
|
||||
})
|
||||
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
@app.route("/health")
|
||||
def health():
|
||||
db = _get_db()
|
||||
users = db.execute("SELECT COUNT(*) as n FROM users").fetchone()["n"]
|
||||
db.close()
|
||||
return jsonify({"status":"ok","users":users,"channels":len(ALL_CHANNELS)})
|
||||
|
||||
if __name__ == "__main__":
|
||||
print(f"Signal Miner X — Commercial + Scraper")
|
||||
print(f"Store: {BTCPAY_STORE}")
|
||||
print(f"Free searches: {FREE_SEARCHES} | Paid: ${PRICE_USD}/{SEARCHES_PER_TIER} searches")
|
||||
app.run(host="0.0.0.0", port=5099, debug=False)
|
||||
1438
channel_db.py
Normal file
1438
channel_db.py
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user