cm_bot_v2/app/cm_bot.py

546 lines
22 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')
soup = soup.find('form', {'id': 'qrCodeForm'})
return soup.find('a')['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:
print(f"Error getting credit.")
now = datetime.datetime.now().strftime('%Y%m%d_%H%M')
# with open(f'credit-{now}.html', 'wb') as f:
# f.write(response.content)
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()