Add temp-SMS tool (SMSPool-backed temp-number marketplace) to Pleiades
- sms.py: NumberProvider abstraction + SMSPoolProvider + routes (/sms, rent, poll, release) + admin (/admin/sms) - Reuses existing sats wallet + BTCPay deposit flow; charges per-rental against balance_sats - Code detection, lazy expiry/release, per-user session limit, per-IP rate limit - Verified end-to-end: rented Discord US +1 534 251 1825, wallet 5000->3500 sats, release refunded SMSPool to $7.45
This commit is contained in:
551
sms.py
Normal file
551
sms.py
Normal file
@@ -0,0 +1,551 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
sms.py — Temporary inbound-SMS tool for Pleiades (Clean Proxys).
|
||||
|
||||
A NumberProvider abstraction over SMSPool, plus Flask routes, pricing, the
|
||||
number lifecycle, code detection, and realtime polling for the temp-number
|
||||
marketplace. Reuses the existing wallet (balance_sats + transactions ledger)
|
||||
and BTCPay deposit flow — customers top up BTC once, then rent numbers on
|
||||
demand against their wallet balance.
|
||||
|
||||
Provider facts (verified live 2026-09-20):
|
||||
GET /request/balance -> {"balance":"7.45"}
|
||||
GET /country/retrieve_all -> [{ID,name,short_name,cc,region}]
|
||||
GET /service/retrieve_all -> [{ID,name,favourite}]
|
||||
GET /pool/retrieve_all -> [{ID,name}]
|
||||
GET /purchase/sms?service=&country= -> {success,number,phonenumber,order_id,
|
||||
country,service,pool,expires_in,
|
||||
expiration,cost,cost_in_cents,
|
||||
current_balance}
|
||||
GET /sms/check?orderid= -> {status,message,resend,expiration,time_left}
|
||||
GET /sms/cancel?orderid= -> {success,message} (refunds unused rental)
|
||||
GET /request/active -> [] (active orders)
|
||||
|
||||
SMSPool /sms/check status codes: 1=SMS received, 2/3=waiting/pending,
|
||||
4=cancelled/expired, 5/6=refunded. Rental cost is charged at purchase and
|
||||
refunded on /sms/cancel if no SMS was received -> float exposure is only
|
||||
`concurrent active sessions * max rental cost`.
|
||||
"""
|
||||
import re
|
||||
import time
|
||||
|
||||
import requests
|
||||
from flask import (abort, flash, jsonify, redirect, render_template, request,
|
||||
session, url_for)
|
||||
|
||||
import db
|
||||
|
||||
CFG = db.CFG
|
||||
|
||||
SMSPOOL_KEY = (CFG.get("smspool") or {}).get("key", "")
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# Provider abstraction
|
||||
# --------------------------------------------------------------------------
|
||||
class NumberProvider:
|
||||
"""Swap the upstream by implementing this interface and changing PROVIDER."""
|
||||
def list_countries(self): raise NotImplementedError
|
||||
def list_services(self): raise NotImplementedError
|
||||
def provision_number(self, service, country, expiry_min): raise NotImplementedError
|
||||
def check_sms(self, order_id): raise NotImplementedError
|
||||
def release_number(self, order_id): raise NotImplementedError
|
||||
def get_balance(self): raise NotImplementedError
|
||||
|
||||
|
||||
class SMSPoolProvider(NumberProvider):
|
||||
BASE = "https://api.smspool.net"
|
||||
|
||||
def __init__(self, key):
|
||||
self.key = key
|
||||
|
||||
def _get(self, path, params):
|
||||
p = dict(params)
|
||||
p["key"] = self.key
|
||||
r = requests.get(f"{self.BASE}/{path}", params=p, timeout=30)
|
||||
ct = r.headers.get("content-type", "")
|
||||
if "json" in ct:
|
||||
try:
|
||||
return r.json()
|
||||
except ValueError:
|
||||
return {"success": 0, "message": r.text[:300]}
|
||||
return {"success": 0, "message": r.text[:300]}
|
||||
|
||||
def list_countries(self):
|
||||
return self._get("country/retrieve_all", {})
|
||||
|
||||
def list_services(self):
|
||||
return self._get("service/retrieve_all", {})
|
||||
|
||||
def provision_number(self, service, country, expiry_min):
|
||||
return self._get("purchase/sms", {"service": service, "country": country,
|
||||
"expiry": int(expiry_min)})
|
||||
|
||||
def check_sms(self, order_id):
|
||||
return self._get("sms/check", {"orderid": order_id})
|
||||
|
||||
def release_number(self, order_id):
|
||||
return self._get("sms/cancel", {"orderid": order_id})
|
||||
|
||||
def get_balance(self):
|
||||
r = self._get("request/balance", {})
|
||||
try:
|
||||
return float(r.get("balance", 0))
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
|
||||
|
||||
PROVIDER = SMSPoolProvider(SMSPOOL_KEY)
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# Curated popular services (SMSPool IDs, verified 2026-09-20)
|
||||
# --------------------------------------------------------------------------
|
||||
POPULAR_SERVICES = [
|
||||
(395, "Google / Gmail"),
|
||||
(907, "Telegram"),
|
||||
(1012, "WhatsApp"),
|
||||
(948, "Twitter / X"),
|
||||
(457, "Instagram / Threads"),
|
||||
(273, "Discord"),
|
||||
(329, "Facebook / Meta"),
|
||||
(671, "OpenAI / ChatGPT"),
|
||||
(924, "TikTok / Douyin"),
|
||||
(951, "Uber / Postmates"),
|
||||
(39, "Amazon / AWS"),
|
||||
(1072, "Microsoft / Outlook / Bing"),
|
||||
(48, "Apple"),
|
||||
(630, "Netflix"),
|
||||
(926, "Tinder"),
|
||||
(846, "Snapchat"),
|
||||
(829, "Signal"),
|
||||
(523, "LinkedIn"),
|
||||
(1554, "Reddit"),
|
||||
(28, "Airbnb"),
|
||||
(280, "DoorDash"),
|
||||
(233, "Craigslist"),
|
||||
(1034, "Yahoo"),
|
||||
]
|
||||
|
||||
DEFAULT_PRICING = [
|
||||
{"duration_min": 5, "price_sats": 1500},
|
||||
{"duration_min": 10, "price_sats": 2000},
|
||||
{"duration_min": 15, "price_sats": 2500},
|
||||
{"duration_min": 20, "price_sats": 3000},
|
||||
]
|
||||
|
||||
MAX_RENTAL_MIN = 20 # SMSPool rental window ceiling; longer durations re-rent
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# Schema
|
||||
# --------------------------------------------------------------------------
|
||||
SCHEMA = """
|
||||
CREATE TABLE IF NOT EXISTS sms_sessions (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL,
|
||||
service_id INTEGER,
|
||||
service_name TEXT,
|
||||
country_id INTEGER,
|
||||
country_name TEXT,
|
||||
country_cc TEXT,
|
||||
provider_order_id TEXT,
|
||||
number TEXT,
|
||||
status TEXT DEFAULT 'active',
|
||||
price_sats INTEGER,
|
||||
provider_cost TEXT,
|
||||
created_at INTEGER,
|
||||
expires_at INTEGER,
|
||||
sms_received_at INTEGER
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS sms_messages (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
session_id INTEGER NOT NULL,
|
||||
from_number TEXT,
|
||||
body TEXT,
|
||||
detected_code TEXT,
|
||||
received_at INTEGER
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS sms_pricing (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
duration_min INTEGER UNIQUE,
|
||||
price_sats INTEGER,
|
||||
active INTEGER DEFAULT 1
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS sms_countries_cache (
|
||||
id INTEGER PRIMARY KEY,
|
||||
name TEXT,
|
||||
short_name TEXT,
|
||||
cc TEXT,
|
||||
region TEXT
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS sms_services_cache (
|
||||
id INTEGER PRIMARY KEY,
|
||||
name TEXT
|
||||
);
|
||||
"""
|
||||
|
||||
|
||||
def init_sms():
|
||||
d = db.get_db()
|
||||
d.executescript(SCHEMA)
|
||||
if d.execute("SELECT COUNT(*) c FROM sms_pricing").fetchone()["c"] == 0:
|
||||
for p in DEFAULT_PRICING:
|
||||
d.execute("INSERT INTO sms_pricing (duration_min, price_sats, active) VALUES (?,?,1)",
|
||||
(p["duration_min"], p["price_sats"]))
|
||||
d.commit()
|
||||
d.close()
|
||||
|
||||
|
||||
def get_pricing():
|
||||
d = db.get_db()
|
||||
rows = [dict(r) for r in d.execute(
|
||||
"SELECT * FROM sms_pricing WHERE active=1 ORDER BY duration_min")]
|
||||
d.close()
|
||||
return rows
|
||||
|
||||
|
||||
def price_for(duration_min):
|
||||
d = db.get_db()
|
||||
r = d.execute("SELECT price_sats FROM sms_pricing WHERE duration_min=? AND active=1",
|
||||
(duration_min,)).fetchone()
|
||||
d.close()
|
||||
return r["price_sats"] if r else None
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# Code detection (convenience only — never alters the raw SMS)
|
||||
# --------------------------------------------------------------------------
|
||||
_CODE_CONTEXT = re.compile(r"(code|verification|verify|otp|pin|confirm|security|login|auth)", re.I)
|
||||
|
||||
|
||||
def detect_code(body):
|
||||
if not body:
|
||||
return None
|
||||
for m in re.finditer(r"\b(\d{4,8})\b", body):
|
||||
s = max(0, m.start() - 40)
|
||||
ctx = body[s:m.end() + 20]
|
||||
if _CODE_CONTEXT.search(ctx):
|
||||
return m.group(1)
|
||||
m = re.search(r"\b(\d{4,8})\b", body)
|
||||
return m.group(1) if m else None
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# Helpers
|
||||
# --------------------------------------------------------------------------
|
||||
def _client_ip():
|
||||
xff = request.headers.get("X-Forwarded-For", "")
|
||||
return xff.split(",")[0].strip() if xff else (request.remote_addr or "?")
|
||||
|
||||
|
||||
def _rate_ok(ip, limit, window):
|
||||
now = time.time()
|
||||
d = db.get_db()
|
||||
d.execute("DELETE FROM rate_limits WHERE ts < ?", (now - window,))
|
||||
c = d.execute("SELECT COUNT(*) c FROM rate_limits WHERE ip=?", (ip,)).fetchone()["c"]
|
||||
if c >= limit:
|
||||
d.close()
|
||||
return False
|
||||
d.execute("INSERT INTO rate_limits (ip, ts) VALUES (?,?)", (ip, now))
|
||||
d.commit()
|
||||
d.close()
|
||||
return True
|
||||
|
||||
|
||||
def _active_session(user_id):
|
||||
d = db.get_db()
|
||||
r = d.execute(
|
||||
"SELECT * FROM sms_sessions WHERE user_id=? AND status IN ('active','sms_received') "
|
||||
"ORDER BY id DESC LIMIT 1", (user_id,)).fetchone()
|
||||
d.close()
|
||||
return dict(r) if r else None
|
||||
|
||||
|
||||
def _session_messages(session_id):
|
||||
d = db.get_db()
|
||||
rows = [dict(r) for r in d.execute(
|
||||
"SELECT * FROM sms_messages WHERE session_id=? ORDER BY id", (session_id,)).fetchall()]
|
||||
d.close()
|
||||
return rows
|
||||
|
||||
|
||||
def _service_name(service_id):
|
||||
for sid, name in POPULAR_SERVICES:
|
||||
if sid == service_id:
|
||||
return name
|
||||
d = db.get_db()
|
||||
r = d.execute("SELECT name FROM sms_services_cache WHERE id=?", (service_id,)).fetchone()
|
||||
d.close()
|
||||
return r["name"] if r else ("Service %d" % service_id)
|
||||
|
||||
|
||||
def _enabled_countries():
|
||||
d = db.get_db()
|
||||
rows = [dict(r) for r in d.execute(
|
||||
"SELECT * FROM sms_countries_cache ORDER BY (id=1) DESC, name").fetchall()]
|
||||
d.close()
|
||||
if rows:
|
||||
return rows
|
||||
try:
|
||||
data = PROVIDER.list_countries()
|
||||
except Exception:
|
||||
data = []
|
||||
if not isinstance(data, list):
|
||||
data = []
|
||||
d = db.get_db()
|
||||
for c in data:
|
||||
try:
|
||||
d.execute("INSERT OR REPLACE INTO sms_countries_cache (id,name,short_name,cc,region) "
|
||||
"VALUES (?,?,?,?,?)",
|
||||
(c.get("ID"), c.get("name"), c.get("short_name"), c.get("cc"), c.get("region")))
|
||||
except Exception:
|
||||
continue
|
||||
d.commit()
|
||||
d.close()
|
||||
return [{"id": c.get("ID"), "name": c.get("name"), "short_name": c.get("short_name"),
|
||||
"cc": c.get("cc"), "region": c.get("region")} for c in data]
|
||||
|
||||
|
||||
def _all_services():
|
||||
d = db.get_db()
|
||||
rows = [dict(r) for r in d.execute("SELECT * FROM sms_services_cache").fetchall()]
|
||||
d.close()
|
||||
if rows:
|
||||
return rows
|
||||
try:
|
||||
data = PROVIDER.list_services()
|
||||
except Exception:
|
||||
data = []
|
||||
if not isinstance(data, list):
|
||||
data = []
|
||||
d = db.get_db()
|
||||
for s in data:
|
||||
try:
|
||||
d.execute("INSERT OR REPLACE INTO sms_services_cache (id,name) VALUES (?,?)",
|
||||
(s.get("ID"), s.get("name")))
|
||||
except Exception:
|
||||
continue
|
||||
d.commit()
|
||||
d.close()
|
||||
return [{"id": s.get("ID"), "name": s.get("name")} for s in data]
|
||||
|
||||
|
||||
def _extract_sms(chk):
|
||||
"""Return (body, from_number) from a /sms/check response, or (None, None)."""
|
||||
if not isinstance(chk, dict):
|
||||
return None, None
|
||||
sms = chk.get("sms")
|
||||
if isinstance(sms, dict) and sms.get("text"):
|
||||
return sms["text"], (sms.get("number") or sms.get("sender") or "")
|
||||
if chk.get("text"):
|
||||
return chk["text"], (chk.get("number") or chk.get("sender") or "")
|
||||
return None, None
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# Route registration
|
||||
# --------------------------------------------------------------------------
|
||||
def init_sms_app(app, login_required, current_user):
|
||||
init_sms()
|
||||
|
||||
@app.route("/sms")
|
||||
@login_required
|
||||
def sms_home():
|
||||
u = current_user()
|
||||
pricing = get_pricing()
|
||||
countries = _enabled_countries()
|
||||
sess = _active_session(u["id"])
|
||||
bal = db.get_balance_sats(u["id"])
|
||||
return render_template("sms.html", user=u, pricing=pricing,
|
||||
countries=countries, session=sess,
|
||||
messages=_session_messages(sess["id"]) if sess else [],
|
||||
popular=POPULAR_SERVICES, balance=bal)
|
||||
|
||||
@app.route("/sms/services")
|
||||
@login_required
|
||||
def sms_services():
|
||||
q = (request.args.get("q") or "").strip().lower()
|
||||
if q:
|
||||
out = [s for s in _all_services() if q in s["name"].lower()][:30]
|
||||
return jsonify({"services": out})
|
||||
return jsonify({"services": [{"id": i, "name": n} for i, n in POPULAR_SERVICES]})
|
||||
|
||||
@app.route("/sms/rent", methods=["POST"])
|
||||
@login_required
|
||||
def sms_rent():
|
||||
u = current_user()
|
||||
if not _rate_ok(_client_ip(), limit=10, window=300):
|
||||
flash("Too many attempts — slow down.", "warn")
|
||||
return redirect(url_for("sms_home"))
|
||||
if _active_session(u["id"]):
|
||||
flash("You already have an active number. Release it first.", "warn")
|
||||
return redirect(url_for("sms_home"))
|
||||
try:
|
||||
service_id = int(request.form.get("service_id"))
|
||||
country_id = int(request.form.get("country_id"))
|
||||
duration = int(request.form.get("duration"))
|
||||
except (TypeError, ValueError):
|
||||
flash("Bad request.", "warn")
|
||||
return redirect(url_for("sms_home"))
|
||||
|
||||
price = price_for(duration)
|
||||
if price is None:
|
||||
flash("Invalid duration.", "warn")
|
||||
return redirect(url_for("sms_home"))
|
||||
if db.get_balance_sats(u["id"]) < price:
|
||||
flash("Insufficient balance. Deposit BTC in your wallet first.", "warn")
|
||||
return redirect(url_for("wallet"))
|
||||
|
||||
country = next((c for c in _enabled_countries() if c["id"] == country_id), None)
|
||||
if not country:
|
||||
flash("Country unavailable.", "warn")
|
||||
return redirect(url_for("sms_home"))
|
||||
|
||||
try:
|
||||
res = PROVIDER.provision_number(service_id, country_id, min(duration, MAX_RENTAL_MIN))
|
||||
except Exception as e:
|
||||
flash(f"Provider error: {e}", "warn")
|
||||
return redirect(url_for("sms_home"))
|
||||
|
||||
if not res.get("success"):
|
||||
msg = (res.get("errors") or [{}])[0].get("message") or res.get("message") or "No numbers available."
|
||||
flash(f"Number unavailable: {msg}", "warn")
|
||||
return redirect(url_for("sms_home"))
|
||||
|
||||
order_id = res.get("order_id") or res.get("orderid")
|
||||
number = res.get("number") or res.get("phonenumber")
|
||||
cost = res.get("cost", "0")
|
||||
service_name = _service_name(service_id)
|
||||
|
||||
d = db.get_db()
|
||||
d.execute(
|
||||
"INSERT INTO sms_sessions (user_id, service_id, service_name, country_id, "
|
||||
"country_name, country_cc, provider_order_id, number, status, price_sats, "
|
||||
"provider_cost, created_at, expires_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)",
|
||||
(u["id"], service_id, service_name, country_id, country["name"], country["cc"],
|
||||
order_id, number, "active", price, str(cost), int(time.time()),
|
||||
int(time.time()) + duration * 60))
|
||||
d.commit()
|
||||
d.close()
|
||||
db.add_transaction(u["id"], -price, "sms", order_id)
|
||||
flash(f"Number {number} reserved for {duration} min.", "ok")
|
||||
return redirect(url_for("sms_home"))
|
||||
|
||||
@app.route("/sms/session/<int:sid>/poll")
|
||||
@login_required
|
||||
def sms_poll(sid):
|
||||
u = current_user()
|
||||
d = db.get_db()
|
||||
row = d.execute("SELECT * FROM sms_sessions WHERE id=? AND user_id=?", (sid, u["id"])).fetchone()
|
||||
if not row:
|
||||
d.close()
|
||||
return jsonify({"error": "not found"}), 404
|
||||
sess = dict(row)
|
||||
now = int(time.time())
|
||||
|
||||
# lazy expiry -> release upstream
|
||||
if sess["status"] in ("active", "sms_received") and now >= sess["expires_at"]:
|
||||
try:
|
||||
PROVIDER.release_number(sess["provider_order_id"])
|
||||
except Exception:
|
||||
pass
|
||||
d.execute("UPDATE sms_sessions SET status='expired' WHERE id=?", (sid,))
|
||||
d.commit()
|
||||
sess["status"] = "expired"
|
||||
|
||||
if sess["status"] == "active":
|
||||
try:
|
||||
chk = PROVIDER.check_sms(sess["provider_order_id"])
|
||||
except Exception:
|
||||
chk = {}
|
||||
body, from_num = _extract_sms(chk)
|
||||
status = chk.get("status")
|
||||
if body:
|
||||
code = detect_code(body)
|
||||
d.execute(
|
||||
"INSERT INTO sms_messages (session_id, from_number, body, detected_code, received_at) "
|
||||
"VALUES (?,?,?,?,?)", (sid, from_num, body, code, now))
|
||||
d.execute("UPDATE sms_sessions SET status='sms_received', sms_received_at=? WHERE id=?",
|
||||
(now, sid))
|
||||
d.commit()
|
||||
sess["status"] = "sms_received"
|
||||
elif status in (4, 5, 6):
|
||||
d.execute("UPDATE sms_sessions SET status='released' WHERE id=?", (sid,))
|
||||
d.commit()
|
||||
sess["status"] = "released"
|
||||
|
||||
msgs = [dict(r) for r in d.execute(
|
||||
"SELECT * FROM sms_messages WHERE session_id=? ORDER BY id", (sid,)).fetchall()]
|
||||
d.close()
|
||||
return jsonify({
|
||||
"id": sess["id"],
|
||||
"number": sess["number"],
|
||||
"service": sess["service_name"],
|
||||
"country": sess["country_name"],
|
||||
"status": sess["status"],
|
||||
"expires_at": sess["expires_at"],
|
||||
"time_remaining": max(0, sess["expires_at"] - now),
|
||||
"messages": msgs,
|
||||
"code": msgs[-1]["detected_code"] if msgs and msgs[-1]["detected_code"] else None,
|
||||
})
|
||||
|
||||
@app.route("/sms/session/<int:sid>/release", methods=["POST"])
|
||||
@login_required
|
||||
def sms_release(sid):
|
||||
u = current_user()
|
||||
d = db.get_db()
|
||||
row = d.execute("SELECT * FROM sms_sessions WHERE id=? AND user_id=?", (sid, u["id"])).fetchone()
|
||||
if not row:
|
||||
d.close()
|
||||
abort(404)
|
||||
if row["status"] in ("active", "sms_received"):
|
||||
try:
|
||||
PROVIDER.release_number(row["provider_order_id"])
|
||||
except Exception:
|
||||
pass
|
||||
d.execute("UPDATE sms_sessions SET status='released' WHERE id=?", (sid,))
|
||||
d.commit()
|
||||
flash("Number released.", "ok")
|
||||
d.close()
|
||||
return redirect(url_for("sms_home"))
|
||||
|
||||
# ----- admin -----
|
||||
@app.route("/admin/sms", methods=["GET", "POST"])
|
||||
def sms_admin():
|
||||
if not session.get("admin"):
|
||||
abort(403)
|
||||
if request.method == "POST":
|
||||
changed = 0
|
||||
for p in get_pricing():
|
||||
val = (request.form.get("price_%d" % p["duration_min"]) or "").strip()
|
||||
if val:
|
||||
try:
|
||||
price = int(val)
|
||||
d = db.get_db()
|
||||
d.execute("UPDATE sms_pricing SET price_sats=? WHERE duration_min=?",
|
||||
(price, p["duration_min"]))
|
||||
d.commit()
|
||||
d.close()
|
||||
changed += 1
|
||||
except ValueError:
|
||||
continue
|
||||
flash("Updated %d pricing rows." % changed, "ok")
|
||||
return redirect(url_for("sms_admin"))
|
||||
|
||||
d = db.get_db()
|
||||
stats = {
|
||||
"active": d.execute("SELECT COUNT(*) c FROM sms_sessions WHERE status IN ('active','sms_received')").fetchone()["c"],
|
||||
"total_sessions": d.execute("SELECT COUNT(*) c FROM sms_sessions").fetchone()["c"],
|
||||
"sms_received": d.execute("SELECT COUNT(*) c FROM sms_messages").fetchone()["c"],
|
||||
"revenue_sats": d.execute("SELECT COALESCE(SUM(amount_sats),0) s FROM transactions WHERE type='sms'").fetchone()["s"],
|
||||
"provider_cost": d.execute("SELECT COALESCE(SUM(provider_cost),0) s FROM sms_sessions").fetchone()["s"],
|
||||
}
|
||||
sessions = [dict(r) for r in d.execute(
|
||||
"SELECT s.*, u.username FROM sms_sessions s LEFT JOIN users u ON u.id=s.user_id "
|
||||
"ORDER BY s.id DESC LIMIT 50").fetchall()]
|
||||
pricing = get_pricing()
|
||||
d.close()
|
||||
try:
|
||||
smspool_balance = PROVIDER.get_balance()
|
||||
except Exception:
|
||||
smspool_balance = None
|
||||
return render_template("sms_admin.html", stats=stats, sessions=sessions,
|
||||
pricing=pricing, smspool_balance=smspool_balance)
|
||||
Reference in New Issue
Block a user