- 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
180 lines
7.7 KiB
HTML
180 lines
7.7 KiB
HTML
{% extends "base.html" %}
|
|
{% block title %}Temp SMS — Clean Proxys{% endblock %}
|
|
{% block body %}
|
|
<div class="sms-wrap">
|
|
<header class="sms-head">
|
|
<h1>Temporary <b>SMS</b></h1>
|
|
<p class="sub">Rent a real number for a few minutes to receive a verification code. No outbound. Pay with your wallet balance.</p>
|
|
<p class="disclaimer">Availability depends on the service you're verifying with. Some services reject temporary, VoIP, or recycled numbers.</p>
|
|
</header>
|
|
|
|
{% if session %}
|
|
<section class="card sms-active" id="active-session" data-sid="{{ session.id }}">
|
|
<div class="sms-number-row">
|
|
<div>
|
|
<span class="label">NUMBER</span>
|
|
<div class="sms-number" id="s-number">+{{ session.country_cc }} {{ session.number }}</div>
|
|
<div class="sms-meta">{{ session.service }} · {{ session.country_name }}</div>
|
|
</div>
|
|
<div class="sms-timer">
|
|
<span class="label">TIME REMAINING</span>
|
|
<div class="sms-count" id="s-time">--:--</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="sms-status-row">
|
|
<span class="status-pill" id="s-status">{{ session.status }}</span>
|
|
<span class="code-out" id="s-code"></span>
|
|
</div>
|
|
|
|
<div class="sms-msgs" id="s-messages">
|
|
<div class="sms-waiting" id="s-waiting">Waiting for incoming message…</div>
|
|
</div>
|
|
|
|
<div class="sms-actions">
|
|
<form method="POST" action="/sms/session/{{ session.id }}/release">
|
|
<button class="btn ghost" type="submit">Release number</button>
|
|
</form>
|
|
</div>
|
|
</section>
|
|
{% endif %}
|
|
|
|
<section class="card sms-form">
|
|
<h2>Get a number</h2>
|
|
<form method="POST" action="/sms/rent" class="sms-form-grid">
|
|
<div class="field">
|
|
<label>Country</label>
|
|
<select name="country_id" required>
|
|
{% for c in countries %}
|
|
<option value="{{ c.id }}">{{ c.name }} (+{{ c.cc }})</option>
|
|
{% endfor %}
|
|
</select>
|
|
</div>
|
|
<div class="field">
|
|
<label>Service</label>
|
|
<select name="service_id" id="service-select" required>
|
|
{% for sid, name in popular %}
|
|
<option value="{{ sid }}">{{ name }}</option>
|
|
{% endfor %}
|
|
</select>
|
|
</div>
|
|
<div class="field">
|
|
<label>Duration</label>
|
|
<select name="duration" id="duration-select" required>
|
|
{% for p in pricing %}
|
|
<option value="{{ p.duration_min }}" data-sats="{{ p.price_sats }}">{{ p.duration_min }} min</option>
|
|
{% endfor %}
|
|
</select>
|
|
</div>
|
|
<div class="field price-field">
|
|
<label>Price</label>
|
|
<div class="sms-price" id="price-out">— sats</div>
|
|
<div class="balance-note">Wallet: {{ balance }} sats</div>
|
|
</div>
|
|
<button class="btn primary" type="submit" id="rent-btn">GET NUMBER</button>
|
|
</form>
|
|
</section>
|
|
</div>
|
|
|
|
<style>
|
|
.sms-wrap{max-width:720px;margin:0 auto;padding:24px 0}
|
|
.sms-head h1{font-size:2.4rem;letter-spacing:-.5px}
|
|
.sms-head h1 b{color:var(--accent,#0ff)}
|
|
.sms-head .sub{color:var(--text-muted,#94a3b8);margin:8px 0}
|
|
.sms-head .disclaimer{font-size:.85rem;color:#f59e0b;margin-top:8px}
|
|
.card{background:rgba(20,28,40,.6);border:1px solid rgba(255,255,255,.08);border-radius:16px;padding:22px;margin:18px 0}
|
|
.sms-number-row{display:flex;justify-content:space-between;align-items:flex-start;gap:16px;flex-wrap:wrap}
|
|
.label{font-size:.72rem;letter-spacing:.12em;color:var(--text-muted,#94a3b8);text-transform:uppercase}
|
|
.sms-number{font-family:'JetBrains Mono',monospace;font-size:2rem;font-weight:700;color:#fff}
|
|
.sms-meta{color:var(--text-muted,#94a3b8);font-size:.9rem;margin-top:4px}
|
|
.sms-timer{text-align:right}
|
|
.sms-count{font-family:'JetBrains Mono',monospace;font-size:2rem;font-weight:700;color:var(--accent,#0ff)}
|
|
.sms-status-row{display:flex;align-items:center;gap:12px;margin:18px 0 12px}
|
|
.status-pill{font-family:'JetBrains Mono',monospace;font-size:.78rem;padding:4px 12px;border-radius:999px;background:rgba(0,229,255,.12);color:#0ff;text-transform:uppercase;letter-spacing:.06em}
|
|
.code-out{font-family:'JetBrains Mono',monospace;font-size:1.3rem;color:#4ade80}
|
|
.sms-msgs{background:rgba(0,0,0,.3);border-radius:10px;padding:14px;min-height:70px}
|
|
.sms-waiting{color:var(--text-muted,#94a3b8)}
|
|
.sms-msg{margin-bottom:10px}
|
|
.sms-msg .from{font-size:.75rem;color:var(--text-muted,#94a3b8)}
|
|
.sms-msg .body{font-family:'JetBrains Mono',monospace;color:#e2e8f0;margin-top:2px}
|
|
.sms-msg .code-tag{display:inline-block;margin-top:6px;background:rgba(74,222,128,.15);color:#4ade80;padding:2px 10px;border-radius:6px;font-family:'JetBrains Mono',monospace}
|
|
.sms-actions{margin-top:14px}
|
|
.sms-form-grid{display:grid;grid-template-columns:1fr 1fr;gap:14px}
|
|
.field{display:flex;flex-direction:column;gap:6px}
|
|
.field label{font-size:.8rem;color:var(--text-muted,#94a3b8)}
|
|
select{background:rgba(0,0,0,.4);border:1px solid rgba(255,255,255,.12);color:#fff;padding:11px;border-radius:10px;font-family:'JetBrains Mono',monospace}
|
|
.sms-price{font-family:'JetBrains Mono',monospace;font-size:1.4rem;color:#0ff}
|
|
.balance-note{font-size:.78rem;color:var(--text-muted,#94a3b8)}
|
|
.btn{font-family:inherit;font-weight:700;border:none;border-radius:10px;padding:12px 18px;cursor:pointer;letter-spacing:.02em}
|
|
.btn.primary{background:linear-gradient(135deg,#0ff,#0af);color:#00141a}
|
|
.btn.ghost{background:transparent;border:1px solid rgba(255,255,255,.2);color:#fff}
|
|
.btn:disabled{opacity:.5;cursor:not-allowed}
|
|
</style>
|
|
|
|
<script>
|
|
(function(){
|
|
// price preview
|
|
const dur = document.getElementById('duration-select');
|
|
const priceOut = document.getElementById('price-out');
|
|
function updPrice(){
|
|
const o = dur.options[dur.selectedIndex];
|
|
if (o && o.dataset.sats) priceOut.textContent = o.dataset.sats + ' sats';
|
|
}
|
|
if (dur){ dur.addEventListener('change', updPrice); updPrice(); }
|
|
|
|
// live session polling
|
|
const active = document.getElementById('active-session');
|
|
if (active){
|
|
const sid = active.dataset.sid;
|
|
const tEl = document.getElementById('s-time');
|
|
const sEl = document.getElementById('s-status');
|
|
const cEl = document.getElementById('s-code');
|
|
const mEl = document.getElementById('s-messages');
|
|
const wEl = document.getElementById('s-waiting');
|
|
let lastMsgs = 0;
|
|
|
|
function fmt(s){
|
|
s = Math.max(0, s);
|
|
const m = Math.floor(s/60), ss = s%60;
|
|
return (m<10?'0':'')+m+':'+(ss<10?'0':'')+ss;
|
|
}
|
|
|
|
function poll(){
|
|
fetch('/sms/session/'+sid+'/poll', {cache:'no-store'})
|
|
.then(r => r.json())
|
|
.then(d => {
|
|
if (!d || d.error) return;
|
|
tEl.textContent = fmt(d.time_remaining);
|
|
sEl.textContent = d.status;
|
|
if (d.messages && d.messages.length){
|
|
if (wEl) wEl.remove();
|
|
if (d.messages.length !== lastMsgs){
|
|
lastMsgs = d.messages.length;
|
|
mEl.innerHTML = '';
|
|
d.messages.forEach(msg => {
|
|
const div = document.createElement('div');
|
|
div.className = 'sms-msg';
|
|
div.innerHTML = '<div class="from">FROM +' + (msg.from_number||'?') + '</div>' +
|
|
'<div class="body"></div>' +
|
|
(msg.detected_code ? '<span class="code-tag">' + msg.detected_code + '</span>' : '');
|
|
div.querySelector('.body').textContent = msg.body;
|
|
mEl.appendChild(div);
|
|
});
|
|
}
|
|
cEl.textContent = d.code ? ('CODE: ' + d.code) : '';
|
|
}
|
|
if (d.status === 'expired' || d.status === 'released'){
|
|
sEl.textContent = d.status + ' — refresh';
|
|
setTimeout(() => location.reload(), 1500);
|
|
return;
|
|
}
|
|
setTimeout(poll, 4000);
|
|
})
|
|
.catch(() => setTimeout(poll, 5000));
|
|
}
|
|
poll();
|
|
}
|
|
})();
|
|
</script>
|
|
{% endblock %}
|