emltechstudio/Quote
0
1from fastapi import FastAPI, Request, HTTPException2from fastapi.middleware.cors import CORSMiddleware3from fastapi.responses import HTMLResponse4from pydantic import BaseModel5import httpx6import os7import time8import hmac9import hashlib10import base6411import secrets12from collections import defaultdict13from datetime import datetime, timedelta14 15app = FastAPI()16 17app.add_middleware(18 CORSMiddleware,19 allow_origins=["*"],20 allow_methods=["*"],21 allow_headers=["*"],22)23 24# ── CONFIG ────────────────────────────────────────────────────25GROQ_API_KEY = os.environ.get("GROQ_API_KEY")26KEY_SECRET = os.environ.get("KEY_SECRET", "change_this_in_hf_secrets")27ADMIN_SECRET = os.environ.get("ADMIN_SECRET", "change_this_in_hf_secrets")28GROQ_URL = "https://api.groq.com/openai/v1/chat/completions"29SELAR_PRO_LINK = "https://selar.com/271q02q156"30SELAR_PREMIUM_LINK = "https://selar.com/077jw01q60"31APP_URL = "https://quotes.afric.site/app.html"32 33ALLOWED_REFERRERS = ["selar.com", "emltechstudio.site", "quotes.afric.site"]34SNAP_LIMITS = {"free": 1, "pro": 2, "premium": 5}35 36# ── STORES ────────────────────────────────────────────────────37rate_store = defaultdict(float)38snap_store = defaultdict(lambda: {"count": 0, "date": ""})39analytics_log = []40admin_keys = [] # tracks keys generated via admin panel41 42# ── HELPERS ───────────────────────────────────────────────────43def get_ip(r: Request) -> str:44 fwd = r.headers.get("X-Forwarded-For")45 return fwd.split(",")[0].strip() if fwd else r.client.host46 47def check_rate(ip: str):48 now = time.time()49 if now - rate_store[ip] < 30:50 raise HTTPException(429, f"Wait {int(30-(now-rate_store[ip]))}s.")51 rate_store[ip] = now52 53def check_referrer(r: Request):54 ref = r.headers.get("referer","") + r.headers.get("origin","")55 if not any(d in ref for d in ALLOWED_REFERRERS):56 raise HTTPException(403, "Access denied.")57 58def sign_key(payload: str) -> str:59 return hmac.new(60 KEY_SECRET.encode(), payload.encode(), hashlib.sha25661 ).hexdigest()[:24]62 63NONCES_FILE = "/data/used_nonces.txt"64 65def load_used_nonces() -> set:66 try:67 with open(NONCES_FILE, "r") as f:68 return set(line.strip() for line in f if line.strip())69 except FileNotFoundError:70 return set()71 72def save_nonce(nonce: str):73 try:74 os.makedirs("/data", exist_ok=True)75 with open(NONCES_FILE, "a") as f:76 f.write(nonce + "\n")77 except Exception:78 pass79 80def make_key(tier: str, days: int, single_use: bool = False) -> str:81 expiry = (datetime.utcnow() + timedelta(days=days)).strftime("%Y-%m-%d")82 nonce = secrets.token_hex(6)83 su_flag = "1" if single_use else "0"84 payload = f"{tier}|{expiry}|{nonce}|{su_flag}"85 sig = sign_key(payload)86 raw = f"{payload}|{sig}"87 return base64.urlsafe_b64encode(raw.encode()).decode().rstrip("=")88 89def parse_key(key: str, consume: bool = False):90 try:91 pad = key + "=" * (-len(key) % 4)92 raw = base64.urlsafe_b64decode(pad.encode()).decode()93 parts = raw.split("|")94 if len(parts) != 5:95 raise ValueError96 tier, expiry, nonce, su_flag, sig = parts97 payload = f"{tier}|{expiry}|{nonce}|{su_flag}"98 expected = sign_key(payload)99 if not hmac.compare_digest(sig, expected):100 raise HTTPException(400, "Invalid key.")101 if datetime.utcnow() > datetime.strptime(expiry, "%Y-%m-%d"):102 raise HTTPException(400, "Key has expired. Please renew your plan.")103 if tier not in ("pro", "premium"):104 raise ValueError105 if su_flag == "1":106 used = load_used_nonces()107 if nonce in used:108 raise HTTPException(400, "This activation link has already been used.")109 if consume:110 save_nonce(nonce)111 return tier, expiry112 except HTTPException:113 raise114 except Exception:115 raise HTTPException(400, "Invalid activation key.")116 117# ── ACTIVATION PAGE ───────────────────────────────────────────118def activation_html(tier: str, key: str) -> str:119 label = "Pro" if tier == "pro" else "Premium"120 color = "#2563eb" if tier == "pro" else "#ea580c"121 r,g,b = int(color[1:3],16), int(color[3:5],16), int(color[5:7],16)122 deep = f"{APP_URL}?key={key}"123 return f"""<!DOCTYPE html>124<html lang="en">125<head>126<meta charset="UTF-8">127<meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover">128<meta name="theme-color" content="#0a0a0a">129<title>Activate Quotes {label}</title>130<link href="https://fonts.googleapis.com/css2?family=Syne:wght@400;700;800&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">131<style>132*{{margin:0;padding:0;box-sizing:border-box}}133:root{{--bg:#0a0a0a;--s1:#111;--s2:#1a1a1a;--bd:#222;--tx:#f2f2ef;--t2:#888;--t3:#444;134 --acc:{color};--fn:'Syne',sans-serif;--mn:'JetBrains Mono',monospace}}135body{{background:var(--bg);color:var(--tx);font-family:var(--fn);min-height:100svh;136 display:flex;flex-direction:column;align-items:center;justify-content:center;137 padding:24px;-webkit-font-smoothing:antialiased}}138.card{{background:var(--s1);border:1px solid var(--bd);border-radius:20px;139 padding:32px 28px;max-width:420px;width:100%;display:flex;flex-direction:column;gap:20px}}140.logo{{display:flex;align-items:center;gap:10px}}141.logo-icon{{width:36px;height:36px;border-radius:9px;background:var(--s2);border:1px solid var(--bd);142 display:flex;align-items:center;justify-content:center}}143.logo-name{{font-size:.95rem;font-weight:800}}144.logo-name span{{color:var(--acc)}}145.badge{{display:inline-flex;align-items:center;gap:6px;146 background:rgba({r},{g},{b},0.12);color:var(--acc);147 border:1px solid rgba({r},{g},{b},0.25);148 border-radius:20px;padding:5px 12px;font-family:var(--mn);font-size:.68rem;font-weight:600;width:fit-content}}149h1{{font-size:1.5rem;font-weight:800;letter-spacing:-.03em;line-height:1.2}}150.warn-box{{background:rgba(234,88,12,0.08);border:1px solid rgba(234,88,12,0.2);151 border-radius:10px;padding:12px 14px;display:flex;align-items:flex-start;gap:10px}}152.warn-box svg{{flex-shrink:0;margin-top:1px}}153.warn-text{{font-family:var(--mn);font-size:.68rem;color:#fb923c;line-height:1.7}}154.warn-text strong{{color:#fed7aa}}155.timer-box{{text-align:center}}156.timer-label{{font-family:var(--mn);font-size:.6rem;color:var(--t3);157 text-transform:uppercase;letter-spacing:.8px;margin-bottom:4px}}158.timer{{font-family:var(--mn);font-size:1.8rem;font-weight:700;color:var(--acc)}}159.timer.urgent{{color:#e03c3c}}160.btn-activate{{width:100%;padding:16px;background:var(--acc);border:none;border-radius:12px;161 color:#fff;font-family:var(--fn);font-size:1.1rem;font-weight:800;cursor:pointer;162 transition:all .15s;display:flex;align-items:center;justify-content:center;gap:10px}}163.btn-activate:hover{{opacity:.88;transform:translateY(-1px)}}164.btn-activate:active{{transform:scale(.97)}}165.btn-activate:disabled{{opacity:.4;cursor:not-allowed;transform:none}}166.steps{{display:flex;flex-direction:column;gap:10px}}167.step{{display:flex;align-items:flex-start;gap:10px}}168.sn{{width:20px;height:20px;border-radius:50%;background:rgba({r},{g},{b},0.12);169 color:var(--acc);font-family:var(--mn);font-size:.6rem;font-weight:700;170 display:flex;align-items:center;justify-content:center;flex-shrink:0;margin-top:2px}}171.st{{font-family:var(--mn);font-size:.7rem;color:var(--t2);line-height:1.6}}172.st strong{{color:var(--tx)}}173.expired-box{{display:none;background:#1a1a1a;border:1px solid #333;border-radius:12px;174 padding:20px;text-align:center}}175.expired-box h3{{font-size:1rem;font-weight:800;color:#e03c3c;margin-bottom:8px}}176.expired-box p{{font-family:var(--mn);font-size:.72rem;color:var(--t2);line-height:1.7}}177footer{{margin-top:16px;font-family:var(--mn);font-size:.6rem;color:var(--t3);text-align:center}}178</style>179</head>180<body>181<div class="card">182 <div class="logo">183 <div class="logo-icon">184 <svg width="20" height="20" viewBox="0 0 24 24" fill="none">185 <circle cx="10" cy="10" r="6" stroke="{color}" stroke-width="2"/>186 <path d="M13 7L10 11H12L9 15" stroke="#2563eb" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/>187 <line x1="15" y1="15" x2="19" y2="19" stroke="{color}" stroke-width="2" stroke-linecap="round"/>188 </svg>189 </div>190 <div class="logo-name">Quo<span>tes</span></div>191 </div>192 193 <div class="badge">194 <svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg>195 {label} Plan — Payment Confirmed196 </div>197 198 <h1>One tap to activate.</h1>199 200 <div class="warn-box">201 <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#fb923c" stroke-width="2" stroke-linecap="round"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>202 <div class="warn-text"><strong>Do not close this page</strong> before tapping Activate Now. This link works once only — if you leave without activating, you will need to contact support.</div>203 </div>204 205 <div class="timer-box" id="timer-box">206 <div class="timer-label">This link expires in</div>207 <div class="timer" id="timer">05:00</div>208 </div>209 210 <div id="activate-section">211 <button class="btn-activate" id="activate-btn" onclick="doActivate()">212 <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><polyline points="20 6 9 17 4 12"/></svg>213 Activate Now — Open App214 </button>215 </div>216 217 <div class="expired-box" id="expired-box">218 <h3>This link has expired</h3>219 <p>You did not activate within the time limit.<br>Please contact us to get a new activation link.<br><br>220 <a href="mailto:emltechstudio@gmail.com" style="color:{color}">emltechstudio@gmail.com</a></p>221 </div>222 223 <div class="steps">224 <div class="step"><div class="sn">1</div><div class="st">Tap <strong>Activate Now</strong> — opens the Quotes app automatically</div></div>225 <div class="step"><div class="sn">2</div><div class="st">Your <strong>{label} plan</strong> activates instantly — no further steps needed</div></div>226 <div class="step"><div class="sn">3</div><div class="st">If the app does not open, make sure Quotes is installed on your device first</div></div>227 </div>228</div>229<footer>Quotes by EML Tech Studio · emltechstudio@gmail.com</footer>230<script>231let activated = false;232const TIMEOUT = 5 * 60; // 5 minutes233let remaining = TIMEOUT;234 235function pad(n){{ return String(n).padStart(2,'0') }}236 237const timerEl = document.getElementById('timer');238const interval = setInterval(()=>{{239 remaining--;240 const m = Math.floor(remaining/60);241 const s = remaining%60;242 timerEl.textContent = pad(m)+':'+pad(s);243 if(remaining <= 60) timerEl.classList.add('urgent');244 if(remaining <= 0){{245 clearInterval(interval);246 document.getElementById('activate-btn').disabled = true;247 document.getElementById('activate-section').style.display = 'none';248 document.getElementById('expired-box').style.display = 'block';249 document.getElementById('timer-box').style.display = 'none';250 }}251}}, 1000);252 253function doActivate(){{254 activated = true;255 clearInterval(interval);256 document.getElementById('timer-box').style.display = 'none';257 document.getElementById('activate-btn').disabled = true;258 document.getElementById('activate-btn').innerHTML = '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><polyline points="20 6 9 17 4 12"/></svg> Activating...';259 window.location.href = '{deep}';260}}261 262window.addEventListener('beforeunload', e=>{{263 if(!activated){{264 e.preventDefault();265 e.returnValue = 'Do not leave this page without tapping Activate Now — your access will be lost.';266 return e.returnValue;267 }}268}});269</script>270</body>271</html>"""272 273 274# ── ADMIN PAGE ────────────────────────────────────────────────275ADMIN_HTML = """<!DOCTYPE html>276<html lang="en">277<head>278<meta charset="UTF-8">279<meta name="viewport" content="width=device-width,initial-scale=1">280<title>Quotes Admin</title>281<link href="https://fonts.googleapis.com/css2?family=Syne:wght@700;800&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">282<style>283*{margin:0;padding:0;box-sizing:border-box}284body{background:#0a0a0a;color:#f2f2ef;font-family:'Syne',sans-serif;min-height:100svh;display:flex;align-items:center;justify-content:center;padding:24px}285.card{background:#111;border:1px solid #222;border-radius:20px;padding:32px;max-width:480px;width:100%;display:flex;flex-direction:column;gap:18px}286h1{font-size:1.4rem;font-weight:800;letter-spacing:-.03em}287.fi{display:flex;flex-direction:column;gap:6px}288label{font-family:'JetBrains Mono',monospace;font-size:.62rem;color:#555;text-transform:uppercase;letter-spacing:.8px}289input,select{background:#1a1a1a;border:1.5px solid #222;border-radius:8px;color:#f2f2ef;font-family:'JetBrains Mono',monospace;font-size:.9rem;padding:11px 13px;outline:none;transition:border-color .15s;-webkit-appearance:none}290input:focus,select:focus{border-color:#ea580c}291select option{background:#1a1a1a}292.btn{width:100%;padding:13px;background:#ea580c;border:none;border-radius:10px;color:#fff;font-family:'Syne',sans-serif;font-size:1rem;font-weight:800;cursor:pointer;transition:opacity .15s}293.btn:hover{opacity:.88}294.result{background:#1a1a1a;border:1.5px solid #222;border-radius:10px;padding:14px;font-family:'JetBrains Mono',monospace;font-size:.75rem;color:#ea580c;word-break:break-all;line-height:1.7;display:none}295.stats{background:#1a1a1a;border:1.5px solid #222;border-radius:10px;padding:14px;font-family:'JetBrains Mono',monospace;font-size:.72rem;color:#888;line-height:1.8;display:none}296.row{display:grid;grid-template-columns:1fr 1fr;gap:12px}297</style>298</head>299<body>300<div class="card">301 <h1>Quotes Admin</h1>302 <div class="fi"><label>Admin Secret</label><input type="password" id="secret" placeholder="Enter admin secret"></div>303 <div class="row">304 <div class="fi"><label>Tier</label>305 <select id="tier"><option value="pro">Pro</option><option value="premium">Premium</option></select>306 </div>307 <div class="fi"><label>Duration (days)</label><input type="number" id="days" value="31" min="1" max="365"></div>308 </div>309 <button class="btn" onclick="genKey()">Generate Activation Key</button>310 <div class="result" id="result"></div>311 <button class="btn" style="background:#222;color:#888;margin-top:4px" onclick="getStats()">View Analytics</button>312 <div class="stats" id="stats"></div>313</div>314<script>315const BASE = '';316async function genKey(){317 const secret=document.getElementById('secret').value;318 const tier=document.getElementById('tier').value;319 const days=document.getElementById('days').value;320 const r=await fetch(BASE+'/admin/generate-key',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({secret,tier,days:parseInt(days)})});321 const d=await r.json();322 const el=document.getElementById('result');323 el.style.display='block';324 el.textContent=d.key||d.detail||'Error';325}326async function getStats(){327 const secret=document.getElementById('secret').value;328 const r=await fetch(BASE+'/analytics?secret='+secret);329 const d=await r.json();330 const el=document.getElementById('stats');331 el.style.display='block';332 el.textContent=JSON.stringify(d,null,2);333}334</script>335</body>336</html>"""337 338# ── ROUTES ────────────────────────────────────────────────────339 340@app.get("/activate/pro", response_class=HTMLResponse)341async def activate_pro(request: Request):342 check_referrer(request)343 key = make_key("pro", 31, single_use=True)344 return activation_html("pro", key)345 346@app.get("/activate/premium", response_class=HTMLResponse)347async def activate_premium(request: Request):348 check_referrer(request)349 key = make_key("premium", 31, single_use=True)350 return activation_html("premium", key)351 352@app.post("/verify-key")353async def verify_key_endpoint(body: dict):354 key = body.get("key","")355 consume = body.get("consume", False)356 if not key:357 raise HTTPException(400, "No key provided.")358 tier, expiry = parse_key(key, consume=consume)359 return {"valid": True, "tier": tier, "expiry": expiry,360 "label": "Pro" if tier == "pro" else "Premium"}361 362@app.get("/pricing")363async def pricing():364 return {"pro": SELAR_PRO_LINK, "premium": SELAR_PREMIUM_LINK}365 366@app.get("/admin", response_class=HTMLResponse)367async def admin_page():368 return ADMIN_HTML369 370@app.post("/admin/generate-key")371async def admin_generate(body: dict):372 if body.get("secret") != ADMIN_SECRET:373 raise HTTPException(403, "Wrong admin secret.")374 tier = body.get("tier","pro")375 days = int(body.get("days", 31))376 if tier not in ("pro","premium"):377 raise HTTPException(400, "tier must be pro or premium.")378 if not (1 <= days <= 3650):379 raise HTTPException(400, "days must be 1–3650.")380 key = make_key(tier, days)381 admin_keys.append({"tier":tier,"days":days,"ts":datetime.utcnow().isoformat()})382 return {"key": key, "tier": tier, "expires_in_days": days}383 384class SnapRequest(BaseModel):385 system_prompt: str386 user_message: str387 image_base64: str = ""388 mime_type: str = "image/jpeg"389 doc_type: str = "general"390 tier: str = "free"391 392@app.post("/generate")393async def generate(request: Request, body: SnapRequest):394 ip = get_ip(request)395 check_rate(ip)396 today = datetime.utcnow().strftime("%Y-%m-%d")397 store = snap_store[ip]398 if store["date"] != today:399 store["count"] = 0400 store["date"] = today401 limit = SNAP_LIMITS.get(body.tier, 1)402 if store["count"] >= limit:403 raise HTTPException(429, f"Daily AI Snap limit reached ({limit}/day on {body.tier} plan).")404 store["count"] += 1405 analytics_log.append({"ip":ip,"ts":datetime.utcnow().isoformat(),"type":body.doc_type,"tier":body.tier})406 407 user_content = []408 if body.image_base64:409 user_content.append({"type":"image_url","image_url":{"url":f"data:{body.mime_type};base64,{body.image_base64}"}})410 user_content.append({"type":"text","text":body.user_message})411 412 async with httpx.AsyncClient(timeout=30) as client:413 resp = await client.post(GROQ_URL,414 json={"model":"meta-llama/llama-4-scout-17b-16e-instruct",415 "messages":[{"role":"system","content":body.system_prompt},416 {"role":"user","content":user_content}],417 "max_tokens":1024,"temperature":0.2},418 headers={"Authorization":f"Bearer {GROQ_API_KEY}","Content-Type":"application/json"})419 if resp.status_code != 200:420 raise HTTPException(resp.status_code, resp.text)421 return {"result": resp.json()["choices"][0]["message"]["content"]}422 423@app.get("/analytics")424async def analytics(secret: str = ""):425 if secret != ADMIN_SECRET:426 raise HTTPException(403, "Forbidden")427 return {"total":len(analytics_log),428 "unique_ips":len(set(e["ip"] for e in analytics_log)),429 "admin_keys_issued":len(admin_keys),430 "recent":analytics_log[-50:]}431 432@app.get("/")433async def health():434 return {"status":"live","app":"Quotes API","version":"2.0"}435 436# ================= RUN =================437if __name__ == "__main__":438 port = int(os.getenv("PORT", 7860))439 uvicorn.run(app, host="0.0.0.0", port=port)