557 lines
23 KiB
Python
557 lines
23 KiB
Python
import datetime
|
|
import requests, re
|
|
from bs4 import BeautifulSoup
|
|
import os
|
|
|
|
|
|
class ScraperError(Exception):
|
|
"""A cm99.net response did not contain the field we expected.
|
|
|
|
The raw response is saved to logs/scraper-failures/ before this is
|
|
raised; the message identifies which method failed and what was
|
|
being looked for.
|
|
"""
|
|
|
|
|
|
class CM_BOT:
|
|
def __init__(self):
|
|
self.session = requests.Session()
|
|
self.base_url = self._get_required_env('CM_BOT_BASE_URL')
|
|
self.is_logged_in = False
|
|
self._setup_headers()
|
|
|
|
def _get_required_env(self, name: str) -> str:
|
|
value = os.getenv(name)
|
|
if value is None or value == "":
|
|
raise RuntimeError(f"Missing required environment variable: {name}")
|
|
return value
|
|
|
|
def _setup_headers(self):
|
|
"""Set up default headers for requests."""
|
|
|
|
self.login_headers = {
|
|
'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8',
|
|
'accept-encoding': 'identity',
|
|
'accept-language': 'en-GB,en;q=0.8',
|
|
'cache-control': 'max-age=0',
|
|
'content-type': 'application/x-www-form-urlencoded',
|
|
'origin': self.base_url,
|
|
'referer': f'{self.base_url}/cm/login',
|
|
'sec-ch-ua': '"Not;A=Brand";v="99", "Brave";v="139", "Chromium";v="139"',
|
|
'sec-ch-ua-mobile': '?0',
|
|
'sec-ch-ua-platform': '"macOS"',
|
|
'sec-fetch-dest': 'document',
|
|
'sec-fetch-mode': 'navigate',
|
|
'sec-fetch-site': 'same-origin',
|
|
'sec-fetch-user': '?1',
|
|
'sec-gpc': '1',
|
|
'upgrade-insecure-requests': '1',
|
|
'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36'
|
|
}
|
|
|
|
self.get_user_tree_headers = {
|
|
'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8',
|
|
'accept-encoding': 'identity',
|
|
'accept-language': 'en-GB,en;q=0.8',
|
|
'priority': 'u=0, i',
|
|
'sec-ch-ua': '"Not;A=Brand";v="99", "Brave";v="139", "Chromium";v="139"',
|
|
'sec-ch-ua-mobile': '?0',
|
|
'sec-ch-ua-platform': '"macOS"',
|
|
'sec-fetch-dest': 'document',
|
|
'sec-fetch-mode': 'navigate',
|
|
'sec-fetch-site': 'none',
|
|
'sec-fetch-user': '?1',
|
|
'sec-gpc': '1',
|
|
'upgrade-insecure-requests': '1',
|
|
'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36'
|
|
}
|
|
|
|
self.get_register_form_headers = {
|
|
'accept': 'text/html, */*; q=0.01',
|
|
'accept-encoding': 'identity',
|
|
'accept-language': 'en-GB,en;q=0.8',
|
|
'content-length': '0',
|
|
'origin': self.base_url,
|
|
'priority': 'u=1, i',
|
|
'referer': f'{self.base_url}/cm/userMainAction',
|
|
'sec-ch-ua': '"Not;A=Brand";v="99", "Brave";v="139", "Chromium";v="139"',
|
|
'sec-ch-ua-mobile': '?0',
|
|
'sec-ch-ua-platform': '"macOS"',
|
|
'sec-fetch-dest': 'empty',
|
|
'sec-fetch-mode': 'cors',
|
|
'sec-fetch-site': 'same-origin',
|
|
'sec-gpc': '1',
|
|
'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36',
|
|
'x-requested-with': 'XMLHttpRequest'
|
|
}
|
|
|
|
self.get_user_credit_headers = {
|
|
'accept': 'text/html, */*; q=0.01',
|
|
'accept-encoding': 'identity',
|
|
'accept-language': 'en-GB,en;q=0.7',
|
|
'content-length': '0',
|
|
'origin': self.base_url,
|
|
'priority': 'u=1, i',
|
|
'referer': f'{self.base_url}/cm/mainMenu',
|
|
'sec-ch-ua': '"Chromium";v="140", "Not=A?Brand";v="24", "Brave";v="140"',
|
|
'sec-ch-ua-mobile': '?0',
|
|
'sec-ch-ua-platform': '"macOS"',
|
|
'sec-fetch-dest': 'empty',
|
|
'sec-fetch-mode': 'cors',
|
|
'sec-fetch-site': 'same-origin',
|
|
'sec-gpc': '1',
|
|
'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36',
|
|
'x-requested-with': 'XMLHttpRequest'
|
|
}
|
|
|
|
# self.change_pass_headers = {
|
|
# 'accept': '*/*',
|
|
# 'accept-encoding': 'identity',
|
|
# 'accept-language': 'en-GB,en;q=0.8',
|
|
# 'content-length': '889',
|
|
# 'content-type': 'application/x-www-form-urlencoded; charset=UTF-8',
|
|
# 'origin': self.base_url,
|
|
# 'priority': 'u=1, i',
|
|
# 'referer': f'{self.base_url}/cm/userMainAction',
|
|
# 'sec-ch-ua': '"Not;A=Brand";v="99", "Brave";v="139", "Chromium";v="139"',
|
|
# 'sec-ch-ua-mobile': '?0',
|
|
# 'sec-ch-ua-platform': '"macOS"',
|
|
# 'sec-fetch-dest': 'empty',
|
|
# 'sec-fetch-mode': 'cors',
|
|
# 'sec-fetch-site': 'same-origin',
|
|
# 'sec-gpc': '1',
|
|
# 'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36',
|
|
# 'x-requested-with': 'XMLHttpRequest'
|
|
# }
|
|
|
|
self.register_form_headers = {
|
|
'accept': '*/*',
|
|
'accept-encoding': 'identity',
|
|
'accept-language': 'en-GB,en;q=0.8',
|
|
'content-type': 'application/x-www-form-urlencoded; charset=UTF-8',
|
|
'origin': self.base_url,
|
|
'priority': 'u=1, i',
|
|
'referer': f'{self.base_url}/cm/userMainAction',
|
|
'sec-ch-ua': '"Not;A=Brand";v="99", "Brave";v="139", "Chromium";v="139"',
|
|
'sec-ch-ua-mobile': '?0',
|
|
'sec-ch-ua-platform': '"macOS"',
|
|
'sec-fetch-dest': 'empty',
|
|
'sec-fetch-mode': 'cors',
|
|
'sec-fetch-site': 'same-origin',
|
|
'sec-gpc': '1',
|
|
'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36',
|
|
'x-requested-with': 'XMLHttpRequest'
|
|
}
|
|
|
|
self.set_security_pin_headers = {
|
|
'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8',
|
|
'accept-encoding': 'identity',
|
|
'accept-language': 'en-GB,en;q=0.9',
|
|
'cache-control': 'max-age=0',
|
|
'content-length': '103',
|
|
'content-type': 'application/x-www-form-urlencoded',
|
|
'origin': self.base_url,
|
|
'priority': 'u=0, i',
|
|
'referer': f'{self.base_url}/cm/setSecurityPin',
|
|
'sec-ch-ua': '"Chromium";v="140", "Not=A?Brand";v="24", "Brave";v="140"',
|
|
'sec-ch-ua-mobile': '?0',
|
|
'sec-ch-ua-platform': '"macOS"',
|
|
'sec-fetch-dest': 'document',
|
|
'sec-fetch-mode': 'navigate',
|
|
'sec-fetch-site': 'same-origin',
|
|
'sec-fetch-user': '?1',
|
|
'sec-gpc': '1',
|
|
'upgrade-insecure-requests': '1',
|
|
'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36'
|
|
}
|
|
|
|
self.transfer_search_headers = {
|
|
'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8',
|
|
'accept-encoding': 'identity',
|
|
'accept-language': 'en-GB,en;q=0.7',
|
|
'cache-control': 'max-age=0',
|
|
'content-length': '78',
|
|
'content-type': 'application/x-www-form-urlencoded',
|
|
'origin': self.base_url,
|
|
'priority': 'u=0, i',
|
|
'referer': f'{self.base_url}/cm/transfer',
|
|
'sec-ch-ua': '"Chromium";v="140", "Not=A?Brand";v="24", "Brave";v="140"',
|
|
'sec-ch-ua-mobile': '?0',
|
|
'sec-ch-ua-platform': '"macOS"',
|
|
'sec-fetch-dest': 'document',
|
|
'sec-fetch-mode': 'navigate',
|
|
'sec-fetch-site': 'same-origin',
|
|
'sec-fetch-user': '?1',
|
|
'sec-gpc': '1',
|
|
'upgrade-insecure-requests': '1',
|
|
'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36'
|
|
}
|
|
|
|
self.transfer_credit_headers = {
|
|
'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8',
|
|
'accept-encoding': 'identity',
|
|
'accept-language': 'en-GB,en;q=0.7',
|
|
'cache-control': 'max-age=0',
|
|
'content-length': '152',
|
|
'content-type': 'application/x-www-form-urlencoded',
|
|
'origin': self.base_url,
|
|
'priority': 'u=0, i',
|
|
'referer': f'{self.base_url}/cm/searchTransferUser',
|
|
'sec-ch-ua': '"Chromium";v="140", "Not=A?Brand";v="24", "Brave";v="140"',
|
|
'sec-ch-ua-mobile': '?0',
|
|
'sec-ch-ua-platform': '"macOS"',
|
|
'sec-fetch-dest': 'document',
|
|
'sec-fetch-mode': 'navigate',
|
|
'sec-fetch-site': 'same-origin',
|
|
'sec-fetch-user': '?1',
|
|
'sec-gpc': '1',
|
|
'upgrade-insecure-requests': '1',
|
|
'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36'
|
|
}
|
|
|
|
def _dump_html(self, context: str, content) -> str:
|
|
"""Save a failing cm99.net response to logs/scraper-failures/.
|
|
|
|
Returns the path written to so callers can include it in error
|
|
messages.
|
|
"""
|
|
ts = datetime.datetime.now().strftime("%Y%m%d-%H%M%S")
|
|
out_dir = os.path.join("logs", "scraper-failures")
|
|
os.makedirs(out_dir, exist_ok=True)
|
|
path = os.path.join(out_dir, f"{context}-{ts}.html")
|
|
if isinstance(content, (bytes, bytearray)):
|
|
data = bytes(content)
|
|
else:
|
|
data = str(content).encode("utf-8", "replace")
|
|
with open(path, "wb") as f:
|
|
f.write(data)
|
|
print(f"[scraper-failure] dumped {context} response to {path}")
|
|
return path
|
|
|
|
def _find_input_value(self, soup, ident: str, *, context: str, raw, by: str = "name") -> str:
|
|
"""Extract <input {by}=IDENT value=...>'s value or raise ScraperError.
|
|
|
|
`by` selects between matching <input name=...> (default) and
|
|
<input id=...>. Saves the raw response to logs/scraper-failures/
|
|
before raising so the operator can postmortem.
|
|
"""
|
|
el = soup.find("input", {by: ident})
|
|
if el is None or "value" not in el.attrs:
|
|
path = self._dump_html(context, raw)
|
|
raise ScraperError(
|
|
f"{context}: input[{by}={ident!r}] missing or has no value attribute "
|
|
f"(response saved to {path})"
|
|
)
|
|
return el["value"]
|
|
|
|
def get_register_data(self, token: str, username: str, password: str):
|
|
return {
|
|
'struts.token.name': 'token',
|
|
'token': f'{token}',
|
|
'userIsNew': 'true',
|
|
'searchContent': '',
|
|
'user.username': f'{username}',
|
|
'user.companyId': '1',
|
|
'user.userRoleId': '4',
|
|
'user.password': f'{password}',
|
|
'user.confirmPassword': f'{password}',
|
|
'user.name': f'{username}',
|
|
'user.mobileNumber': '',
|
|
'user.email': '',
|
|
'user.remarks': '',
|
|
'user.accountStatus': 'A',
|
|
'user.currencyMYR': 'true',
|
|
'__checkbox_user.currencyMYR': 'true',
|
|
'user.allowBetHl': 'true',
|
|
'__checkbox_user.allowBetHl': 'true',
|
|
'user.commission3d4d': '5.00',
|
|
'user.commission5d6d': '5.00',
|
|
'user.commissionHL': '19.00',
|
|
'user.commissionHL6d': '19.00',
|
|
'user.commissionNL': '19.00',
|
|
'user.commissionNL6d': '19.00',
|
|
'checkAllPrizePackages': 'on',
|
|
'selectedPrizePackageList': ['6', '1', '5', '3'],
|
|
'__multiselect_selectedPrizePackageList': '',
|
|
'checkAllPrizePackages5D6D': 'on',
|
|
'selectedPrizePackage5D6DList': '2',
|
|
'__multiselect_selectedPrizePackage5D6DList': ''
|
|
}
|
|
|
|
def get_security_pin_data(self, token: str, security_pin: str):
|
|
return {
|
|
'struts.token.name': 'token',
|
|
'token': token,
|
|
'newPin': security_pin,
|
|
'confirmNewPin': security_pin
|
|
}
|
|
|
|
def get_transfer_search_data(self, token: str, username: str):
|
|
return {
|
|
'struts.token.name': 'token',
|
|
'token': token,
|
|
'username': username
|
|
}
|
|
|
|
def get_transfer_data(self, token: str, username: str, name: str, toUserId: str, amount: float, security_pin: str):
|
|
return {
|
|
'struts.token.name': 'token',
|
|
'token': token,
|
|
'username': username,
|
|
'name': name,
|
|
'toUserId': toUserId,
|
|
'amount': amount,
|
|
'securityPin': security_pin
|
|
}
|
|
|
|
# def get_change_pass_data(self, token: str, user_encrypted_id: str, username: str, password: str):
|
|
# return {
|
|
# 'struts.token.name': 'token',
|
|
# 'token': f'{token}',
|
|
# 'searchContent': '',
|
|
# 'userIsNew': 'false',
|
|
# 'user.encryptedId': f'{user_encrypted_id}',
|
|
# 'user.username': f'{username}',
|
|
# 'user.parentId': '31308',
|
|
# 'user.companyId': '1',
|
|
# 'user.userRoleId': '4',
|
|
# 'user.password': f'{password}',
|
|
# 'user.confirmPassword': f'{password}',
|
|
# 'user.name': f'{username}',
|
|
# 'user.mobileNumber': '',
|
|
# 'user.email': '',
|
|
# 'user.remarks': '',
|
|
# 'user.accountStatus': 'A',
|
|
# 'user.currencyMYR': 'true',
|
|
# '__checkbox_user.currencyMYR': 'true',
|
|
# 'user.allowBetHl': 'true',
|
|
# '__checkbox_user.allowBetHl': 'true',
|
|
# 'user.balance': '1',
|
|
# 'user.outstanding': '1',
|
|
# 'user.commission3d4d': '5.00',
|
|
# 'user.commission5d6d': '5.00',
|
|
# 'user.commissionHL': '19.00',
|
|
# 'user.commissionHL6d': '19.00',
|
|
# 'user.commissionNL': '19.00',
|
|
# 'user.commissionNL6d': '19.00',
|
|
# 'selectedPrizePackageList': ['6', '1', '5', '3'],
|
|
# '__multiselect_selectedPrizePackageList': '',
|
|
# 'selectedPrizePackage5D6DList': '2',
|
|
# '__multiselect_selectedPrizePackage5D6DList': ''
|
|
# }
|
|
|
|
def login(self, username: str, password: str):
|
|
try:
|
|
# print("Starting login process...")
|
|
login_page = self.session.get(f'{self.base_url}/cm/login')
|
|
# print(f"Login page status: {login_page.status_code}")
|
|
|
|
login_data = {
|
|
'j_username': username,
|
|
'j_password': password
|
|
}
|
|
|
|
login_response = self.session.post(
|
|
f'{self.base_url}/cm/j_security_check',
|
|
data=login_data,
|
|
headers=self.login_headers,
|
|
allow_redirects=True
|
|
)
|
|
|
|
if login_response.status_code == 200 and 'login' not in login_response.url.lower():
|
|
# print("Login successful!")
|
|
self.is_logged_in = True
|
|
return True
|
|
else:
|
|
print("Login failed!")
|
|
self.is_logged_in = False
|
|
return False
|
|
|
|
except requests.exceptions.RequestException as e:
|
|
print(f"Error during login: {e}")
|
|
self.is_logged_in = False
|
|
return False
|
|
|
|
def get_max_user_in_pattern(self, prefix_pattern: str = None):
|
|
if prefix_pattern is None:
|
|
prefix_pattern = self._get_required_env("CM_PREFIX_PATTERN")
|
|
response = self.session.get(f'{self.base_url}/cm/json/generateUserTree?id=0')
|
|
regex = f'\\[{prefix_pattern}\\d+]'
|
|
matches = re.findall(regex, response.text)
|
|
last_match_text = matches[-1]
|
|
number_part = last_match_text.replace(f"[{prefix_pattern}", "").replace("]", "")
|
|
last_match = int(number_part)
|
|
return last_match
|
|
|
|
def get_register_form_token(self):
|
|
try:
|
|
response = self.session.post(
|
|
f'{self.base_url}/cm/loadUserAccount',
|
|
headers=self.get_register_form_headers
|
|
)
|
|
soup = BeautifulSoup(response.content, 'html.parser')
|
|
return self._find_input_value(
|
|
soup, "token",
|
|
context="register_form_token",
|
|
raw=response.content,
|
|
)
|
|
except requests.exceptions.RequestException as e:
|
|
print(f"Error getting register form: {e}")
|
|
return None
|
|
|
|
|
|
def get_security_pin_form_token(self):
|
|
response = self.session.get(f'{self.base_url}/cm/setSecurityPin')
|
|
soup = BeautifulSoup(response.content, 'html.parser')
|
|
return self._find_input_value(
|
|
soup, "token",
|
|
context="security_pin_form_token",
|
|
raw=response.content,
|
|
)
|
|
|
|
def register_user(self, user_id, user_password):
|
|
try:
|
|
print("Creating user account...")
|
|
token = self.get_register_form_token()
|
|
user_data = self.get_register_data(token, user_id, user_password)
|
|
response = self.session.post(
|
|
f'{self.base_url}/cm/saveUserAccount',
|
|
data=user_data,
|
|
headers=self.register_form_headers
|
|
)
|
|
|
|
if re.search('User created successfully', response.text):
|
|
print(f"User account: {user_id} password: {user_password} creation completed!")
|
|
return True
|
|
else:
|
|
print(f"User account: {user_id} creation FAIL!")
|
|
return False
|
|
|
|
except requests.exceptions.RequestException as e:
|
|
print(f"Error creating user account: {e}")
|
|
return False
|
|
|
|
# def change_user_password(self, user_id: str, new_user_pass: str):
|
|
# try:
|
|
# print(f"Changing user: {user_id} password...")
|
|
# self.login(self.username, self.password)
|
|
# token = self.get_register_form_token()
|
|
# headers = self.change_pass_headers
|
|
|
|
# user_data = self.get_change_pass_data(token, )
|
|
# response = self.session.post(
|
|
# f'{self.base_url}/cm/saveUserAccount',
|
|
# data=user_data,
|
|
# headrs=headers
|
|
# )
|
|
|
|
# except requests.exceptions.RequestException as e:
|
|
# print(f"Error change user password: {e}")
|
|
# return None
|
|
|
|
def get_register_link(self):
|
|
response = self.session.get(f"{self.base_url}/cm/showQrCode")
|
|
soup = BeautifulSoup(response.content, 'html.parser')
|
|
form = soup.find('form', {'id': 'qrCodeForm'})
|
|
if form is None:
|
|
path = self._dump_html("register_link_form", response.content)
|
|
raise ScraperError(
|
|
f"register_link: form#qrCodeForm not found "
|
|
f"(response saved to {path})"
|
|
)
|
|
anchor = form.find('a')
|
|
if anchor is None or 'href' not in anchor.attrs:
|
|
path = self._dump_html("register_link_anchor", response.content)
|
|
raise ScraperError(
|
|
f"register_link: <a href> inside form#qrCodeForm not found "
|
|
f"(response saved to {path})"
|
|
)
|
|
return anchor['href']
|
|
|
|
def get_generate_username(self, max_username_index: int):
|
|
max_username_index += 1
|
|
if max_username_index % 10 == 4:
|
|
max_username_index += 1
|
|
return max_username_index
|
|
|
|
def set_security_pin(self, security_pin: str):
|
|
token = self.get_security_pin_form_token()
|
|
security_data = self.get_security_pin_data(token, security_pin)
|
|
response = self.session.post(
|
|
f'{self.base_url}/cm/saveSecurityPin',
|
|
data=security_data,
|
|
headers=self.set_security_pin_headers
|
|
)
|
|
if 'java.lang.NullPointerException' in response.text:
|
|
return False
|
|
return True
|
|
|
|
def transfer_credit(self, t_username: str, t_password: str, amount: float):
|
|
token = self.get_transfer_token()
|
|
transfer_search_data = self.get_transfer_search_data(token, t_username)
|
|
response = self.session.post(
|
|
f'{self.base_url}/cm/searchTransferUser',
|
|
data=transfer_search_data,
|
|
headers=self.transfer_search_headers
|
|
)
|
|
soup = BeautifulSoup(response.content, 'html.parser')
|
|
name = self._find_input_value(
|
|
soup, "name",
|
|
context="transfer_search_name",
|
|
raw=response.content,
|
|
by="id",
|
|
)
|
|
token = self._find_input_value(
|
|
soup, "token",
|
|
context="transfer_search_token",
|
|
raw=response.content,
|
|
)
|
|
toUserId = self._find_input_value(
|
|
soup, "toUserId",
|
|
context="transfer_search_toUserId",
|
|
raw=response.content,
|
|
by="id",
|
|
)
|
|
transfer_data = self.get_transfer_data(token, t_username, name, toUserId, amount, t_password)
|
|
response = self.session.post(
|
|
f'{self.base_url}/cm/saveTransfer',
|
|
data=transfer_data,
|
|
headers=self.transfer_credit_headers
|
|
)
|
|
return True if re.search(r'Successfully saved the record\.', response.text) else False
|
|
|
|
def get_user_credit(self):
|
|
response = self.session.post(
|
|
f'{self.base_url}/cm/userProfile',
|
|
headers=self.get_user_credit_headers
|
|
)
|
|
soup = BeautifulSoup(response.content, 'html.parser')
|
|
try:
|
|
return round(float(soup.find('table', {'class': 'generalContent'}).find(text=re.compile('Credit Available')).parent.parent.find_all('td')[2].text.replace(",","")), 2)
|
|
except Exception as exc:
|
|
self._dump_html("get_user_credit", response.content)
|
|
print(f"Error getting credit: {exc}")
|
|
return 0
|
|
|
|
def get_transfer_token(self):
|
|
response = self.session.get(f'{self.base_url}/cm/transfer')
|
|
soup = BeautifulSoup(response.content, 'html.parser')
|
|
return self._find_input_value(
|
|
soup, "token",
|
|
context="transfer_token",
|
|
raw=response.content,
|
|
)
|
|
|
|
def logout(self):
|
|
"""Logout from the system."""
|
|
self.session.close()
|
|
self.is_logged_in = False
|
|
print("Logged out successfully.")
|
|
|
|
|
|
def main():
|
|
print("CM_BOT helper module. Use from service entrypoints instead of running direct debug actions.")
|
|
|
|
if __name__ == "__main__":
|
|
main()
|